Files

130 lines
4.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package speech
import (
"bufio"
"fmt"
"reflect"
"strings"
"testing"
)
func TestLargeVoiceCatalogAndSilentPreparation(t *testing.T) {
server := new_fake_server(t, false)
server.mu.Lock()
for index := range 14805 {
server.voices = append(server.voices, fmt.Sprintf("Voice %d\ten-US\tnone", index))
}
server.mu.Unlock()
catalog, err := Discover(Options{Socket: server.path})
if err != nil || len(catalog.Voices["fixture"]) != 14805 || len(catalog.Errors) != 0 {
t.Fatal(len(catalog.Voices["fixture"]), err, catalog.Errors)
}
for _, options := range []Options{
{Socket: server.path, Module: "absent"},
{Socket: server.path, Module: "fixture", Language: "de"},
{Socket: server.path, Module: "fixture", Voice: "missing"},
{Socket: server.path, Voice: "bad\r\nCANCEL ALL"},
} {
if client, err := Prepare("speech-dispatcher", options); err == nil {
client.Close()
t.Fatal("accepted unavailable selection", options)
}
}
candidate, err := Prepare("speech-dispatcher", Options{Socket: server.path, Module: "fixture", Language: "en-US", Voice: "Voice 12000"})
if err != nil {
t.Fatal(err)
}
select {
case event := <-server.events:
t.Fatal("preparation spoke or canceled active speech", event)
default:
}
server.mu.Lock()
commands := strings.Join(server.settings, "|")
server.mu.Unlock()
if !strings.Contains(commands, "SET SELF LANGUAGE en-US|SET SELF SYNTHESIS_VOICE Voice 12000") {
t.Fatal("voice must follow language")
}
candidate.Close()
}
func TestBoundedRepliesAndInjection(t *testing.T) {
for _, reply := range []string{
"249-" + strings.Repeat("a", 5000) + "\n249 OK\n",
strings.Repeat("249-"+strings.Repeat("a", 3000)+"\n", 3000) + "249 OK\n",
"249-one\n250 OK\n",
} {
w := wire{reader: bufio.NewReaderSize(strings.NewReader(reply), 4096)}
if _, err := w.reply(); err == nil {
t.Fatal("accepted unbounded or malformed reply")
}
}
for _, row := range []string{"Missing tabs", "name\ten\tvariant\textra", "name\ten\rCANCEL ALL", "bad\x00name\ten"} {
if _, err := parse_voices([]string{row}); err == nil {
t.Fatal("accepted malformed catalog row", row)
}
}
}
func TestEspeakLocaleDialectBaseAndExplicitLanguage(t *testing.T) {
voices := []Voice{{Name: "US+Variant", Language: "en-US"}, {Name: "German", Language: "de"}, {Name: "US", Language: "en-US"}, {Name: "English", Language: "en"}}
t.Setenv("LC_ALL", "en_US.UTF-8")
got := Language_voices(voices, "")
if !reflect.DeepEqual(got, []Voice{voices[2], voices[0]}) {
t.Fatal(got)
}
t.Setenv("LC_ALL", "en_AU.UTF-8")
if got := Language_voices(voices, ""); !reflect.DeepEqual(got, []Voice{voices[3]}) {
t.Fatal(got)
}
if got := Language_voices(voices, "de"); !reflect.DeepEqual(got, []Voice{voices[1]}) {
t.Fatal(got)
}
if got := Language_voices(voices, "unknown"); len(got) != 0 {
t.Fatal(got)
}
t.Setenv("LC_ALL", "zz_ZZ")
if got := Language_voices(voices, ""); !reflect.DeepEqual(got, voices) {
t.Fatal(got)
}
if voices[0].Name != "US+Variant" {
t.Fatal("catalog reordered in place")
}
}
func TestCandidateRejectionLeavesOriginalClientUsable(t *testing.T) {
server := new_fake_server(t, false)
original, err := Open(Options{Socket: server.path})
if err != nil {
t.Fatal(err)
}
defer original.Close()
server.mu.Lock()
server.reject = "SET SELF SYNTHESIS_VOICE"
server.mu.Unlock()
if candidate, err := Prepare("speech-dispatcher", Options{Socket: server.path, Module: "fixture", Voice: "Base Voice"}); err == nil {
candidate.Close()
t.Fatal("accepted rejected voice")
}
if err := original.Say("still working"); err != nil {
t.Fatal(err)
}
wait_event(t, server.events, "say:still working")
}
func TestCatalogPreservesEspeakVoiceWhitespace(t *testing.T) {
voices, err := parse_voices([]string{"Cherokee \tchr\tnone"})
if err != nil || len(voices) != 1 || voices[0].Name != "Cherokee " {
t.Fatal(voices, err)
}
if err := Validate_selection("espeak-ng", "chr", "Cherokee "); err != nil {
t.Fatal(err)
}
if err := Validate_selection("espeak-ng", "chr", " "); err == nil {
t.Fatal("accepted whitespace-only name")
}
}