Files
Artemis-Eapil/tests/integration/test_helper_dynamic_web.py
T

116 lines
4.4 KiB
Python

"""Opt-in cache regression on a selected Android device with Chrome and the helper.
Set ARTEMIS_TEST_DEVICE_SERIAL and run this file with ``pytest -m android``.
Opens a disposable local page via ADB reverse; no model call or system-setting edit.
"""
import base64
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from io import BytesIO
import os
import subprocess
import threading
import time
from uuid import uuid4
from PIL import Image
import pytest
from artemis.clients.accessibility_client import AccessibilityClient
from artemis.runtime.adb_endpoint import adb_command
from artemis.runtime.device_lock import DeviceExecutionLock
pytestmark = [pytest.mark.android, pytest.mark.integration]
def test_helper_refreshes_dynamic_web_text_and_pixels(tmp_path):
serial = os.getenv("ARTEMIS_TEST_DEVICE_SERIAL")
if not serial:
pytest.skip("Select ARTEMIS_TEST_DEVICE_SERIAL explicitly")
marker = "updated-" + uuid4().hex[:8]
state = {"value": "baseline"}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args):
pass
def do_GET(self):
if self.path == "/state":
body = state["value"].encode()
else:
body = b"""<!doctype html><meta name="viewport" content="width=device-width">
<title>ARTEMIS dynamic observation fixture</title>
<body style="background:#cc2222;font:24px sans-serif"><p id="result">loading</p>
<script>setInterval(async()=>{const v=await(await fetch('/state')).text();
document.getElementById('result').textContent=v;
document.body.style.background=v==='baseline'?'#cc2222':'#22cc22';},500)</script>"""
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def adb(*arguments, check=True):
return subprocess.run(
adb_command(["-s", serial, *arguments]),
capture_output=True,
check=check,
timeout=15,
)
lease = DeviceExecutionLock(serial, description="Dynamic helper observation regression")
lease.acquire(blocking=False)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
worker = threading.Thread(target=server.serve_forever, daemon=True)
worker.start()
client = AccessibilityClient(serial, provision_on_connect=False)
device_port = None
try:
client.connect()
# Let ADB allocate an unused reverse port; never replace a user's mapping.
device_port = adb("reverse", "tcp:0", f"tcp:{server.server_port}").stdout.decode().strip()
assert device_port.isdigit(), "ADB did not allocate a reverse port"
adb(
"shell",
"am",
"start",
"-a",
"android.intent.action.VIEW",
"-d",
f"http://127.0.0.1:{device_port}/",
"-p",
"com.android.chrome",
)
def await_observation(expected, green):
deadline = time.monotonic() + 15
observed_text = []
while time.monotonic() < deadline:
observation = client.get_screen_data()
observed_text = [element.get("text") for element in observation.elements]
raw = base64.b64decode(observation.base64)
with Image.open(BytesIO(raw)) as picture:
pixels = list(picture.convert("RGB").resize((32, 32)).getdata())
count = sum(
g > 150 and r < 100 if green else r > 150 and g < 100 for r, g, _ in pixels
)
if expected in observed_text and count > len(pixels) // 4:
(tmp_path / f"{expected}.jpg").write_bytes(raw)
return
time.sleep(0.5)
pytest.fail(f"Pixels and UI hierarchy did not converge to {expected}: {observed_text}")
await_observation("baseline", False)
state["value"] = marker
await_observation(marker, True)
state["value"] = "baseline"
await_observation("baseline", False)
finally:
client.disconnect()
if device_port and device_port.isdigit():
adb("reverse", "--remove", f"tcp:{device_port}", check=False)
server.shutdown()
server.server_close()
worker.join(timeout=5)
lease.release()