Files
cthulhu/internal/app/runtime_platform_test.go

105 lines
3.5 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"errors"
"log/slog"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
)
func TestDesktopInputAvailability(t *testing.T) {
logger := slog.New(slog.DiscardHandler)
for _, kind := range []string{"x11", "wayland"} {
t.Run(kind, func(t *testing.T) {
t.Setenv("XDG_SESSION_TYPE", kind)
t.Setenv("DISPLAY", "")
backends := []desktopInputBackend{xorg.New(), xorg.NewCommands([]string{"Insert"}, input.DesktopKeyboardLayout)}
for _, backend := range backends {
service := desktopInputService("test", backend, logger)
err := service.Start(context.Background())
if (err == nil) != (kind == "wayland") {
t.Fatalf("%T startup in %s: %v", backend, kind, err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
if err := service.Stop(ctx); err != nil {
t.Errorf("%T cleanup: %v", backend, err)
}
cancel()
}
// Future non-Xorg backends must keep their own failure contract.
failure := errors.New("other backend failed")
other := &fakeInputListener{startError: failure}
if err := desktopInputService("other", other, logger).Start(context.Background()); !errors.Is(err, failure) {
t.Fatalf("other backend startup: %v", err)
}
})
}
}
func TestOptionalXorgPreservesCancellation(t *testing.T) {
logger := slog.New(slog.DiscardHandler)
for _, failure := range []error{context.Canceled, context.DeadlineExceeded} {
if err := startOptionalXorg(context.Background(), "test", func(context.Context) error { return failure }, logger); !errors.Is(err, failure) {
t.Fatalf("startup: %v, want %v", err, failure)
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := startOptionalXorg(ctx, "test", func(context.Context) error { return nil }, logger); !errors.Is(err, context.Canceled) {
t.Fatalf("canceled startup: %v", err)
}
}
func TestRunWaylandWithoutX11SpeaksAndStops(t *testing.T) {
t.Setenv("XDG_SESSION_TYPE", "wayland")
t.Setenv("DISPLAY", "")
speaker := &fakeSpeaker{spoken: make(chan string, 8)}
adapter := &fakeAdapter{events: make(chan atspi.Event, 1)}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
completed := make(chan error, 1)
go func() {
completed <- Run(ctx, Options{
Logger: slog.New(slog.DiscardHandler),
newSpeaker: func() Speaker { return speaker },
newSound: func() soundService { return silentSound{} },
newAdapter: func() Adapter { return adapter },
})
}()
adapter.events <- atspi.NewEvent("focus:", atspi.NewSnapshot("/accessible/save", "Save", "push button", "", ""))
for _, expected := range []string{"", "Save", "push button"} {
select {
case spoken := <-speaker.spoken:
if expected == "" && contains_string(startupMessages, spoken) || expected != "" && spoken == expected {
continue
}
t.Fatalf("speech = %q, expected %q", spoken, expected)
case err := <-completed:
t.Fatalf("runtime exited before focus speech: %v", err)
case <-time.After(3 * time.Second):
t.Fatal("runtime did not present focus with X11 unavailable")
}
}
cancel()
select {
case err := <-completed:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("runtime did not stop")
}
if !adapter.stopped || !speaker.closed {
t.Fatal("runtime did not clean up AT-SPI and speech")
}
}