Files

161 lines
4.8 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package speech
import (
"errors"
"reflect"
"strings"
"testing"
)
type recordingHardware struct {
writes []string
cancels []string
closed int
fail error
}
func (p *recordingHardware) Write(data []byte, canceled func() bool) error {
if canceled() {
return errHardwareCanceled
}
if p.fail != nil {
return p.fail
}
p.writes = append(p.writes, string(data))
return nil
}
func (p *recordingHardware) Cancel(data []byte) error {
p.cancels = append(p.cancels, string(data))
return nil
}
func (p *recordingHardware) Close() { p.closed++ }
func TestHardwareProtocolsMatchPythonFixtures(t *testing.T) {
for _, id := range []string{"dectalk", "dectalk-usb", "litetalk", "doubletalk", "tripletalk"} {
t.Run(id, func(t *testing.T) {
port := &recordingHardware{}
client := new_hardware_client(id, Options{}, port)
parameters := Parameters{Rate: 100, Pitch: -100, Volume: 0}
if err := client.Configure(parameters); err != nil {
t.Fatal(err)
}
if err := client.Configure(parameters); err != nil {
t.Fatal(err)
}
if err := client.Say("Hello\nworld ☃ [:dv g5 10]\x01"); err != nil {
t.Fatal(err)
}
want := []string{"\x019S", "\x010P", "\x014V", "Hello world [:dv g5 10] \r"}
cancel := "\x18"
if strings.HasPrefix(id, "dectalk") {
want = []string{"[:ra 650]", "[:dv ap 50]", "[:dv g5 73]", "Hello world [ :dv g5 10] \x0b"}
cancel = "\x03"
if id == "dectalk-usb" {
cancel = "\x06"
}
}
if !reflect.DeepEqual(port.writes, want) {
t.Fatalf("got %q, want %q", port.writes, want)
}
if err := client.Cancel(); err != nil {
t.Fatal(err)
}
// A flush can discard partially queued settings; the next utterance
// must reapply them even when their values have not changed.
port.writes = nil
if err := client.Say("fresh"); err != nil {
t.Fatal(err)
}
if len(port.writes) != 4 || !reflect.DeepEqual(port.writes[:3], want[:3]) {
t.Fatal(port.writes)
}
client.Close()
client.Close()
if port.closed != 1 || !reflect.DeepEqual(port.cancels, []string{cancel, cancel}) {
t.Fatal(port)
}
})
}
}
func TestHardwareRangesAndNativeMidpoints(t *testing.T) {
for _, test := range []struct {
value int
dectalk, lite [3]string
}{
{-100, [3]string{"[:ra 75]", "[:dv ap 50]", "[:dv g5 60]"}, [3]string{"\x010S", "\x010P", "\x010V"}},
{0, [3]string{"[:ra 180]", "[:dv ap 122]", "[:dv g5 73]"}, [3]string{"\x014S", "\x0150P", "\x014V"}},
{100, [3]string{"[:ra 650]", "[:dv ap 350]", "[:dv g5 86]"}, [3]string{"\x019S", "\x0199P", "\x019V"}},
} {
for _, dectalk := range []bool{false, true} {
client := HardwareClient{dectalk: dectalk, parameters: Parameters{Rate: test.value, Pitch: test.value, Volume: test.value}}
want := test.lite
if dectalk {
want = test.dectalk
}
if got := client.commands(); got != want {
t.Fatal(test.value, got, want)
}
}
}
}
func TestHardwareFailedSettingsAreRetriedAndPitchRestores(t *testing.T) {
port := &recordingHardware{fail: errors.New("disconnected")}
client := new_hardware_client("dectalk", Options{}, port)
if err := client.Configure(Parameters{}); err == nil {
t.Fatal("failed write accepted")
}
port.fail = nil
if err := client.Configure(Parameters{}); err != nil {
t.Fatal(err)
}
if err := client.Set_pitch(100); err != nil {
t.Fatal(err)
}
if err := client.Say("A"); err != nil {
t.Fatal(err)
}
if err := client.Set_pitch(0); err != nil {
t.Fatal(err)
}
want := []string{"[:ra 180]", "[:dv ap 122]", "[:dv g5 73]", "[:dv ap 350]", "A\x0b", "[:dv ap 122]"}
if !reflect.DeepEqual(port.writes, want) {
t.Fatal(port.writes)
}
if err := client.Configure(Parameters{Rate: 101}); err == nil {
t.Fatal("invalid rate accepted")
}
if err := client.Set_pitch(-101); err == nil {
t.Fatal("invalid pitch accepted")
}
}
func TestHardwareValidationDoesNotOpenDevices(t *testing.T) {
for _, id := range []string{"dectalk", "litetalk", "doubletalk", "tripletalk"} {
options := Options{Hardware: HardwareOptions{Device: "/dev/fixture-synth"}}
if err := Validate_driver_options(id, options); err != nil {
t.Fatal(id, err)
}
for _, path := range []string{"", "auto", "ttyUSB0", "/dev/ttyUSB0\n"} {
options.Hardware.Device = path
if err := Validate_driver_options(id, options); err == nil {
t.Fatal(id, path)
}
}
options.Hardware = HardwareOptions{Device: "/dev/fixture-synth", BaudRate: 12345}
if err := Validate_driver_options(id, options); err == nil {
t.Fatal("unsupported baud accepted")
}
}
if err := Validate_driver_options("dectalk-usb", Options{}); err != nil {
t.Fatal(err)
}
if err := Validate_driver_options("dectalk-usb", Options{Voice: "invented"}); err == nil {
t.Fatal("hardware voice accepted")
}
}