142 lines
4.3 KiB
Go
142 lines
4.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"image"
|
|
"image/color"
|
|
"os"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/ocr"
|
|
)
|
|
|
|
func TestNativeOCRProviderGeometry(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_OCR_GEOMETRY") != "1" {
|
|
t.Skip("requires private Xorg/AT-SPI session")
|
|
}
|
|
executable := os.Getenv("CTHULHU_OCR_CAPTURE_BINARY")
|
|
if executable == "" {
|
|
t.Fatal("set freshly built CTHULHU_OCR_CAPTURE_BINARY")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
|
|
defer cancel()
|
|
adapter := New()
|
|
if err := adapter.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adapter.Stop(context.Background())
|
|
for _, scale := range []string{"1", "2"} {
|
|
t.Run("scale"+scale, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
fixture := exec.CommandContext(ctx, "/usr/bin/python3", "-u", "../../tests/ocr_geometry_fixture.py")
|
|
fixture.Env = append(os.Environ(), "GDK_SCALE="+scale, "NO_AT_BRIDGE=0", "GTK_MODULES=gail:atk-bridge")
|
|
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)
|
|
type state struct {
|
|
Window uint64
|
|
Target [4]int
|
|
Focus uint64
|
|
}
|
|
read := func() state {
|
|
t.Helper()
|
|
if !scanner.Scan() {
|
|
t.Fatal("fixture did not respond")
|
|
}
|
|
var s state
|
|
if err = json.Unmarshal(scanner.Bytes(), &s); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
initial := read()
|
|
action := func(name string) state {
|
|
t.Helper()
|
|
data, _ := json.Marshal(map[string]string{"action": name})
|
|
if _, err = stdin.Write(append(data, '\n')); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return read()
|
|
}
|
|
var tree *AccessibleTree
|
|
var ref ObjectReference
|
|
for !ref.Valid() {
|
|
tree, err = adapter.CaptureDesktop(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ref = findNamedReference(t, tree, "Painted target")
|
|
if !ref.Valid() {
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
case <-time.After(10 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
geometry, err := adapter.CaptureOCRGeometry(ctx, ref.ID(), nil)
|
|
expected := image.Rect(initial.Target[0], initial.Target[1], initial.Target[2], initial.Target[3])
|
|
if err != nil || geometry.ScreenBounds != expected || geometry.Foreground.Client.Window != initial.Window {
|
|
provider, _ := tree.Geometry(ref)
|
|
t.Fatalf("geometry=%+v err=%v want=%v provider=%+v foreground=%+v", geometry, err, expected, provider, adapter.Foreground())
|
|
}
|
|
result, err := ocr.NewCapturer(executable).Capture(ctx, ocr.CaptureRequest{Kind: ocr.ObjectSource, Bounds: geometry.ScreenBounds, Foreground: initial.Window, Generation: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, point := range []image.Point{image.Pt(0, 0), image.Pt(result.Pixels.Bounds().Dx()-1, result.Pixels.Bounds().Dy()-1)} {
|
|
if got := color.RGBAModel.Convert(result.Pixels.At(point.X, point.Y)); got != (color.RGBA{51, 102, 153, 255}) {
|
|
t.Fatalf("mapped pixel %v = %v", point, got)
|
|
}
|
|
}
|
|
zone := geometry.ProviderBounds
|
|
zone.Width /= 2
|
|
zone.Height /= 2
|
|
part, err := adapter.CaptureOCRGeometry(ctx, ref.ID(), &zone)
|
|
if err != nil || part.ScreenBounds.Dx() != expected.Dx()/2 || part.ScreenBounds.Dy() != expected.Dy()/2 {
|
|
t.Fatalf("zone: %+v %v", part, err)
|
|
}
|
|
invalid := zone
|
|
invalid.X -= 10
|
|
if _, err = adapter.CaptureOCRGeometry(ctx, ref.ID(), &invalid); err == nil {
|
|
t.Fatal("outside zone accepted")
|
|
}
|
|
after := action("state")
|
|
if after.Focus != initial.Focus {
|
|
t.Fatal("geometry/capture stole focus")
|
|
}
|
|
protected := findNamedReference(t, tree, "Protected target")
|
|
if !protected.Valid() {
|
|
t.Fatal("protected entry missing")
|
|
}
|
|
if _, err = adapter.CaptureOCRGeometry(ctx, protected.ID(), nil); err != nil {
|
|
t.Fatalf("masked password capture geometry unavailable: %v", err)
|
|
}
|
|
action("hide")
|
|
if _, err = adapter.CaptureOCRGeometry(ctx, ref.ID(), nil); err == nil {
|
|
t.Fatal("hidden source accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|