Files
cthulhu/internal/atspi/ocr_native_test.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")
}
})
}
}