Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,120 @@
|
||||
//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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user