161 lines
5.3 KiB
Go
161 lines
5.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/preferences"
|
|
)
|
|
|
|
func TestEveryEchoPreferenceRoundTrips(t *testing.T) {
|
|
settings := config.DefaultSettings()
|
|
if !settings.EnableKeyEcho {
|
|
t.Fatal("key echo must default on")
|
|
}
|
|
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
|
|
values := preferences.FromSettings(settings)
|
|
// Flip each echo switch, including false defaults, to exercise both directions.
|
|
value := reflect.ValueOf(&values).Elem()
|
|
for i := 0; i < value.NumField(); i++ {
|
|
if value.Field(i).Kind() == reflect.Bool {
|
|
value.Field(i).SetBool(!value.Field(i).Bool())
|
|
}
|
|
}
|
|
rate, pitch, volume := 100.0, 10.0, 10.0
|
|
values.EchoVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &volume, Family: &config.VoiceFamily{Name: "Test Voice", Lang: "en", Variant: "test"}}
|
|
values.EchoSpeechServer = "test-module"
|
|
next, err := values.Merge(settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store, err := config.NewStore(settings.Path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = store.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = store.Save(next); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := config.Load(settings.Path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := preferences.FromSettings(loaded); !reflect.DeepEqual(got, values) {
|
|
t.Fatalf("round trip = %+v; want %+v", got, values)
|
|
}
|
|
}
|
|
|
|
func TestApplyUpdatesRunningTextAndKeyboardEcho(t *testing.T) {
|
|
settings := config.DefaultSettings()
|
|
settings.EnableModifierKeys = false
|
|
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
|
|
live := &liveSettings{}
|
|
live.Store(settings)
|
|
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, nil)
|
|
worker.live = live
|
|
queue := &fakeSpeechQueue{spoken: make(chan string, 10), interrupt: make(chan struct{}, 4)}
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 10)}
|
|
listener := &fakeInputListener{events: make(chan input.Event, 10), modifierKeycodes: input.NewModifierKeycodes(37)}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{}, 2)
|
|
go func() {
|
|
processEventsWithSettings(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, nil, 0, live)
|
|
done <- struct{}{}
|
|
}()
|
|
go func() {
|
|
processInputEvents(ctx, slog.New(slog.DiscardHandler), queue, listener, inputEchoSettingsFromConfig(settings), live, nil)
|
|
done <- struct{}{}
|
|
}()
|
|
defer func() { cancel(); <-done; <-done }()
|
|
expect := func(want string) {
|
|
t.Helper()
|
|
select {
|
|
case got := <-queue.spoken:
|
|
if got != want {
|
|
t.Fatalf("speech %q, want %q", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("missing %q", want)
|
|
}
|
|
}
|
|
snapshot := atspi.NewSnapshot("/editor", "", "text", "", "")
|
|
snapshot.Editable = true
|
|
insert := func(text string) {
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, snapshot, text)
|
|
}
|
|
insert("a")
|
|
expect("a")
|
|
listener.events <- input.NewKeyEvent(67, true, "F1")
|
|
expect("f1")
|
|
<-queue.interrupt
|
|
values := preferences.FromSettings(settings)
|
|
values.EnableKeyEcho = false
|
|
// Echo-only changes must work without a speech settings updater.
|
|
if r := worker.apply(ctx, preferences.Request{Values: values, Save: true}); !r.Accepted || r.Close {
|
|
t.Fatalf("Apply = %+v", r)
|
|
}
|
|
insert("b")
|
|
insert("barrier")
|
|
expect("barrier")
|
|
listener.events <- input.NewKeyEvent(68, true, "F2")
|
|
listener.events <- input.NewKeyEvent(37, true, "Control_L")
|
|
for range 2 {
|
|
select {
|
|
case <-queue.interrupt:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("keyboard barrier missing")
|
|
}
|
|
}
|
|
values.EnableKeyEcho = true
|
|
if r := worker.apply(ctx, preferences.Request{Values: values, Save: true, Close: true}); !r.Accepted || !r.Close {
|
|
t.Fatalf("OK = %+v", r)
|
|
}
|
|
insert("c")
|
|
expect("c")
|
|
listener.events <- input.NewKeyEvent(69, true, "F3")
|
|
expect("f3")
|
|
loaded, err := config.Load(settings.Path)
|
|
if err != nil || !loaded.EnableKeyEcho {
|
|
t.Fatalf("saved state: %+v, %v", loaded, err)
|
|
}
|
|
}
|
|
|
|
func TestCustomEchoVoiceInheritanceAndRouting(t *testing.T) {
|
|
settings := config.DefaultSettings()
|
|
settings.UseCustomEchoVoice = true
|
|
rate, pitch, volume := 65.0, 10.0, 10.0
|
|
settings.DefaultVoice = config.Voice{Rate: &rate, Family: &config.VoiceFamily{Name: "base", Lang: "en"}}
|
|
settings.EchoVoice = config.Voice{AveragePitch: &pitch, Gain: &volume}
|
|
settings.UseCustomEchoSpeechServer = true
|
|
settings.EchoSpeechServer = "echo-module"
|
|
got := echoSpeechSettings(settings)
|
|
if *got.Rate != 32 || *got.Pitch != 90 || *got.Volume != 100 || got.OutputModule != "echo-module" || got.SynthesisVoice != "base" {
|
|
t.Fatalf("echo settings = %+v", got)
|
|
}
|
|
if !customEcho(settings, "key") || !customEcho(settings, "character") || customEcho(settings, "word") || customEcho(settings, "sentence") || customEcho(settings, "") {
|
|
t.Fatal("wrong default custom voice routing")
|
|
}
|
|
settings.UseCustomEchoForWord = true
|
|
settings.UseCustomEchoForSentence = true
|
|
if !customEcho(settings, "word") || !customEcho(settings, "sentence") {
|
|
t.Fatal("custom routing flags ignored")
|
|
}
|
|
settings.EnableKeyEcho = false
|
|
if customEcho(settings, "key") {
|
|
t.Fatal("custom echo survived master disable")
|
|
}
|
|
}
|