Files

178 lines
6.8 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package preferences
import (
"bufio"
"errors"
"fmt"
"net"
"path/filepath"
"testing"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/speech"
)
func TestModuleDiscoveryAndOfflinePreservation(t *testing.T) {
base := config.Defaults()
base.Presentation.Speech.Module = "saved-module"
t.Setenv("SPEECHD_ADDRESS", "unix_socket:"+filepath.Join(t.TempDir(), "missing.sock"))
offline := Discover_choices(base).Choices(base)
choices := offline["presentation.speech.module"]
if len(choices) != 2 || choices[1].ID != "saved-module" || choices[1].Unavailable == "" {
t.Fatal(choices)
}
session, err := New_session(base, nil, offline)
if err != nil {
t.Fatal(err)
}
draft, err := Settings(session, base)
if err != nil || draft.Presentation.Speech.Module != "saved-module" {
t.Fatal(draft, err)
}
path := filepath.Join(t.TempDir(), "speech.sock")
listener, err := net.Listen("unix", path)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
done := make(chan string, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
done <- err.Error()
return
}
defer conn.Close()
request, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
done <- err.Error()
return
}
fmt.Fprint(conn, "200-fixture-module\r\n200-saved-module\r\n200 OK\r\n")
done <- request
}()
t.Setenv("SPEECHD_ADDRESS", "unix_socket:"+path)
online := Discover_choices(base).Choices(base)["presentation.speech.module"]
if request := <-done; request != "LIST OUTPUT_MODULES\r\n" {
t.Fatal(request)
}
if len(online) != 3 || online[1].ID != "fixture-module" || online[2].ID != "saved-module" || online[2].Unavailable != "" {
t.Fatal(online)
}
}
func TestDependentSpeechChoicesKeepSavedVoicesAndFilterEspeak(t *testing.T) {
t.Setenv("LC_ALL", "en_US.UTF-8")
base := config.Defaults()
base.Presentation.Speech.Module = "espeak-ng"
catalog := speech.Catalog{Modules: []string{"espeak-ng", "fixture"}, Voices: map[string][]speech.Voice{
"espeak-ng": {{Name: "US+Variant", Language: "en-US"}, {Name: "German", Language: "de"}, {Name: "US", Language: "en-US"}},
"fixture": {{Name: "Fixture", Language: "en"}},
}}
discovery := Discovery{Catalog: catalog}
choices := discovery.Choices(base)
if got := choices["presentation.speech.voice"]; len(got) != 3 || got[1].ID != "US" || got[2].ID != "US+Variant" {
t.Fatal(got)
}
base.Presentation.Speech.Voice = "German"
choices = discovery.Choices(base)
if got := choices["presentation.speech.voice"]; len(got) != 4 || got[3].ID != "German" || got[3].Unavailable != "" {
t.Fatal("valid saved voice lost", got)
}
session, err := New_session(base, nil, choices)
if err != nil {
t.Fatal(err)
}
previous := base
session.Replace_choices("presentation", "speech.language", interaction_choices(choices["presentation.speech.language"]), "de")
discovery.Refresh_speech(session, &previous)
values := session.Values()
if got := values["presentation.speech.voice"]; len(got.Choices) != 2 || got.Choices[1].ID != "German" || got.Selected != 0 {
t.Fatal(got)
}
session.Replace_choices("presentation", "speech.module", interaction_choices(choices["presentation.speech.module"]), "fixture")
discovery.Refresh_speech(session, &previous)
values = session.Values()
if values["presentation.speech.language"].Selected != 0 || values["presentation.speech.voice"].Selected != 0 || len(values["presentation.speech.voice"].Choices) != 2 {
t.Fatal(values)
}
base.Presentation.Speech.Language = "zz"
base.Presentation.Speech.Voice = "Missing"
offline := Discovery{Err: errors.New("fixture offline")}.Choices(base)
for _, path := range []string{"presentation.speech.module", "presentation.speech.language", "presentation.speech.voice"} {
if got := offline[path]; len(got) != 2 || got[1].Unavailable == "" {
t.Fatal(path, got)
}
}
session, err = New_session(base, nil, offline)
if err != nil {
t.Fatal(err)
}
saved, err := Settings(session, base)
if err != nil || saved.Presentation.Speech != base.Presentation.Speech {
t.Fatal(saved, err)
}
}
func TestHardwareDriverChangesHideDispatcherChoicesAndPreserveDevice(t *testing.T) {
base := config.Defaults()
base.Presentation.Speech.HardwareDevice = "/dev/fixture-synth"
base.Presentation.Speech.HardwareBaudRate = 19200
base.Presentation.Speech.Module = "fixture"
base.Presentation.Speech.Language = "en"
base.Presentation.Speech.Voice = "Fixture Voice"
discovery := Discovery{Driver: "speech-dispatcher", Catalog: speech.Catalog{Modules: []string{"fixture"}, Voices: map[string][]speech.Voice{"fixture": {{Name: "Fixture Voice", Language: "en"}}}}}
session, err := New_session(base, nil, discovery.Choices(base))
if err != nil {
t.Fatal(err)
}
previous := base
for _, id := range []string{"dectalk", "litetalk", "doubletalk", "tripletalk", "speech-dispatcher"} {
var choices []config.Choice
for _, definition := range config.Definitions() {
if definition.Path == "presentation.speech.driver" {
choices = definition.Choices(base)
}
}
session.Replace_choices("presentation", "speech.driver", interaction_choices(choices), id)
discovery.Refresh_speech(session, &previous)
draft, err := Settings(session, base)
if err != nil {
t.Fatal(id, err)
}
selected := draft.Presentation.Speech
if selected.Driver != id || selected.HardwareDevice != base.Presentation.Speech.HardwareDevice || selected.HardwareBaudRate != 19200 {
t.Fatal(selected)
}
if id != "speech-dispatcher" {
if selected.Module != "" || selected.Language != "" || selected.Voice != "" {
t.Fatal("inactive selections saved", selected)
}
for _, path := range []string{"presentation.speech.module", "presentation.speech.language", "presentation.speech.voice"} {
if got := session.Values()[path]; !got.Hidden {
t.Fatal("dispatcher choices leaked", id, path, got)
}
}
} else if selected.Module != base.Presentation.Speech.Module || selected.Language != base.Presentation.Speech.Language || selected.Voice != base.Presentation.Speech.Voice {
t.Fatal("returning to Speech Dispatcher lost the draft", selected)
}
}
}