From 761225f5b895ee2e361b42aec36775a5d7b2dd38 Mon Sep 17 00:00:00 2001 From: davidbalbino Date: Sun, 4 Oct 2026 14:49:34 -0300 Subject: [PATCH] Recover launcher ANR in community emulator gate --- scripts/community-experience-gate.sh | 12 +++++++ scripts/parse-system-anr-wait.py | 45 +++++++++++++++++++++++++ tests/test_community_experience_gate.py | 33 ++++++++++++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 scripts/parse-system-anr-wait.py diff --git a/scripts/community-experience-gate.sh b/scripts/community-experience-gate.sh index 12ed9bf..3c15318 100755 --- a/scripts/community-experience-gate.sh +++ b/scripts/community-experience-gate.sh @@ -6,6 +6,7 @@ set -euo pipefail surface=${1:-all} pam_bin=${PAM_BIN:-target/debug/pam} +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) owns_gate_root=0 if [[ -n "${PAM_COMMUNITY_GATE_ROOT:-}" ]]; then gate_root=${PAM_COMMUNITY_GATE_ROOT} @@ -200,6 +201,17 @@ init_mobile() { ui_dump=$(adb shell uiautomator dump /sdcard/pam-community-window.xml 2>/dev/null && \ adb shell cat /sdcard/pam-community-window.xml 2>/dev/null) || true last_ui_dump=${ui_dump} + local wait_point + wait_point=$(printf '%s' "${ui_dump}" | \ + python3 "${script_dir}/parse-system-anr-wait.py") + if [[ -n "${wait_point}" ]]; then + local wait_x wait_y + read -r wait_x wait_y <<<"${wait_point}" + printf 'Community gate: dismissing launcher ANR dialog and retrying %s\n' "${package}" >&2 + adb shell input tap "${wait_x}" "${wait_y}" + sleep 2 + continue + fi if [[ "${ui_dump}" != *"${expected_content}"* ]]; then printf 'Community gate: waiting for rendered content in %s\n' "${package}" sleep 2 diff --git a/scripts/parse-system-anr-wait.py b/scripts/parse-system-anr-wait.py new file mode 100644 index 0000000..ff55593 --- /dev/null +++ b/scripts/parse-system-anr-wait.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Return the center of a system launcher ANR's Wait button, if present.""" + +import re +import sys +import xml.etree.ElementTree as ET + + +def wait_point(dump: str) -> tuple[int, int] | None: + start = dump.find(" left and bottom > top: + return ((left + right) // 2, (top + bottom) // 2) + return None + + +if __name__ == "__main__": + point = wait_point(sys.stdin.read()) + if point is not None: + print(*point) diff --git a/tests/test_community_experience_gate.py b/tests/test_community_experience_gate.py index 5fd8b45..14b027a 100644 --- a/tests/test_community_experience_gate.py +++ b/tests/test_community_experience_gate.py @@ -1,4 +1,5 @@ import pathlib +import subprocess import unittest @@ -14,7 +15,7 @@ def test_gate_creates_and_develops_every_official_project_from_temp(self) -> Non self.assertIn("init_server raw", script) self.assertIn("init_server http", script) self.assertIn("init_server laravel", script) - self.assertIn("init_mobile mobile", script) + self.assertIn("init_mobile native", script) self.assertIn("init_mobile native-ui", script) self.assertIn("screencap -p", script) self.assertIn("PluginException", script) @@ -22,13 +23,14 @@ def test_gate_creates_and_develops_every_official_project_from_temp(self) -> Non self.assertIn('composer.lock is missing in Composer project', script) self.assertIn('dependency artifacts unexpectedly exist in Composer-free project', script) self.assertNotIn('test -f "${directory}/composer.lock"', script) - self.assertIn('exec env PAM_PORT="${port}" "${pam_bin}" dev', script) - self.assertIn('exec "${pam_bin}" dev .', script) + self.assertIn('exec setsid env PAM_PORT="${port}" "${pam_bin}" dev', script) + self.assertIn('exec setsid "${pam_bin}" dev .', script) self.assertIn("stop_dev", script) self.assertIn("report_error", script) self.assertIn("android-logcat.txt", script) self.assertIn("screenshot attempt", script) self.assertIn("Android launch proven", script) + self.assertIn("parse-system-anr-wait.py", script) self.assertIn("for attempt in 1 2 3 4 5", script) def test_release_publication_requires_a_real_first_run(self) -> None: @@ -38,6 +40,31 @@ def test_release_publication_requires_a_real_first_run(self) -> None: publish = workflow.split("\n publish:\n", maxsplit=1)[1] self.assertIn("community-first-run", publish.split("\n runs-on:", maxsplit=1)[0]) + def test_only_system_launcher_anr_wait_button_is_recovered(self) -> None: + parser = ROOT / "scripts/parse-system-anr-wait.py" + + def parse(title: str, package: str = "android") -> str: + hierarchy = ( + 'UI hierchary dumped to: /sdcard/window.xml\n' + '' + f'' + '' + ) + result = subprocess.run( + ["python3", str(parser)], + input=hierarchy, + text=True, + capture_output=True, + check=True, + ) + return result.stdout.strip() + + self.assertEqual(parse("Quickstep isn't responding"), "200 230") + self.assertEqual(parse("Pixel Launcher isn't responding"), "200 230") + self.assertEqual(parse("PAM Native isn't responding"), "") + self.assertEqual(parse("Quickstep isn't responding", "dev.pam.app"), "") + if __name__ == "__main__": unittest.main()