package app import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "os" "strings" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/braille" "git.stormux.org/storm/cthulhu/internal/logging" ) // The test executable doubles as a real pipe-protocol peer. Only the explicit // child invocation takes this path; the parent runs the ordinary test suite. func TestMain(m *testing.M) { if os.Getenv("CTHULHU_BRAILLE_TEST_FAIL") == "1" && len(os.Args) == 2 && os.Args[1] == "--client" { fmt.Fprintln(os.Stderr, "braille test startup failure") os.Exit(1) } if os.Getenv("CTHULHU_BRAILLE_TEST_CLIENT") == "1" && len(os.Args) == 2 && os.Args[1] == "--client" { fmt.Println("ready") decoder, encoder := json.NewDecoder(os.Stdin), json.NewEncoder(os.Stdout) for { var frame braille.Frame if err := decoder.Decode(&frame); err != nil { os.Exit(0) } if frame.Text != "" { if err := encoder.Encode(braille.RoutingEvent{ID: frame.ID, Offset: 1}); err != nil { os.Exit(1) } } } } os.Exit(m.Run()) } func TestBrailleWorkerReportsHelperStartupFailure(t *testing.T) { t.Setenv("CTHULHU_BRAILLE_TEST_FAIL", "1") path, err := os.Executable() if err != nil { t.Fatal(err) } worker := &brailleWorker{path: path, logger: slog.New(slog.DiscardHandler)} ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := worker.session(ctx); err == nil || !strings.Contains(err.Error(), "braille test startup failure") { t.Fatalf("helper startup error = %v", err) } } func TestBrailleWorkerHelperProtocolAndRestart(t *testing.T) { t.Setenv("CTHULHU_BRAILLE_TEST_CLIENT", "1") path, err := os.Executable() if err != nil { t.Fatal(err) } router := &recordingBrailleRouter{calls: make(chan atspi.BrailleRoute, 8)} var output bytes.Buffer worker := &brailleWorker{path: path, router: router, logger: logging.New(&output, logging.Options{Level: slog.LevelDebug}), wake: make(chan struct{}, 1)} stop := func() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := worker.Stop(ctx); err != nil { t.Errorf("stop helper: %v", err) } } defer stop() for attempt := 0; attempt < 2; attempt++ { if err := worker.Start(); err != nil { t.Fatal(err) } worker.EnqueueFrame(braille.Frame{Text: "é猫", Cursor: 0, Routes: []braille.TextRoute{{Start: 0, End: 2, TextLength: 2, SourceStart: 10, BusName: ":1.42", Path: "/entry", SourceText: "é猫"}}}) select { case route := <-router.calls: if route.Offset != 11 || route.Focus.Path != "/entry" { t.Fatalf("route %+v", route) } case <-time.After(3 * time.Second): t.Fatal("helper route was not delivered") } stop() // Stop joins the writer before inspecting its debug output. for _, expected := range []string{`"message":"sent braille frame"`, `"braille_text":"é猫"`, `"cursor_offset":0`} { if !strings.Contains(output.String(), expected) { t.Fatalf("braille diagnostics missing %s: %s", expected, output.String()) } } // A duplicate send can be coalesced by either side; discard already handled // keys before checking that the restarted child returns a new event. for len(router.calls) > 0 { <-router.calls } if worker.current().Text != "" { t.Fatal("stopped worker retained old content") } } }