121 lines
3.4 KiB
Go
121 lines
3.4 KiB
Go
//go:build linux && cgo
|
|
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package speech
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
func serial_fixture(t *testing.T) (*os.File, string) {
|
|
t.Helper()
|
|
fd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
master := os.NewFile(uintptr(fd), "serial-test")
|
|
t.Cleanup(func() { master.Close() })
|
|
var unlocked int32
|
|
var number uint32
|
|
if _, _, code := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&unlocked))); code != 0 {
|
|
t.Fatal(code)
|
|
}
|
|
if _, _, code := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&number))); code != 0 {
|
|
t.Fatal(code)
|
|
}
|
|
return master, fmt.Sprintf("/dev/pts/%d", number)
|
|
}
|
|
|
|
func TestHardwareSerialWritesCancelsAndCloses(t *testing.T) {
|
|
for _, synth := range []string{"litetalk", "doubletalk", "tripletalk", "dectalk"} {
|
|
t.Run(synth, func(t *testing.T) {
|
|
master, device := serial_fixture(t)
|
|
client, err := NewHardware(device, synth, 9600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { client.Close(context.Background()) })
|
|
if err := client.Speak(context.Background(), "Serial test", HardwareVoice{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := hardware_bytes(synth, "Serial test", HardwareVoice{})
|
|
master.SetReadDeadline(time.Now().Add(time.Second))
|
|
buffer := make([]byte, 256)
|
|
n, err := master.Read(buffer)
|
|
if err != nil || !bytes.Equal(buffer[:n], want) {
|
|
t.Fatalf("bytes=%q error=%v", buffer[:n], err)
|
|
}
|
|
if err := client.Cancel(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n, err = master.Read(buffer)
|
|
if err != nil || !bytes.Equal(buffer[:n], []byte{0x18}) {
|
|
t.Fatalf("cancel=%q error=%v", buffer[:n], err)
|
|
}
|
|
if err := client.Close(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := client.Speak(context.Background(), "Closed", HardwareVoice{}); err == nil {
|
|
t.Fatal("closed serial owner accepted speech")
|
|
}
|
|
})
|
|
}
|
|
if client, err := NewHardware("/dev/null", "dectalk", 9600); err == nil {
|
|
client.Close(context.Background())
|
|
t.Fatal("non-terminal device accepted")
|
|
}
|
|
}
|
|
|
|
func TestHardwareCancellationRetiresBlockedSerialWrite(t *testing.T) {
|
|
_, device := serial_fixture(t)
|
|
client, err := NewHardware(device, "litetalk", 9600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() { done <- client.Speak(context.Background(), strings.Repeat("x", 1024*1024), HardwareVoice{}) }()
|
|
// Wait until the PTY output queue fills, then use the same cancellation
|
|
// path used by keyboard input. No reader drains that queue.
|
|
deadline := time.Now().Add(time.Second)
|
|
for {
|
|
client.mu.Lock()
|
|
locked := !client.ioMu.TryLock()
|
|
if !locked {
|
|
client.ioMu.Unlock()
|
|
}
|
|
client.mu.Unlock()
|
|
if locked {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("serial write did not start")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
if err := client.Cancel(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
if err != context.Canceled {
|
|
t.Fatalf("blocked write: %v", err)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("cancellation did not release serial writer")
|
|
}
|
|
}
|