183 lines
5.8 KiB
Go
183 lines
5.8 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/braille"
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
|
)
|
|
|
|
type modeTestSpeaker struct{ spoken chan string }
|
|
|
|
func (*modeTestSpeaker) Connect(context.Context) error { return nil }
|
|
func (*modeTestSpeaker) Close(context.Context) error { return nil }
|
|
func (*modeTestSpeaker) Cancel(context.Context) error { return nil }
|
|
func (speaker *modeTestSpeaker) Speak(_ context.Context, text string) error {
|
|
speaker.spoken <- text
|
|
return nil
|
|
}
|
|
|
|
type modeTestBraille struct{ messages chan string }
|
|
|
|
func (*modeTestBraille) EnqueueFrame(braille.Frame) {}
|
|
func (display *modeTestBraille) Flash(text string, _ config.BrailleOptions) bool {
|
|
display.messages <- text
|
|
return true
|
|
}
|
|
|
|
func TestSpeechModesCoverEveryDeliveryPath(t *testing.T) {
|
|
for _, mode := range []speechMode{speechModeBeeps, speechModeOff} {
|
|
t.Run(mode.announcement(), func(t *testing.T) {
|
|
speaker := &fakeSpeaker{spoken: make(chan string, 10)}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
worker.mode = mode
|
|
beeps, began := 0, false
|
|
worker.beep = func(context.Context) error { beeps++; return nil }
|
|
for _, request := range []speechRequest{
|
|
{Text: "Focused button"},
|
|
{Text: "A", Character: true},
|
|
{Text: "book", Spelling: []string{"b", "o", "o", "k"}, Character: true},
|
|
{Text: "word", CustomEcho: true},
|
|
{Text: "External message", Cause: "remote-speak"},
|
|
{ReadAll: []readAllPart{{Text: "First line", Began: func() { began = true }}}},
|
|
} {
|
|
if err := worker.deliver(context.Background(), request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if len(speaker.spoken) != 0 || speaker.canceled || began {
|
|
t.Fatal("silent/beeps mode used synth, canceled the mode announcement, or advanced Say All")
|
|
}
|
|
wantBeeps := 0
|
|
if mode == speechModeBeeps {
|
|
wantBeeps = 6
|
|
}
|
|
if beeps != wantBeeps {
|
|
t.Fatalf("beeps = %d, want %d", beeps, wantBeeps)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpeechModeCommandAnnouncesAndPreservesBraille(t *testing.T) {
|
|
speaker := &modeTestSpeaker{spoken: make(chan string, 5)}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
beeps := make(chan struct{}, 5)
|
|
worker.beep = func(context.Context) error { beeps <- struct{}{}; return nil }
|
|
if err := worker.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer worker.Stop(context.Background())
|
|
settings := config.DefaultSettings()
|
|
display := &modeTestBraille{messages: make(chan string, 10)}
|
|
live := &liveSettings{}
|
|
live.Store(settings)
|
|
output := outputQueue{speech: worker, braille: display, live: live}
|
|
commands := &fakeCommands{commands: make(chan input.CommandEvent)}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- (commandRuntime{logger: slog.New(slog.DiscardHandler), commands: commands,
|
|
focusGate: &focusGate{}, focusChanges: make(chan xorg.FocusState),
|
|
speechMessages: worker, speechOutput: output}).run(ctx)
|
|
}()
|
|
for index, want := range []string{"Speech Mode Beeps", "Speech off", "Speech Mode Speech"} {
|
|
commands.commands <- input.CommandEvent{Command: input.CycleSpeechMode}
|
|
select {
|
|
case got := <-speaker.spoken:
|
|
if got != want {
|
|
t.Fatalf("announcement = %q, want %q", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("mode announcement missing")
|
|
}
|
|
select {
|
|
case got := <-display.messages:
|
|
if got != want {
|
|
t.Fatalf("Braille = %q, want %q", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Braille mode announcement missing")
|
|
}
|
|
result := make(chan error, 1)
|
|
output.Enqueue(speechRequest{Text: "Ordinary message", Role: "system", Append: true, result: result})
|
|
select {
|
|
case err := <-result:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("ordinary message did not complete")
|
|
}
|
|
if got := <-display.messages; got != "Ordinary message" {
|
|
t.Fatalf("ordinary Braille = %q", got)
|
|
}
|
|
switch index {
|
|
case 0:
|
|
if len(beeps) != 1 {
|
|
t.Fatalf("beeps = %d, want 1", len(beeps))
|
|
}
|
|
case 1:
|
|
if len(beeps) != 1 || len(speaker.spoken) != 0 {
|
|
t.Fatal("off mode emitted output")
|
|
}
|
|
case 2:
|
|
if got := <-speaker.spoken; got != "Ordinary message" {
|
|
t.Fatalf("speech did not resume: %q", got)
|
|
}
|
|
}
|
|
}
|
|
cancel()
|
|
<-done
|
|
}
|
|
|
|
func TestSpeechModeSwitchCancelsActiveBeepAndDropsPendingSpeech(t *testing.T) {
|
|
speaker := &modeTestSpeaker{spoken: make(chan string, 2)}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
worker.mode = speechModeBeeps
|
|
started := make(chan struct{})
|
|
worker.beep = func(ctx context.Context) error { close(started); <-ctx.Done(); return ctx.Err() }
|
|
if err := worker.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer worker.Stop(context.Background())
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() { done <- worker.EnqueueAndWait(ctx, speechRequest{Text: "Active"}) }()
|
|
select {
|
|
case <-started:
|
|
case <-ctx.Done():
|
|
t.Fatal("beep did not start")
|
|
}
|
|
worker.Enqueue(speechRequest{Text: "Queued"})
|
|
if got := worker.cycle_speech_mode(); got != speechModeOff {
|
|
t.Fatalf("mode = %v", got)
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("beep result = %v", err)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("mode switch did not cancel beep")
|
|
}
|
|
if err := worker.EnqueueAndWait(ctx, speechRequest{Text: "Speech off", ForceSpeech: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := <-speaker.spoken; got != "Speech off" {
|
|
t.Fatalf("retained stale speech: %q", got)
|
|
}
|
|
}
|