130 lines
4.1 KiB
Go
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")
|
|
}
|
|
}
|