// SPDX-License-Identifier: GPL-3.0-or-later package wine import ( "context" "errors" "fmt" "git.stormux.org/storm/cthulhu/internal/atspi" "github.com/godbus/dbus/v5" "testing" "time" ) func TestReviewRegistryRetiresInactiveWindowTrees(t *testing.T) { r := readyReviewRegistry(t) n := registerNotice(t, r, ":1.2") r.captureCall = func(_ context.Context, _ string, _ dbus.ObjectPath, notice ReviewFocusNotice) (map[string]dbus.Variant, error) { f := reviewWire() f["window"] = dbus.MakeVariant(notice.Window) f["revision"] = dbus.MakeVariant(notice.Revision) nodes := f["nodes"].Value().([]map[string]dbus.Variant) nodes[0]["id"] = dbus.MakeVariant(notice.Window) for _, node := range nodes[1:] { if node["parent"].Value() == "window" { node["parent"] = dbus.MakeVariant(notice.Window) } } return f, nil } for i := 1; i <= 32; i++ { n.Window = fmt.Sprintf("window%d", i) n.Revision = uint64(i) if !r.PresentFocus(n.Sender, n) { t.Fatal("focus rejected") } one, err := r.Capture(context.Background(), n.ID()) if err != nil { t.Fatal(err) } two, err := r.Capture(context.Background(), n.ID()) if err != nil || one != two { t.Fatal("active window tree is not stable") } if len(r.providers[n.Sender].trees) != 1 { t.Fatalf("retained %d inactive trees", len(r.providers[n.Sender].trees)-1) } } } func registryNotice(sender string) ReviewFocusNotice { return ReviewFocusNotice{Sender: sender, Path: ReviewObjectPath, Instance: "helper-instance", Window: "window", Focus: "input", Revision: 1, Title: "same title", Role: "window", Bounds: atspi.Rectangle{X: 10, Y: 20, Width: 400, Height: 300}} } func readyReviewRegistry(t *testing.T) *ReviewRegistry { t.Helper() registry := NewReviewRegistry(nil) // Unit tests substitute transport only; registration still requires lifecycle. registry.ctx, registry.cancel = context.WithCancel(context.Background()) t.Cleanup(func() { registry.Stop(context.Background()) }) return registry } func registerNotice(t *testing.T, r *ReviewRegistry, sender string) ReviewFocusNotice { t.Helper() notice := registryNotice(sender) if !r.RegisterProvider(sender, ReviewObjectPath, 1) || !r.PresentFocus(sender, notice) { t.Fatal("registration rejected") } return notice } func TestReviewRegistryTwoProviders(t *testing.T) { r := readyReviewRegistry(t) one := registerNotice(t, r, ":1.2") two := registerNotice(t, r, ":1.3") if one.ID() == two.ID() || len(r.Focuses()) != 2 || !r.Current(one.ID()) || !r.Current(two.ID()) { t.Fatal("providers collided") } if len(r.changes) != 1 { t.Fatal("notifications are not coalesced") } if !r.RegisterProvider(one.Sender, ReviewObjectPath, 1) || r.Current(one.ID()) || len(r.Focuses()) != 1 { t.Fatal("registration retained old lifetime") } } func TestReviewRegistrySenderAssociation(t *testing.T) { r := readyReviewRegistry(t) n := registryNotice(":1.2") if r.PresentFocus(n.Sender, n) || r.RegisterProvider("well.known", ReviewObjectPath, 1) || r.RegisterProvider(n.Sender, ReviewObjectPath, 2) || r.RegisterProvider(n.Sender, "/wrong", 1) { t.Fatal("invalid provider accepted") } registerNotice(t, r, n.Sender) n.Sender = ":1.99" n.Path = "/forged" if !r.PresentFocus(":1.2", n) { t.Fatal("actual sender rejected") } got := r.Focuses()[0] if got.Sender != ":1.2" || got.Path != ReviewObjectPath { t.Fatal("payload forged destination") } n.Revision = 0 if r.PresentFocus(":1.2", n) { t.Fatal("invalid focus accepted") } } func TestReviewRegistryProviderDisappears(t *testing.T) { r := readyReviewRegistry(t) n := registerNotice(t, r, ":1.2") r.disappeared(n.Sender) if r.Current(n.ID()) || len(r.Focuses()) != 0 { t.Fatal("dead provider retained") } if _, err := r.Capture(context.Background(), n.ID()); !errors.Is(err, ErrReviewUnavailable) { t.Fatal(err) } } func TestReviewCaptureDeadlineAndStaleReply(t *testing.T) { r := readyReviewRegistry(t) n := registerNotice(t, r, ":1.2") calls := 0 r.captureCall = func(ctx context.Context, sender string, path dbus.ObjectPath, notice ReviewFocusNotice) (map[string]dbus.Variant, error) { calls++ fields := reviewWire() if calls == 1 { fields["status"] = dbus.MakeVariant("stale") fields["nodes"] = dbus.MakeVariant([]map[string]dbus.Variant{}) } return fields, nil } tree, err := r.Capture(context.Background(), n.ID()) if err != nil || tree == nil || calls != 2 { t.Fatalf("stale retry: %v calls=%d", err, calls) } again, err := r.Capture(context.Background(), n.ID()) if err != nil || again != tree { t.Fatal("capture replaced live tree") } r.captureCall = func(ctx context.Context, _ string, _ dbus.ObjectPath, _ ReviewFocusNotice) (map[string]dbus.Variant, error) { <-ctx.Done() return nil, ctx.Err() } started := time.Now() if _, err = r.Capture(context.Background(), n.ID()); err == nil || time.Since(started) > 750*time.Millisecond { t.Fatalf("deadline: %v", err) } r.captureCall = func(context.Context, string, dbus.ObjectPath, ReviewFocusNotice) (map[string]dbus.Variant, error) { f := reviewWire() f["revision"] = dbus.MakeVariant(uint64(2)) return f, nil } if _, err = r.Capture(context.Background(), n.ID()); !errors.Is(err, ErrReviewStale) { t.Fatalf("wrong source: %v", err) } } func TestReviewCaptureStopCancels(t *testing.T) { r := readyReviewRegistry(t) n := registerNotice(t, r, ":1.2") entered := make(chan struct{}) done := make(chan error, 1) r.captureCall = func(ctx context.Context, _ string, _ dbus.ObjectPath, _ ReviewFocusNotice) (map[string]dbus.Variant, error) { close(entered) <-ctx.Done() return nil, ctx.Err() } go func() { _, err := r.Capture(context.Background(), n.ID()); done <- err }() <-entered r.Stop(context.Background()) select { case err := <-done: if err == nil { t.Fatal("cancelled capture accepted") } case <-time.After(time.Second): t.Fatal("stop blocked") } }