Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions scripts/community-experience-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions scripts/parse-system-anr-wait.py
Original file line number Diff line number Diff line change
@@ -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("<hierarchy")
if start < 0:
return None
try:
root = ET.fromstring(dump[start:])
except ET.ParseError:
return None

launcher_titles = {
"Quickstep isn't responding",
"Pixel Launcher isn't responding",
}
if not any(
node.get("package") == "android"
and node.get("resource-id") == "android:id/alertTitle"
and node.get("text") in launcher_titles
for node in root.iter("node")
):
return None

for node in root.iter("node"):
if node.get("package") != "android" or node.get("text") != "Wait":
continue
match = re.fullmatch(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", node.get("bounds", ""))
if match is None:
continue
left, top, right, bottom = map(int, match.groups())
if right > 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)
33 changes: 30 additions & 3 deletions tests/test_community_experience_gate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pathlib
import subprocess
import unittest


Expand All @@ -14,21 +15,22 @@ 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)
self.assertIn('if [[ -f "${directory}/composer.json" ]]', script)
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:
Expand All @@ -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'
'<hierarchy><node package="android" resource-id="android:id/alertTitle" '
f'text="{title}" />'
f'<node package="{package}" text="Wait" bounds="[100,200][300,260]" />'
'</hierarchy>'
)
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()
Loading