154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
"""Native input is one-shot and remains owned through uncertain transport cleanup."""
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
import urllib.error
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from artemis.clients import accessibility_client as module
|
|
from artemis.clients.accessibility_client import AccessibilityClient
|
|
from artemis.clients.screen_client_factory import FallbackScreenClient
|
|
from artemis.runtime.helper_manager import HelperSession
|
|
from artemis.runtime.native_protocol import input_completion_seconds
|
|
|
|
|
|
@pytest.fixture
|
|
def native(monkeypatch):
|
|
manager = Mock()
|
|
client = AccessibilityClient("test-device", manager=manager)
|
|
client._session = HelperSession(
|
|
serial="test-device",
|
|
local_port=40001,
|
|
transport_id="1",
|
|
version_code=12,
|
|
version_name="1.3.0",
|
|
owns_forward=True,
|
|
token="test-only",
|
|
protocol_version=3,
|
|
capabilities=("inspect_control", "control_action", "touch_gesture", "cancel_gesture"),
|
|
)
|
|
clock = [0.0]
|
|
sleeps = []
|
|
|
|
def sleep(seconds):
|
|
sleeps.append(seconds)
|
|
clock[0] += seconds
|
|
|
|
monkeypatch.setattr(module, "time", SimpleNamespace(monotonic=lambda: clock[0], sleep=sleep))
|
|
client._http_once = Mock(return_value=b'{"success":true,"execution":"completed","code":"OK"}')
|
|
return client, manager, clock, sleeps
|
|
|
|
|
|
def test_exactly_one_request_and_command_cannot_be_overridden(native):
|
|
client, manager, _, sleeps = native
|
|
receipt = {
|
|
"success": True,
|
|
"execution": "observed",
|
|
"code": "OK",
|
|
"node": {"selection_start": 3},
|
|
}
|
|
client._http_once.return_value = json.dumps(receipt).encode()
|
|
assert client.native_request("inspect_control", {"cmd": "swipe"}, timeout=0.02) == receipt
|
|
client._http_once.assert_called_once_with(
|
|
client.session, "/action", {"cmd": "inspect_control"}, 0.02
|
|
)
|
|
assert sleeps == []
|
|
manager.attach.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"status,execution,attempts,repairs",
|
|
[
|
|
(400, "not_started", 1, 0),
|
|
(401, "not_started", 1, 0),
|
|
(403, "not_started", 1, 0),
|
|
(503, "unknown", 1, 0),
|
|
],
|
|
)
|
|
def test_http_rejection_never_replays_native_input(native, status, execution, attempts, repairs):
|
|
client, manager, _, sleeps = native
|
|
client._http_once.side_effect = urllib.error.HTTPError(
|
|
"http://test", status, "rejected", {}, None
|
|
)
|
|
result = client.native_request("control_action", {"action": "click"}, timeout=0.02)
|
|
assert result["execution"] == execution and not result["success"]
|
|
assert client._http_once.call_count == attempts
|
|
assert client._http_once.call_args.args[-1] >= 5
|
|
assert manager.push_token.call_count == repairs
|
|
manager.reattach.assert_not_called()
|
|
assert bool(sleeps) == (execution == "unknown")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"response", [b"broken", b"[]", b"{}", b'{"success":true,"execution":"unknown"}']
|
|
)
|
|
def test_uncertain_write_drains_without_retry(native, response):
|
|
client, _, clock, sleeps = native
|
|
client._http_once.return_value = response
|
|
result = client.native_request("control_action", {"action": "click"})
|
|
assert result["execution"] == "unknown"
|
|
assert sleeps == [5.0] and clock[0] == 5.0
|
|
client._http_once.assert_called_once()
|
|
|
|
|
|
def test_partial_transport_elapsed_time_counts_toward_input_drain(native):
|
|
client, _, clock, sleeps = native
|
|
|
|
def broken(*args):
|
|
clock[0] = 2
|
|
raise OSError("response lost after dispatch")
|
|
|
|
client._http_once.side_effect = broken
|
|
assert client.native_request("control_action", {})["execution"] == "unknown"
|
|
assert sleeps == [3.0] and clock[0] == 5
|
|
client._http_once.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize("command", ["inspect_control", "cancel_gesture"])
|
|
def test_non_input_calls_do_not_drain_on_failure(native, command):
|
|
client, _, _, sleeps = native
|
|
client._http_once.side_effect = OSError("offline")
|
|
result = client.native_request(command, {}, timeout=0.02)
|
|
assert not result["success"] and sleeps == []
|
|
client._http_once.assert_called_once()
|
|
|
|
|
|
def test_no_session_or_capability_cannot_trigger_attach_or_input(native):
|
|
client, manager, _, sleeps = native
|
|
client._session.capabilities = ()
|
|
assert client.native_request("touch_gesture", {})["code"] == "UNSUPPORTED"
|
|
client._session = None
|
|
assert client.native_capabilities == frozenset()
|
|
assert client.native_request("control_action", {})["execution"] == "not_started"
|
|
client._http_once.assert_not_called()
|
|
manager.attach.assert_not_called()
|
|
assert sleeps == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"duration,budget", [(0, 5), (1000, 6), (10000, 15), (999999, 15), (None, 15)]
|
|
)
|
|
def test_touch_completion_budget_is_bounded(duration, budget):
|
|
assert input_completion_seconds("touch_gesture", {"frames": [{"at_ms": duration}]}) == budget
|
|
|
|
|
|
@pytest.mark.parametrize("backend", ["helper", "uiautomator"])
|
|
def test_native_input_never_uses_fallback_dispatch(backend):
|
|
client = object.__new__(FallbackScreenClient)
|
|
client._active_backend = backend
|
|
client._helper = Mock()
|
|
client._helper.native_capabilities = frozenset({"control_action"})
|
|
unknown = {"success": False, "execution": "unknown", "code": "TRANSPORT_ERROR"}
|
|
client._helper.native_request.return_value = unknown
|
|
client._call = Mock(side_effect=AssertionError("fallback is forbidden"))
|
|
result = client.native_request("control_action", {})
|
|
if backend == "helper":
|
|
assert result is unknown and client.native_capabilities == frozenset({"control_action"})
|
|
client._helper.native_request.assert_called_once()
|
|
else:
|
|
assert result["execution"] == "not_started" and client.native_capabilities == frozenset()
|
|
client._helper.native_request.assert_not_called()
|
|
client._call.assert_not_called()
|