After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+382
View File
@@ -0,0 +1,382 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build linux && cgo
package speech
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"reflect"
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
"unsafe"
)
func TestHardwarePreviewKeepsTemporaryPortUntilCanceledAndReleasesIt(t *testing.T) {
master, path := hardware_pty(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan error, 1)
go func() {
done <- Test_hardware(ctx, "dectalk", Options{Hardware: HardwareOptions{Device: path}}, "test sample")
}()
want := "[:ra 180][:dv ap 122][:dv g5 73]test sample\x0b"
if got := string(read_hardware(t, master, len(want))); got != want {
t.Fatal(got)
}
if other, err := Open_driver("dectalk", Options{Hardware: HardwareOptions{Device: path}}); err == nil {
other.Close()
t.Fatal("preview closed before device could speak")
}
cancel()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("preview did not cancel")
}
client, err := Open_driver("dectalk", Options{Hardware: HardwareOptions{Device: path}})
if err != nil {
t.Fatal("preview retained port", err)
}
client.Close()
}
func hardware_pty(t *testing.T) (*os.File, string) {
t.Helper()
fd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK|syscall.O_CLOEXEC, 0)
if err != nil {
t.Fatal(err)
}
master := os.NewFile(uintptr(fd), "hardware fixture")
t.Cleanup(func() { master.Close() })
var unlock int32
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&unlock))); err != 0 {
t.Fatal(err)
}
var number uint32
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&number))); err != 0 {
t.Fatal(err)
}
return master, fmt.Sprintf("/dev/pts/%d", number)
}
func read_hardware(t *testing.T, master *os.File, count int) []byte {
t.Helper()
var result []byte
deadline := time.Now().Add(2 * time.Second)
for len(result) < count && time.Now().Before(deadline) {
buffer := make([]byte, count-len(result))
n, err := syscall.Read(int(master.Fd()), buffer)
if n > 0 {
result = append(result, buffer[:n]...)
}
if err != nil && err != syscall.EAGAIN && err != syscall.EINTR {
t.Fatal(err)
}
if n <= 0 {
time.Sleep(time.Millisecond)
}
}
if len(result) != count {
t.Fatalf("read %q (%d of %d)", result, len(result), count)
}
return result
}
func assert_hardware_idle(t *testing.T, master *os.File) {
t.Helper()
var buffer [128]byte
if n, err := syscall.Read(int(master.Fd()), buffer[:]); n > 0 || err != syscall.EAGAIN {
t.Fatalf("unexpected bytes %q, %v", buffer[:max(n, 0)], err)
}
}
func hardware_termios(t *testing.T, fd int) syscall.Termios {
t.Helper()
var value syscall.Termios
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TCGETS, uintptr(unsafe.Pointer(&value))); err != 0 {
t.Fatal(err)
}
return value
}
func TestNativeSerialHardwareLifecycle(t *testing.T) {
for _, id := range []string{"dectalk", "litetalk", "doubletalk", "tripletalk"} {
t.Run(id, func(t *testing.T) {
master, path := hardware_pty(t)
slave, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
if err != nil {
t.Fatal(err)
}
defer syscall.Close(slave)
original := hardware_termios(t, slave)
options := Options{Hardware: HardwareOptions{Device: path, BaudRate: 19200}}
transport, err := Open_driver(id, options)
if err != nil {
t.Fatal(err)
}
defer transport.Close()
configured := hardware_termios(t, slave)
if configured.Cflag&syscall.CS8 == 0 || configured.Lflag&(syscall.ICANON|syscall.ECHO) != 0 || configured.Iflag&(syscall.IXOFF|syscall.IXANY) != 0 || (configured.Iflag&syscall.IXON != 0) != (id == "dectalk") {
t.Fatal("serial mode", configured)
}
assert_hardware_idle(t, master) // Opening a candidate never speaks.
if other, err := Open_driver(id, options); err == nil {
other.Close()
t.Fatal("another reader acquired an owned serial device")
}
if err := transport.Configure(Parameters{Volume: 100}); err != nil {
t.Fatal(err)
}
if err := transport.Say("Hello\nworld ☃"); err != nil {
t.Fatal(err)
}
want := "\x014S\x0150P\x019VHello world \r"
cancel := "\x18"
if id == "dectalk" {
want = "[:ra 180][:dv ap 122][:dv g5 86]Hello world \x0b"
cancel = "\x03"
}
if got := string(read_hardware(t, master, len(want))); got != want {
t.Fatalf("%q != %q", got, want)
}
if err := transport.Configure(Parameters{Volume: 100}); err != nil {
t.Fatal(err)
}
assert_hardware_idle(t, master)
if err := transport.Cancel(); err != nil {
t.Fatal(err)
}
if got := string(read_hardware(t, master, 1)); got != cancel {
t.Fatal(got)
}
transport.Close()
if restored := hardware_termios(t, slave); restored != original {
t.Fatal("termios not restored", restored, original)
}
reopened, err := Open_driver(id, options)
if err != nil {
t.Fatal("serial lock survived close", err)
}
reopened.Close()
})
}
}
func TestSerialCancellationStopsLargeWriteAndAllowsFreshSpeech(t *testing.T) {
master, path := hardware_pty(t)
transport, err := Open_driver("dectalk", Options{Hardware: HardwareOptions{Device: path}})
if err != nil {
t.Fatal(err)
}
defer transport.Close()
if err := transport.Configure(Parameters{}); err != nil {
t.Fatal(err)
}
read_hardware(t, master, len("[:ra 180][:dv ap 122][:dv g5 73]"))
done := make(chan error, 1)
go func() { done <- transport.Say(strings.Repeat("x", 1<<20)) }()
if string(read_hardware(t, master, 1)) != "x" {
t.Fatal("text write did not start")
}
if err := transport.Cancel(); err != nil {
t.Fatal(err)
}
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("cancellation waited behind speech")
}
// Discard already transmitted fixture bytes, then ensure nothing from the
// canceled utterance can appear after a fresh request.
var buffer [32768]byte
for {
if _, err := syscall.Read(int(master.Fd()), buffer[:]); err == syscall.EAGAIN {
break
} else if err != nil {
t.Fatal(err)
}
}
if err := transport.Say("fresh"); err != nil {
t.Fatal(err)
}
want := "[:ra 180][:dv ap 122][:dv g5 73]fresh\x0b"
if got := string(read_hardware(t, master, len(want))); got != want {
t.Fatalf("stale output %q", got)
}
assert_hardware_idle(t, master)
}
func TestSerialDisconnectReturnsError(t *testing.T) {
master, path := hardware_pty(t)
transport, err := Open_driver("litetalk", Options{Hardware: HardwareOptions{Device: path}})
if err != nil {
t.Fatal(err)
}
defer transport.Close()
master.Close()
if err := transport.Say("after disconnect"); err == nil {
t.Fatal("disconnected serial device accepted speech")
}
}
func TestSerialXoffPauseRemainsCancelable(t *testing.T) {
master, path := hardware_pty(t)
transport, err := Open_driver("dectalk", Options{Hardware: HardwareOptions{Device: path}})
if err != nil {
t.Fatal(err)
}
defer transport.Close()
if err := transport.Configure(Parameters{}); err != nil {
t.Fatal(err)
}
read_hardware(t, master, len("[:ra 180][:dv ap 122][:dv g5 73]"))
// Packet mode acknowledges that the terminal processed XOFF before the
// write starts; this does not rely on a sleep or on filling a PTY buffer.
packetMode := int32(1)
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), syscall.TIOCPKT, uintptr(unsafe.Pointer(&packetMode))); err != 0 {
t.Fatal(err)
}
if _, err := syscall.Write(int(master.Fd()), []byte{0x13}); err != nil {
t.Fatal(err)
}
if got := read_hardware(t, master, 1)[0]; got&syscall.TIOCPKT_STOP == 0 {
t.Fatal("XOFF was not processed", got)
}
packetMode = 0
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), syscall.TIOCPKT, uintptr(unsafe.Pointer(&packetMode))); err != 0 {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- transport.Say("paused") }()
select {
case err := <-done:
t.Fatal("normal flow pause terminated speech", err)
case <-time.After(time.Second):
}
if err := transport.Cancel(); err != nil {
t.Fatal(err)
}
slave, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
t.Fatal(err)
}
configured := hardware_termios(t, slave)
syscall.Close(slave)
if configured.Iflag&syscall.IXON == 0 {
t.Fatal("cancellation did not restore XON/XOFF")
}
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("cancel failed during flow pause")
}
if got := string(read_hardware(t, master, 1)); got != "\x03" {
t.Fatal(got)
}
if err := transport.Say("resumed"); err != nil {
t.Fatal(err)
}
want := "[:ra 180][:dv ap 122][:dv g5 73]resumed\x0b"
if got := string(read_hardware(t, master, len(want))); got != want {
t.Fatal(got)
}
}
func TestUSBCancelBypassesBlockedPacketAndDiscardsRemainingText(t *testing.T) {
started, release := make(chan struct{}), make(chan struct{})
var releaseOnce sync.Once
unblock := func() { releaseOnce.Do(func() { close(release) }) }
defer unblock()
var normal, immediate atomic.Int32
port := &usbPort{send: func(endpoint byte, data []byte) error {
if endpoint == 0x04 {
if !bytes.Equal(data, []byte{0x06}) {
return fmt.Errorf("bad cancellation %x", data)
}
immediate.Add(1)
} else if bytes.HasPrefix(data, []byte("xxx")) {
if normal.Add(1) == 1 {
close(started)
<-release
}
}
return nil
}}
client := new_hardware_client("dectalk-usb", Options{}, port)
done := make(chan error, 1)
go func() { done <- client.Say(strings.Repeat("x", 200)) }()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("packet not started")
}
if err := client.Cancel(); err != nil {
t.Fatal(err)
}
if immediate.Load() != 1 {
t.Fatal("immediate endpoint was blocked")
}
unblock()
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("stale send stuck")
}
if normal.Load() != 1 {
t.Fatal("more stale packets transmitted", normal.Load())
}
client.Close()
}
func TestUSBPacketLimitsFailureAndCleanup(t *testing.T) {
var sizes []int
closed := 0
port := &usbPort{send: func(endpoint byte, data []byte) error {
if endpoint != 2 {
t.Fatal(endpoint)
}
sizes = append(sizes, len(data))
return nil
}, close: func() { closed++ }}
if err := port.Write(make([]byte, 130), func() bool { return false }); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(sizes, []int{64, 64, 2}) {
t.Fatal(sizes)
}
failure := errors.New("short transfer")
port.send = func(byte, []byte) error { return failure }
if err := port.Write(make([]byte, 70), func() bool { return false }); !errors.Is(err, failure) {
t.Fatal(err)
}
port.Close()
port.Close()
if closed != 1 {
t.Fatal(closed)
}
if err := port.Write([]byte{1}, func() bool { return false }); err == nil {
t.Fatal("closed USB device accepted write")
}
}