Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package atspi
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func TestBackgroundInvalidationPreservesOtherProviderLines(t *testing.T) {
|
||||
owned := NewObjectID(":1.42", "/line")
|
||||
other := NewObjectID(":1.43", "/line")
|
||||
adapter := New()
|
||||
adapter.documentLineCache = map[ObjectID]DocumentCaretText{owned: {}, other: {}}
|
||||
adapter.invalidateTreeEvent(TextInsertedEvent, 0, other, ObjectID{})
|
||||
if _, ok := adapter.documentLineCache[owned]; !ok {
|
||||
t.Fatal("background text change cleared the owned provider's line cache")
|
||||
}
|
||||
if _, ok := adapter.documentLineCache[other]; ok {
|
||||
t.Fatal("changed provider's line cache survived invalidation")
|
||||
}
|
||||
adapter.invalidateTreeEvent(AccessibleNameEvent, 0, owned, ObjectID{})
|
||||
if len(adapter.documentLineCache) != 0 {
|
||||
t.Fatal("owned provider change failed to invalidate its lines")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeBackgroundEventFiltering(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_BACKGROUND_EVENTS") != "1" {
|
||||
t.Skip("requires private Xorg/AT-SPI session")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
|
||||
defer cancel()
|
||||
adapter := New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adapter.Stop(context.Background())
|
||||
|
||||
type fixture struct {
|
||||
input io.WriteCloser
|
||||
output *bufio.Reader
|
||||
xid string
|
||||
}
|
||||
start := func(name string) fixture {
|
||||
command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_background_events.py", name)
|
||||
input, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
command.Stderr = os.Stderr
|
||||
if err := command.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { input.Close(); command.Process.Kill(); command.Wait() })
|
||||
reader := bufio.NewReader(output)
|
||||
line, err := reader.ReadString('\n')
|
||||
fields := strings.Fields(line)
|
||||
if err != nil || len(fields) != 2 || fields[0] != "ready" {
|
||||
t.Fatalf("fixture readiness: %q, %v", line, err)
|
||||
}
|
||||
return fixture{input, reader, fields[1]}
|
||||
}
|
||||
wait := func(predicate func(Event) bool) Event {
|
||||
t.Helper()
|
||||
var last Event
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
last = event
|
||||
count++
|
||||
if predicate(event) {
|
||||
return event
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("native event missing: %v; filtered=%d received=%d last=%s %s %q", ctx.Err(), adapter.backgroundEventsFiltered.Load(), count, last.Type, last.Source.Role, last.Source.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
focus := func(xid string) {
|
||||
t.Helper()
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", xid).CombinedOutput(); err != nil {
|
||||
t.Fatalf("focus: %s, %v", output, err)
|
||||
}
|
||||
}
|
||||
background := start("Background fixture")
|
||||
foreground := start("Foreground fixture")
|
||||
// Publish the complete AT-SPI cache, including table cells, before flooding.
|
||||
if _, err := adapter.CaptureDesktop(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
focus(foreground.xid)
|
||||
fmt.Fprintln(foreground.input, "edit")
|
||||
owned := wait(func(event Event) bool {
|
||||
return event.Type == TextInsertedEvent && event.Payload == "Updated text" && !event.ForegroundMismatch
|
||||
})
|
||||
|
||||
sessionBus, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sessionBus.Close()
|
||||
var address string
|
||||
if err := sessionBus.Object("org.a11y.Bus", "/org/a11y/bus").CallWithContext(ctx, "org.a11y.Bus.GetAddress", 0).Store(&address); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
control, err := dbus.Connect(address)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer control.Close()
|
||||
monitor, err := dbus.Connect(address)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer monitor.Close()
|
||||
if err := monitor.BusObject().CallWithContext(ctx, "org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, []string{"type='method_call'"}, uint32(0)).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
messages := make(chan *dbus.Message, 65536)
|
||||
monitor.Eavesdrop(messages)
|
||||
|
||||
flood := func(generation string, sameProvider bool) {
|
||||
t.Helper()
|
||||
before := adapter.backgroundEventsFiltered.Load()
|
||||
fmt.Fprintln(background.input, "flood "+generation)
|
||||
marker := wait(func(event Event) bool {
|
||||
return event.Type == TextInsertedEvent && event.Payload == "Flood complete "+generation
|
||||
})
|
||||
if (marker.Source.BusName == owned.Source.BusName) != sameProvider {
|
||||
t.Fatal("fixture provider identity did not match test case")
|
||||
}
|
||||
if count := adapter.backgroundEventsFiltered.Load() - before; count < 300 {
|
||||
t.Fatalf("filtered %d events; want at least 300 real selection updates", count)
|
||||
}
|
||||
if err := control.Object(marker.Source.BusName, dbus.ObjectPath(marker.Source.Path)).CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
queries := 0
|
||||
reading:
|
||||
for {
|
||||
select {
|
||||
case message := <-messages:
|
||||
if message.Headers[dbus.FieldDestination].Value() != marker.Source.BusName {
|
||||
continue
|
||||
}
|
||||
member := message.Headers[dbus.FieldMember].Value()
|
||||
if member == "Ping" {
|
||||
break reading
|
||||
}
|
||||
if message.Headers[dbus.FieldPath].Value() == dbus.ObjectPath(marker.Source.Path) {
|
||||
continue
|
||||
}
|
||||
switch message.Headers[dbus.FieldInterface].Value() {
|
||||
case "org.a11y.atspi.Selection", "org.a11y.atspi.Table", "org.a11y.atspi.Text", "org.a11y.atspi.Value":
|
||||
queries++
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("monitor barrier missing")
|
||||
}
|
||||
}
|
||||
if queries != 0 {
|
||||
t.Fatalf("background flood caused %d selection/table/text/value queries", queries)
|
||||
}
|
||||
t.Logf("%s: filtered at least 300 updates with zero content queries", generation)
|
||||
}
|
||||
flood("other-provider", false)
|
||||
|
||||
// The same application can have an inactive sibling on the same bus.
|
||||
fmt.Fprintln(background.input, "sibling")
|
||||
line, err := background.output.ReadString('\n')
|
||||
fields := strings.Fields(line)
|
||||
if err != nil || len(fields) != 2 || fields[0] != "sibling" {
|
||||
t.Fatalf("sibling readiness: %q, %v", line, err)
|
||||
}
|
||||
if _, err := adapter.CaptureDesktop(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
focus(fields[1])
|
||||
owned = wait(func(event Event) bool {
|
||||
return !event.ForegroundMismatch && event.Source.Name == "Background fixture sibling entry"
|
||||
})
|
||||
flood("same-provider", true)
|
||||
|
||||
// Returning to the previously noisy window must read its current selection.
|
||||
focus(background.xid)
|
||||
fmt.Fprintln(background.input, "focus-table")
|
||||
wait(func(event Event) bool {
|
||||
return !event.ForegroundMismatch && event.Source.Name == "Result 1" && event.Source.GUITable
|
||||
})
|
||||
t.Log("returning to the noisy application delivers current table focus")
|
||||
}
|
||||
Reference in new issue
Block a user