//go:build linux && cgo // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "fmt" "io" "log/slog" "os" "strings" "syscall" "testing" "time" "unsafe" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/preferences" ) func test_hardware_switch(t *testing.T, speaker *configuredSpeaker, software config.Settings) { 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), "hardware-speech-fixture") defer 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) } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() hardware := software hardware.SpeechServerFactory = "hardwarefactory" hardware.SpeechOutputModule = "litetalk" hardware.HardwareSpeechDevice = fmt.Sprintf("/dev/pts/%d", number) hardware.HardwareSpeechBaudRate = 9600 rate, pitch, volume := 99.0, 9.0, 9.0 hardware.DefaultVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &volume} if err := speaker.UpdateConfiguration(ctx, hardware); err != nil { t.Fatal(err) } check := func(speak func(context.Context, string) error, text, want string) { t.Helper() if err := speak(ctx, text); err != nil { t.Fatal(err) } master.SetReadDeadline(time.Now().Add(time.Second)) buffer := make([]byte, len(want)) if _, err := io.ReadFull(master, buffer); err != nil || string(buffer) != want { t.Fatalf("serial=%q want=%q error=%v", buffer, want, err) } } check(speaker.Speak, "Default", "\x019S\x0199P\x019VDefault\r") check(speaker.SpeakCharacter, "A", "\x019S\x0199P\x019VA\r") check(speaker.SpeakCharacter, "a", "\x019S\x0199P\x019Va\r") worker := newPreferencesWorker(slog.New(slog.DiscardHandler), hardware, speaker) values := preferences.FromSettings(hardware) rateLow := 0.0 values.Rate = &rateLow if response := worker.apply(ctx, preferences.Request{Values: values}); !response.Accepted { t.Fatal(response.Message) } check(speaker.Speak, "Applied", "\x18\x010S\x0199P\x019VApplied\r") values.HardwareSpeechDevice = "/dev/cthulhu-missing-serial-fixture" if response := worker.apply(ctx, preferences.Request{Values: values}); response.Accepted { t.Fatal("unavailable serial device accepted") } if worker.settings.HardwareSpeechDevice != hardware.HardwareSpeechDevice { t.Fatal("failed Apply committed a missing device") } check(speaker.Speak, "Retained", "\x010S\x0199P\x019VRetained\r") if err := speaker.UpdateConfiguration(ctx, software); err != nil { t.Fatal(err) } if !strings.HasPrefix(worker.settings.HardwareSpeechDevice, "/dev/pts/") { t.Fatal("unexpected fixture device") } }