204 lines
9.7 KiB
Python
204 lines
9.7 KiB
Python
"""Dispatch-race and uncertain-outcome tests using inert CUA engines only."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from framebridge_expanded import display_context as dc
|
|
from framebridge_expanded import native_host as nh
|
|
from framebridge_expanded import adapter as ad
|
|
|
|
class CuaError(RuntimeError):
|
|
def __init__(self, code, message=None):
|
|
self.code = code
|
|
super().__init__(message or code)
|
|
|
|
class NativeDispatchTests(unittest.TestCase):
|
|
def fake(self, calls, action=None):
|
|
cua = SimpleNamespace(CuaTransportError=CuaError)
|
|
def run(argv, timeout=15):
|
|
calls.append(list(argv))
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
cua._run = run
|
|
cua.handle_cua_message = lambda msg: action(cua) if action else {"ok": True}
|
|
return cua
|
|
|
|
def checker(self, fail_at):
|
|
state = {"count": 0}
|
|
def check(env):
|
|
state["count"] += 1
|
|
if state["count"] >= fail_at:
|
|
raise dc.DisplayContextError("CUA_FOREGROUND_SESSION_CHANGED")
|
|
return {}
|
|
return check
|
|
|
|
def test_queue_then_context_change_blocks_first_primitive(self):
|
|
calls = []
|
|
cua = self.fake(calls, lambda engine: engine._run(["xdotool", "key", "a"]))
|
|
state = nh.install_cua_guard(cua, env={}, checker=self.checker(2))
|
|
with self.assertRaises(CuaError):
|
|
cua.handle_cua_message({})
|
|
self.assertEqual(calls, [])
|
|
result = nh.decorate_response({"executed": False}, state, {})
|
|
self.assertFalse(result["input_may_have_been_sent"])
|
|
self.assertFalse(result["performed"])
|
|
self.assertFalse(result["do_not_replay"])
|
|
|
|
def test_context_change_between_query_and_input_blocks_input(self):
|
|
calls = []
|
|
def action(cua):
|
|
cua._run(["xdotool", "getactivewindow"])
|
|
cua._run(["xdotool", "click", "1"])
|
|
cua = self.fake(calls, action)
|
|
state = nh.install_cua_guard(cua, env={}, checker=self.checker(3))
|
|
with self.assertRaises(CuaError):
|
|
cua.handle_cua_message({})
|
|
self.assertEqual(calls, [["xdotool", "getactivewindow"]])
|
|
self.assertFalse(state["input_started"])
|
|
|
|
def test_postflight_failure_after_input_retains_uncertain_outcome(self):
|
|
calls = []
|
|
cua = self.fake(calls, lambda engine: engine._run(["xdotool", "type", "x"]))
|
|
state = nh.install_cua_guard(cua, env={}, checker=self.checker(3))
|
|
with self.assertRaises(CuaError):
|
|
cua.handle_cua_message({})
|
|
result = nh.decorate_response({"executed": True, "performed": True}, state, {})
|
|
self.assertFalse(result["executed"])
|
|
self.assertTrue(result["input_may_have_been_sent"])
|
|
self.assertTrue(result["do_not_replay"])
|
|
self.assertNotEqual(result.get("performed"), False)
|
|
self.assertTrue(json.loads(result["output"])["result_discarded"])
|
|
|
|
def test_observe_late_failure_does_not_claim_input(self):
|
|
calls = []
|
|
cua = self.fake(calls, lambda engine: engine._run(["import", "-window", "root", "fixture.png"]))
|
|
state = nh.install_cua_guard(cua, env={}, checker=self.checker(3))
|
|
with self.assertRaises(CuaError):
|
|
cua.handle_cua_message({})
|
|
result = nh.decorate_response({"executed": False}, state, {})
|
|
self.assertFalse(result["input_may_have_been_sent"])
|
|
self.assertFalse(result["performed"])
|
|
|
|
def test_successful_input_is_reported_performed(self):
|
|
calls = []
|
|
cua = self.fake(calls, lambda engine: engine._run(["/usr/bin/xdotool", "key", "a"]))
|
|
state = nh.install_cua_guard(cua, env={}, checker=lambda env: {})
|
|
cua.handle_cua_message({})
|
|
result = nh.decorate_response({"executed": True}, state, {})
|
|
self.assertTrue(result["performed"])
|
|
self.assertTrue(result["input_may_have_been_sent"])
|
|
self.assertFalse(result["do_not_replay"])
|
|
|
|
def test_captured_pointer_helper_rechecks_after_its_internal_query(self):
|
|
calls = []
|
|
cua = self.fake(calls)
|
|
cua._run_v3_base = cua._run
|
|
def helper(argv, timeout=15):
|
|
cua._run_v3_base(["xdotool", "getmouselocation", "--shell"])
|
|
return cua._run_v3_base(argv, timeout=timeout)
|
|
cua._run = helper
|
|
cua.handle_cua_message = lambda msg: cua._run(["xdotool", "mousemove", "1", "1"])
|
|
state = nh.install_cua_guard(cua, env={}, checker=self.checker(4))
|
|
with self.assertRaises(CuaError):
|
|
cua.handle_cua_message({})
|
|
self.assertEqual(calls, [["xdotool", "getmouselocation", "--shell"]])
|
|
self.assertFalse(state["input_started"])
|
|
|
|
def test_metadata_does_not_leak_to_next_non_cua_response(self):
|
|
state = {"active": True, "input_started": True, "failed_code": ""}
|
|
first = nh.decorate_response({"executed": True}, state, {})
|
|
self.assertTrue(first["input_may_have_been_sent"])
|
|
second = {"executed": True, "channel": "CONTROL-DIRECT"}
|
|
self.assertEqual(nh.decorate_response(second, state, {}), second)
|
|
self.assertNotIn("input_may_have_been_sent", second)
|
|
|
|
class NativeFramingTests(unittest.TestCase):
|
|
def test_captured_host_protocol_rejects_unbound_display_without_input(self):
|
|
root = Path(__file__).resolve().parents[1]
|
|
with tempfile.TemporaryDirectory(prefix="fb-native-guard-") as home:
|
|
env = {"HOME": home, "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
|
"PYTHONDONTWRITEBYTECODE": "1", "LC_ALL": "C.UTF-8",
|
|
"FRAMEBRIDGE_EXPANDED_PREFIX": str(root), dc.BINDING: "{}"}
|
|
message = json.dumps({"op": "cua", "action": "OBSERVE", "authorized": "YES",
|
|
"target": "ASUS", "id": "r2-native-test"}).encode()
|
|
proc = subprocess.run([sys.executable, str(root / "packaging/framebridge-native-expanded")],
|
|
input=struct.pack("<I", len(message)) + message,
|
|
env=env, capture_output=True, timeout=15)
|
|
self.assertEqual(proc.returncode, 0, proc.stderr.decode(errors="replace")[-500:])
|
|
self.assertGreaterEqual(len(proc.stdout), 4)
|
|
length = struct.unpack("<I", proc.stdout[:4])[0]
|
|
self.assertEqual(len(proc.stdout), length + 4)
|
|
result = json.loads(proc.stdout[4:])
|
|
self.assertFalse(result["executed"])
|
|
self.assertFalse(result["performed"])
|
|
self.assertFalse(result["input_may_have_been_sent"])
|
|
self.assertFalse(result["do_not_replay"])
|
|
self.assertEqual(json.loads(result["output"])["error_code"], "CUA_DISPLAY_UNAVAILABLE")
|
|
|
|
class AdapterDispatchTests(unittest.TestCase):
|
|
def adapter(self, response=None, error=None):
|
|
adapter = SimpleNamespace()
|
|
def roundtrip(payload, timeout_s=60):
|
|
if error:
|
|
raise error
|
|
return dict(response or {"executed": True})
|
|
def handle(request, job_id):
|
|
native = adapter.native_cua_roundtrip(request["cua"], timeout_s=60)
|
|
return {"ok": bool(native.get("executed")), "mode": "CUA"}
|
|
adapter.native_cua_roundtrip = roundtrip
|
|
adapter.handle_cua_request = handle
|
|
return adapter
|
|
|
|
def test_native_metadata_survives_original_adapter_mapping(self):
|
|
adapter = self.adapter({"executed": False, "input_may_have_been_sent": True,
|
|
"do_not_replay": True})
|
|
ad.install_adapter_guard(adapter, env={}, checker=lambda env: {})
|
|
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
|
|
self.assertTrue(result["input_may_have_been_sent"])
|
|
self.assertTrue(result["do_not_replay"])
|
|
|
|
def test_before_dispatch_failure_claims_no_input(self):
|
|
adapter = self.adapter()
|
|
def check(env):
|
|
raise dc.DisplayContextError("CUA_FOREGROUND_SESSION_CHANGED")
|
|
ad.install_adapter_guard(adapter, env={}, checker=check)
|
|
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
|
|
self.assertFalse(result["performed"])
|
|
self.assertFalse(result["do_not_replay"])
|
|
|
|
def test_timeout_after_dispatch_never_invites_input_replay(self):
|
|
adapter = self.adapter(error=TimeoutError("native transport timed out"))
|
|
ad.install_adapter_guard(adapter, env={}, checker=lambda env: {})
|
|
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
|
|
self.assertFalse(result["ok"])
|
|
self.assertTrue(result["input_may_have_been_sent"])
|
|
self.assertTrue(result["do_not_replay"])
|
|
self.assertNotIn("performed", result)
|
|
|
|
def test_observe_timeout_has_no_input_outcome(self):
|
|
adapter = self.adapter(error=TimeoutError("native transport timed out"))
|
|
ad.install_adapter_guard(adapter, env={}, checker=lambda env: {})
|
|
result = adapter.handle_cua_request({"cua": {"action": "OBSERVE"}}, "fixture")
|
|
self.assertFalse(result["input_may_have_been_sent"])
|
|
self.assertFalse(result["performed"])
|
|
|
|
def test_adapter_postflight_preserves_explicit_no_input(self):
|
|
adapter = self.adapter({"executed": False, "input_may_have_been_sent": False})
|
|
count = [0]
|
|
def check(env):
|
|
count[0] += 1
|
|
if count[0] == 2:
|
|
raise dc.DisplayContextError("CUA_FOREGROUND_SESSION_CHANGED")
|
|
ad.install_adapter_guard(adapter, env={}, checker=check)
|
|
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
|
|
self.assertFalse(result["input_may_have_been_sent"])
|
|
self.assertFalse(result["performed"])
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|