126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"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 TestNativeAutomaticTitleDispatchLatency(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_WINDOW_TITLES") != "1" {
|
|
t.Skip("private Xvfb and D-Bus fixture only")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
fixture := exec.CommandContext(ctx, "python3", "-u", "../../tests/window_title_fixture.py")
|
|
stdin, err := fixture.StdinPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdout, err := fixture.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fixture.Stderr = os.Stderr
|
|
if err = fixture.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { stdin.Close(); fixture.Wait() }()
|
|
scanner := bufio.NewScanner(stdout)
|
|
if !scanner.Scan() {
|
|
t.Fatal("fixture did not start")
|
|
}
|
|
adapter := atspi.New()
|
|
if err = adapter.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adapter.Stop(context.Background())
|
|
listener := xorg.New()
|
|
if err = listener.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Stop(context.Background())
|
|
queue := &fakeSpeechQueue{spoken: make(chan string, 8)}
|
|
inputDone := make(chan struct{})
|
|
go func() {
|
|
defer close(inputDone)
|
|
processInputEvents(ctx, slog.New(slog.DiscardHandler), queue, listener, input.EchoSettings{}, nil, nil)
|
|
}()
|
|
defer func() { cancel(); <-inputDone }()
|
|
completed := make(chan error, 1)
|
|
go func() {
|
|
completed <- processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, nil, 0, nil, listener, nil, nil)
|
|
}()
|
|
defer func() {
|
|
cancel()
|
|
if err := <-completed; err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
wait := func(want string, deadline <-chan time.Time) {
|
|
t.Helper()
|
|
for {
|
|
select {
|
|
case got := <-queue.spoken:
|
|
if got != want {
|
|
t.Fatalf("title = %q, want %q", got, want)
|
|
}
|
|
return
|
|
case <-deadline:
|
|
t.Fatalf("title %q was not queued promptly", want)
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
}
|
|
wait("Example title", time.After(2*time.Second))
|
|
for _, title := range []string{"Status one", "Status two", "Status three"} {
|
|
data, _ := json.Marshal(map[string]string{"action": "title", "window": "normal", "title": title})
|
|
began := time.Now()
|
|
deadline := time.After(200 * time.Millisecond)
|
|
if _, err = stdin.Write(append(data, '\n')); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !scanner.Scan() || scanner.Text() != "ready" {
|
|
t.Fatal("fixture command failed")
|
|
}
|
|
wait(title, deadline)
|
|
t.Logf("X11 title update to speech queue: %s", time.Since(began))
|
|
}
|
|
// The switch chord must not suppress the destination's introduction. The
|
|
// fixture has no window manager, so activate each destination explicitly.
|
|
for _, destination := range []struct{ window, title string }{
|
|
{"unrelated", "Unrelated title"}, {"normal", "Status three"},
|
|
} {
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "alt+Tab").CombinedOutput(); err != nil {
|
|
t.Fatalf("switch keys: %v %s", err, out)
|
|
}
|
|
data, _ := json.Marshal(map[string]string{"action": "activate", "window": destination.window})
|
|
if _, err = stdin.Write(append(data, '\n')); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !scanner.Scan() || scanner.Text() != "ready" {
|
|
t.Fatal("fixture switch failed")
|
|
}
|
|
wait(destination.title, time.After(2*time.Second))
|
|
}
|
|
select {
|
|
case got := <-queue.spoken:
|
|
t.Fatalf("duplicate window title: %q", got)
|
|
case <-time.After(2 * windowTitleIntroductionDelay):
|
|
}
|
|
}
|