ci(e2e): gate the emulator on window focus to fix the RootViewPicker flake (#468) #474
@@ -365,10 +365,18 @@ def main() -> int:
|
||||
if not booted:
|
||||
raise RuntimeError("API 37 preview emulator failed to boot after 2 attempts.")
|
||||
|
||||
# 4. Dismiss the keyguard, then run the instrumented/E2E suite against the booted emulator.
|
||||
subprocess.run(cmd(adb, "shell", "input", "keyevent", "82"), check=False)
|
||||
|
||||
# 4. Force the emulator to grant the app window focus, then GATE on it (the SAME shared
|
||||
# helper CI's e2e / e2e-preview jobs invoke, issue #468), before running the suite: wake
|
||||
# the display, dismiss + disable the keyguard, keep the screen on, disable animations, and
|
||||
# wait for a focused window. Replaces the lone `input keyevent 82`. Best-effort: fall back
|
||||
# to that legacy nudge if the shared helper is somehow missing.
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
focus_gate = repo_root / ".github" / "scripts" / "emulator_focus_gate.py"
|
||||
if focus_gate.is_file():
|
||||
subprocess.run([sys.executable, str(focus_gate), "--adb", adb], check=False)
|
||||
else:
|
||||
subprocess.run(cmd(adb, "shell", "input", "keyevent", "82"), check=False)
|
||||
|
||||
gradlew = repo_root / ("gradlew.bat" if IS_WINDOWS else "gradlew")
|
||||
print(f"Running :app:connectedDebugAndroidTest against {AVD_NAME}...")
|
||||
test_exit = subprocess.run(
|
||||
|
||||
@@ -324,10 +324,17 @@ def wait_for_boot(adb: str, proc: subprocess.Popen, timeout: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def dismiss_keyguard(adb: str) -> None:
|
||||
# Dismiss the keyguard (mirrors CI + api37_e2e.py). Best-effort: a cold -wipe-data boot
|
||||
# rarely needs it, and the input service can lose a race right after boot.
|
||||
_run_quiet(cmd(adb, "-s", SERIAL, "shell", "input", "keyevent", "82"))
|
||||
def prepare_focus(adb: str) -> None:
|
||||
# Force the emulator to grant the app window focus BEFORE the suite, then gate on it -- the
|
||||
# SAME shared helper CI's e2e / e2e-preview jobs invoke (issue #468), so local preflight
|
||||
# exercises the identical fix. It wakes the display, dismisses + disables the keyguard, keeps
|
||||
# the screen on, disables animations, and waits for a focused window. Best-effort: fall back
|
||||
# to the legacy `input keyevent 82` nudge if the shared helper is somehow missing.
|
||||
gate = REPO_ROOT / ".github" / "scripts" / "emulator_focus_gate.py"
|
||||
if gate.is_file():
|
||||
subprocess.run([sys.executable, str(gate), "--adb", adb, "--serial", SERIAL], check=False)
|
||||
else:
|
||||
_run_quiet(cmd(adb, "-s", SERIAL, "shell", "input", "keyevent", "82"))
|
||||
|
||||
|
||||
def run_tests(test_classes: str) -> int:
|
||||
@@ -420,7 +427,7 @@ def main() -> int:
|
||||
proc = start_emulator(emulator)
|
||||
if wait_for_boot(adb, proc, BOOT_TIMEOUT):
|
||||
print("Emulator booted.")
|
||||
dismiss_keyguard(adb)
|
||||
prepare_focus(adb)
|
||||
_STATE.test_exit = run_tests(args.test_classes)
|
||||
else:
|
||||
warn(f"Emulator did not reach sys.boot_completed within {BOOT_TIMEOUT}s.")
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""emulator_focus_gate.py -- make a booted emulator reliably grant the app window focus
|
||||
BEFORE an instrumented UI suite runs, then GATE on that state (issue #468).
|
||||
|
||||
WHY THIS EXISTS (issue #468 -- the environmental app-window-focus flake)
|
||||
------------------------------------------------------------------------
|
||||
Intermittently, on the CI emulator the launched activity window has
|
||||
``has-window-focus=false`` for the WHOLE instrumented run, so Espresso's ``RootViewPicker``
|
||||
(used by ``onView(...).check()``, ``Intents.intended()``, ``Espresso.pressBack()`` and
|
||||
focus-dependent clipboard reads) waits 10s for a focused root and times out --
|
||||
``RootViewWithoutFocusException``. It fails EVERY window-focus-dependent test at once while
|
||||
the ~280 pure-Compose semantics tests (which do not need window focus) pass. Root-cause
|
||||
evidence from a failing ``E2E (35)`` leg (PR #470, run 28985259521): across the entire
|
||||
captured logcat ``has-window-focus=true`` appears ZERO times and both the first attempt and
|
||||
the once-retry fail identically -- i.e. the window NEVER gains focus for the session, a
|
||||
persistent environmental state, not a per-test transient.
|
||||
|
||||
The prior mitigation was a single fire-and-forget ``adb shell input keyevent 82`` (MENU)
|
||||
right after ``sys.boot_completed=1``. On modern Android (API 30+) MENU does NOT reliably
|
||||
dismiss the keyguard, and when it is delivered before SystemUI/keyguard finishes coming up it
|
||||
is simply dropped ("no focused window"). The insecure keyguard / non-interactive display then
|
||||
persists and no app window ever takes focus -- hence the intermittent, whole-leg flake.
|
||||
|
||||
WHAT THIS DOES
|
||||
--------------
|
||||
A single shared mechanism invoked identically by every E2E job (the ``e2e`` API 29-36 matrix
|
||||
AND the ``e2e-preview`` API 37 job in ``.github/workflows/ci.yml``) and by the local preflight
|
||||
runners (``local_instrumented.py`` / ``api37_e2e.py``), so the fix cannot drift between them:
|
||||
|
||||
1. PREPARE the device so an app window CAN take focus, and keep it that way for the whole
|
||||
run (all best-effort; a missing service right after boot must never abort the leg):
|
||||
* ``input keyevent WAKEUP`` (224) -- force the display INTERACTIVE (never toggles it
|
||||
off the way POWER would).
|
||||
* ``wm dismiss-keyguard`` -- dismiss the (insecure) keyguard now.
|
||||
* ``locksettings set-disabled true`` -- disable the lock screen for the session so it
|
||||
cannot re-curtain the app window mid-run.
|
||||
* ``svc power stayon true`` + a max ``screen_off_timeout`` -- never sleep during the run.
|
||||
* ``input keyevent 82`` (MENU) -- legacy nudge, kept harmless for parity with #454.
|
||||
* zero the three animation scales -- deterministic UI tests (this also gives the
|
||||
``e2e-preview`` job the animation-disable the matrix already had -- uniformly).
|
||||
2. GATE: poll ``dumpsys power`` + ``dumpsys window`` until the device is interactive
|
||||
(``mWakefulness=Awake``) AND a real window holds input focus (``mCurrentFocus`` is a
|
||||
``Window{...}``, not ``null``) -- i.e. the exact precondition ``RootViewPicker`` needs --
|
||||
re-issuing the wake / dismiss-keyguard nudges each iteration so a lost race self-heals.
|
||||
|
||||
The gate is SOFT: it waits up to ``--timeout`` seconds and then proceeds regardless, printing a
|
||||
GitHub ``::warning::`` annotation and the final device state if it never confirmed focus (the
|
||||
determinism comes from the PREPARE actions + the wait; a parsing quirk on some API level must
|
||||
not convert an otherwise-fine leg into a hard failure -- the real tests remain the arbiter).
|
||||
It always prints the final ``mWakefulness`` / ``mCurrentFocus`` / keyguard state so a genuine
|
||||
environmental failure is diagnosable from the step log without downloading artifacts.
|
||||
|
||||
Pure standard library, cross-platform (Windows / Linux / macOS): ``adb`` is invoked via
|
||||
subprocess. The readiness parser (``evaluate_readiness``) is a pure function, unit-tested by
|
||||
``test_emulator_focus_gate.py`` (run by the ``traffic-control-tests`` CI job).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
|
||||
# WAKEUP (not POWER): guarantees the display ends up INTERACTIVE. POWER (26) toggles, so it
|
||||
# would turn an already-on display OFF. MENU (82) is kept only as a legacy parity nudge.
|
||||
KEYCODE_WAKEUP = "224"
|
||||
KEYCODE_MENU = "82"
|
||||
# Max int -- effectively "never" auto-sleep the screen during the suite.
|
||||
SCREEN_OFF_TIMEOUT_MS = "2147483647"
|
||||
DEFAULT_TIMEOUT_S = 90
|
||||
POLL_INTERVAL_S = 2
|
||||
|
||||
|
||||
class Readiness(NamedTuple):
|
||||
"""Outcome of parsing ``dumpsys power`` + ``dumpsys window`` for focus readiness."""
|
||||
|
||||
ready: bool
|
||||
awake: bool
|
||||
focus_state: str # 'focused' | 'unfocused' | 'unknown'
|
||||
focus_value: str # the mCurrentFocus / mFocusedWindow token, or ''
|
||||
keyguard_state: str # 'showing' | 'not_showing' | 'unknown'
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
return (
|
||||
f"awake={self.awake} focus={self.focus_state}"
|
||||
f"({self.focus_value or '-'}) keyguard={self.keyguard_state}"
|
||||
)
|
||||
|
||||
|
||||
def _is_awake(power_out: str) -> bool:
|
||||
"""True if ``dumpsys power`` reports an INTERACTIVE display. ``mWakefulness=Awake`` is the
|
||||
stable signal across API 29-37; ``Display Power: state=ON`` / ``mInteractive=true`` are
|
||||
accepted as fallbacks for dump-format drift."""
|
||||
return bool(
|
||||
re.search(r"mWakefulness=Awake\b", power_out)
|
||||
or re.search(r"Display Power:\s*state=ON\b", power_out)
|
||||
or re.search(r"mInteractive=true\b", power_out)
|
||||
)
|
||||
|
||||
|
||||
def _focus(window_out: str) -> tuple[str, str]:
|
||||
"""Classify the current input focus from ``dumpsys window``.
|
||||
|
||||
Returns ``(state, value)`` where state is 'focused' (a non-null ``Window{...}`` holds
|
||||
focus -- what RootViewPicker needs), 'unfocused' (focus is explicitly ``null`` -- asleep /
|
||||
keyguard-curtained / no focusable window), or 'unknown' (the field is absent on this dump
|
||||
format). ``mCurrentFocus`` is preferred; ``mFocusedWindow`` is the fallback field name."""
|
||||
tokens = re.findall(r"mCurrentFocus=(\S+)", window_out)
|
||||
if not tokens:
|
||||
tokens = re.findall(r"mFocusedWindow=(\S+)", window_out)
|
||||
if not tokens:
|
||||
return ("unknown", "")
|
||||
non_null = [t for t in tokens if t != "null"]
|
||||
if non_null:
|
||||
return ("focused", non_null[0])
|
||||
return ("unfocused", "null")
|
||||
|
||||
|
||||
def _keyguard(window_out: str) -> str:
|
||||
"""Best-effort keyguard state from ``dumpsys window``: 'showing' / 'not_showing' /
|
||||
'unknown'. Informational for the summary, plus a fallback readiness signal when the focus
|
||||
field is absent. Field names vary by API level, so several are accepted."""
|
||||
match = re.search(
|
||||
r"(?:mShowingLockscreen|mDreamingLockscreen|isKeyguardShowing|"
|
||||
r"mKeyguardShowing|keyguardShowing|mKeyguardOccluded)=(true|false)",
|
||||
window_out,
|
||||
)
|
||||
if not match:
|
||||
return "unknown"
|
||||
return "showing" if match.group(1) == "true" else "not_showing"
|
||||
|
||||
|
||||
def evaluate_readiness(power_out: str, window_out: str) -> Readiness:
|
||||
"""Pure decision core (unit-tested). The device is READY for a focus-dependent UI suite
|
||||
when it is interactive AND a real window holds input focus. When the focus field is absent
|
||||
on a given dump format, fall back to "interactive AND keyguard explicitly not showing" so a
|
||||
format quirk cannot hang the gate forever."""
|
||||
awake = _is_awake(power_out)
|
||||
focus_state, focus_value = _focus(window_out)
|
||||
keyguard_state = _keyguard(window_out)
|
||||
ready = awake and (
|
||||
focus_state == "focused"
|
||||
or (focus_state == "unknown" and keyguard_state == "not_showing")
|
||||
)
|
||||
return Readiness(ready, awake, focus_state, focus_value, keyguard_state)
|
||||
|
||||
|
||||
def _adb_base(adb: str, serial: str | None) -> list[str]:
|
||||
return [adb, "-s", serial] if serial else [adb]
|
||||
|
||||
|
||||
def _adb_quiet(adb: str, serial: str | None, *args: str) -> None:
|
||||
"""Run an ``adb`` command, swallowing output and any error -- every prepare nudge is
|
||||
best-effort (a service can lose a race right after boot; a missing tool must not abort)."""
|
||||
try:
|
||||
subprocess.run(
|
||||
_adb_base(adb, serial) + list(args),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
|
||||
def _adb_capture(adb: str, serial: str | None, *args: str) -> str:
|
||||
try:
|
||||
return (
|
||||
subprocess.run(
|
||||
_adb_base(adb, serial) + list(args),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
).stdout
|
||||
or ""
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ""
|
||||
|
||||
|
||||
def nudge_focus(adb: str, serial: str | None) -> None:
|
||||
"""Wake the display + dismiss the keyguard. Cheap and idempotent, so it is re-issued every
|
||||
poll iteration to self-heal a nudge that lost the post-boot race with SystemUI/keyguard."""
|
||||
_adb_quiet(adb, serial, "shell", "input", "keyevent", KEYCODE_WAKEUP)
|
||||
_adb_quiet(adb, serial, "shell", "wm", "dismiss-keyguard")
|
||||
|
||||
|
||||
def prepare_device(adb: str, serial: str | None) -> None:
|
||||
"""One-time device preparation: disable the lock screen for the session, keep the screen on
|
||||
for the whole run, zero the animation scales for deterministic UI tests, and issue the first
|
||||
wake / dismiss-keyguard nudge. All best-effort."""
|
||||
print("focus-gate: preparing device (wake + dismiss-keyguard + stay-awake + no-animations)")
|
||||
nudge_focus(adb, serial)
|
||||
_adb_quiet(adb, serial, "shell", "input", "keyevent", KEYCODE_MENU) # legacy #454 parity
|
||||
_adb_quiet(adb, serial, "shell", "locksettings", "set-disabled", "true")
|
||||
_adb_quiet(adb, serial, "shell", "svc", "power", "stayon", "true")
|
||||
_adb_quiet(adb, serial, "shell", "settings", "put", "system",
|
||||
"screen_off_timeout", SCREEN_OFF_TIMEOUT_MS)
|
||||
for scale in ("window_animation_scale", "transition_animation_scale",
|
||||
"animator_duration_scale"):
|
||||
_adb_quiet(adb, serial, "shell", "settings", "put", "global", scale, "0.0")
|
||||
|
||||
|
||||
def probe(adb: str, serial: str | None) -> Readiness:
|
||||
power_out = _adb_capture(adb, serial, "shell", "dumpsys", "power")
|
||||
window_out = _adb_capture(adb, serial, "shell", "dumpsys", "window")
|
||||
return evaluate_readiness(power_out, window_out)
|
||||
|
||||
|
||||
def wait_for_focus(adb: str, serial: str | None, timeout: int, label: str) -> Readiness:
|
||||
"""Prepare the device, then poll (re-nudging each iteration) until it is interactive with a
|
||||
focused window, or ``timeout`` seconds elapse. Returns the final Readiness (SOFT gate: the
|
||||
caller proceeds regardless -- see the module docstring)."""
|
||||
tag = f" [{label}]" if label else ""
|
||||
prepare_device(adb, serial)
|
||||
deadline = time.monotonic() + timeout
|
||||
last = probe(adb, serial)
|
||||
attempt = 0
|
||||
while True:
|
||||
if last.ready:
|
||||
elapsed = timeout - max(0, int(deadline - time.monotonic()))
|
||||
print(f"focus-gate{tag}: READY after ~{elapsed}s -- {last.summary}")
|
||||
return last
|
||||
if time.monotonic() >= deadline:
|
||||
print(f"::warning::focus-gate{tag}: window focus NOT confirmed within {timeout}s "
|
||||
f"-- proceeding anyway -- {last.summary}")
|
||||
return last
|
||||
attempt += 1
|
||||
if attempt % 5 == 0:
|
||||
print(f"focus-gate{tag}: waiting for window focus -- {last.summary}")
|
||||
nudge_focus(adb, serial)
|
||||
time.sleep(POLL_INTERVAL_S)
|
||||
last = probe(adb, serial)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="emulator_focus_gate.py",
|
||||
description=(
|
||||
"Force a booted emulator to grant the app window focus (wake + dismiss-keyguard + "
|
||||
"stay-awake + no-animations) and gate on that state before an instrumented UI "
|
||||
"suite runs. Shared by CI's e2e / e2e-preview jobs and the local preflight runners "
|
||||
"(issue #468)."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--serial", default=None,
|
||||
help="adb device serial (default: the single attached device).")
|
||||
parser.add_argument("--adb", default=None,
|
||||
help="Path to adb (default: resolve from PATH). For callers that resolve "
|
||||
"adb from the SDK rather than PATH (e.g. api37_e2e.py).")
|
||||
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_S,
|
||||
help=f"Max seconds to wait for window focus (default {DEFAULT_TIMEOUT_S}).")
|
||||
parser.add_argument("--label", default="",
|
||||
help="Label for log lines (e.g. an API level), for multi-leg runs.")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
adb = args.adb or shutil.which("adb")
|
||||
if not adb:
|
||||
# Non-fatal by contract: never turn a missing-tool hiccup into a red leg. The suite that
|
||||
# follows will surface a genuinely broken device.
|
||||
print("::warning::focus-gate: adb not on PATH -- skipping focus preparation/gate")
|
||||
return 0
|
||||
|
||||
wait_for_focus(adb, args.serial, args.timeout, args.label)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Unit tests for the pure readiness parser of emulator_focus_gate.py (no adb, no emulator).
|
||||
|
||||
Covers the decision core that decides whether a booted emulator is ready for a focus-dependent
|
||||
instrumented UI suite (issue #468): interactive (``mWakefulness=Awake``) AND a real window holds
|
||||
input focus (``mCurrentFocus`` is a non-null ``Window{...}``). The window-focus flake this guards
|
||||
against is exactly the "awake but mCurrentFocus=null" state, so that case must read NOT ready."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
import emulator_focus_gate as gate
|
||||
|
||||
# A ``dumpsys power`` where the display is interactive vs. asleep.
|
||||
POWER_AWAKE = "Power Manager State:\n mWakefulness=Awake\n mWakefulnessChanging=false\n"
|
||||
POWER_ASLEEP = "Power Manager State:\n mWakefulness=Asleep\n mWakefulnessChanging=false\n"
|
||||
|
||||
# ``dumpsys window`` with a focused app window (the healthy state RootViewPicker needs)...
|
||||
WINDOW_FOCUSED = (
|
||||
" mCurrentFocus=Window{23e192a u0 org.libremail.app/org.libremail.MainActivity}\n"
|
||||
" mFocusedApp=ActivityRecord{a1 u0 org.libremail.app/.MainActivity t9}\n"
|
||||
" mDreamingLockscreen=false\n"
|
||||
)
|
||||
# ...and the flake state: interactive-parse aside, NO window holds focus.
|
||||
WINDOW_NO_FOCUS = " mCurrentFocus=null\n mFocusedApp=null\n mDreamingLockscreen=true\n"
|
||||
|
||||
|
||||
class AwakeParsingTests(unittest.TestCase):
|
||||
def test_mwakefulness_awake(self) -> None:
|
||||
self.assertTrue(gate._is_awake(POWER_AWAKE))
|
||||
|
||||
def test_mwakefulness_asleep(self) -> None:
|
||||
self.assertFalse(gate._is_awake(POWER_ASLEEP))
|
||||
|
||||
def test_display_power_state_on_fallback(self) -> None:
|
||||
self.assertTrue(gate._is_awake("Display Power: state=ON"))
|
||||
|
||||
def test_minteractive_fallback(self) -> None:
|
||||
self.assertTrue(gate._is_awake("mInteractive=true"))
|
||||
|
||||
def test_empty_is_not_awake(self) -> None:
|
||||
self.assertFalse(gate._is_awake(""))
|
||||
|
||||
|
||||
class FocusParsingTests(unittest.TestCase):
|
||||
def test_non_null_current_focus(self) -> None:
|
||||
state, value = gate._focus(WINDOW_FOCUSED)
|
||||
self.assertEqual(state, "focused")
|
||||
self.assertTrue(value.startswith("Window{"))
|
||||
|
||||
def test_null_current_focus(self) -> None:
|
||||
self.assertEqual(gate._focus(WINDOW_NO_FOCUS), ("unfocused", "null"))
|
||||
|
||||
def test_focused_window_fallback_field(self) -> None:
|
||||
state, value = gate._focus("mFocusedWindow=Window{deadbeef u0 launcher}\n")
|
||||
self.assertEqual(state, "focused")
|
||||
self.assertEqual(value, "Window{deadbeef")
|
||||
|
||||
def test_absent_focus_field_is_unknown(self) -> None:
|
||||
self.assertEqual(gate._focus("no focus fields here"), ("unknown", ""))
|
||||
|
||||
|
||||
class KeyguardParsingTests(unittest.TestCase):
|
||||
def test_showing(self) -> None:
|
||||
self.assertEqual(gate._keyguard("mDreamingLockscreen=true"), "showing")
|
||||
|
||||
def test_not_showing(self) -> None:
|
||||
self.assertEqual(gate._keyguard("isKeyguardShowing=false"), "not_showing")
|
||||
|
||||
def test_unknown(self) -> None:
|
||||
self.assertEqual(gate._keyguard("nothing relevant"), "unknown")
|
||||
|
||||
|
||||
class EvaluateReadinessTests(unittest.TestCase):
|
||||
def test_awake_and_focused_is_ready(self) -> None:
|
||||
result = gate.evaluate_readiness(POWER_AWAKE, WINDOW_FOCUSED)
|
||||
self.assertTrue(result.ready)
|
||||
self.assertTrue(result.awake)
|
||||
self.assertEqual(result.focus_state, "focused")
|
||||
|
||||
def test_the_flake_awake_but_no_focus_is_not_ready(self) -> None:
|
||||
# The exact issue #468 signature: display parses/awake but no window has focus.
|
||||
result = gate.evaluate_readiness(POWER_AWAKE, WINDOW_NO_FOCUS)
|
||||
self.assertFalse(result.ready)
|
||||
|
||||
def test_asleep_even_with_focus_is_not_ready(self) -> None:
|
||||
result = gate.evaluate_readiness(POWER_ASLEEP, WINDOW_FOCUSED)
|
||||
self.assertFalse(result.ready)
|
||||
|
||||
def test_unknown_focus_but_awake_and_keyguard_gone_is_ready(self) -> None:
|
||||
# Fallback so a dump format without mCurrentFocus can't hang the gate forever.
|
||||
result = gate.evaluate_readiness(POWER_AWAKE, "mDreamingLockscreen=false")
|
||||
self.assertTrue(result.ready)
|
||||
|
||||
def test_unknown_focus_and_keyguard_showing_is_not_ready(self) -> None:
|
||||
result = gate.evaluate_readiness(POWER_AWAKE, "mDreamingLockscreen=true")
|
||||
self.assertFalse(result.ready)
|
||||
|
||||
def test_unknown_focus_and_keyguard_unknown_is_not_ready(self) -> None:
|
||||
result = gate.evaluate_readiness(POWER_AWAKE, "")
|
||||
self.assertFalse(result.ready)
|
||||
|
||||
def test_summary_is_human_readable(self) -> None:
|
||||
summary = gate.evaluate_readiness(POWER_AWAKE, WINDOW_FOCUSED).summary
|
||||
self.assertIn("awake=True", summary)
|
||||
self.assertIn("focus=focused", summary)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -632,12 +632,20 @@ jobs:
|
||||
for attempt in 1 2; do boot_emulator "$attempt" && { booted=1; break; }; done
|
||||
[ "$booted" = "1" ] || { echo "::error::API ${{ matrix.api-level }} emulator failed to boot after 2 attempts"; exit 1; }
|
||||
|
||||
# NON-FATAL unlock (the boot-race fix) + disable animations for deterministic UI tests
|
||||
# (parity with the replaced android-emulator-runner `disable-animations: true`).
|
||||
adb shell input keyevent 82 || true
|
||||
adb shell settings put global window_animation_scale 0.0 || true
|
||||
adb shell settings put global transition_animation_scale 0.0 || true
|
||||
adb shell settings put global animator_duration_scale 0.0 || true
|
||||
# Focus/readiness gate (issue #468): the durable fix for the intermittent app-window-
|
||||
# focus flake. Occasionally the launched activity window has has-window-focus=false for
|
||||
# the WHOLE run, so Espresso's RootViewPicker (onView().check(), Intents.intended(),
|
||||
# pressBack(), focus-dependent clipboard) times out after 10s and fails EVERY
|
||||
# focus-dependent test at once while the ~280 pure-Compose tests pass (evidence: a
|
||||
# failing E2E leg's logcat had has-window-focus=true ZERO times, both attempt + retry).
|
||||
# The single `input keyevent 82` here was too weak (MENU no longer dismisses the modern
|
||||
# keyguard, and races SystemUI coming up). This shared helper WAKES the display, dismisses
|
||||
# + disables the keyguard, keeps the screen on, disables animations (parity with the
|
||||
# replaced `disable-animations: true`), then WAITS until a real window holds input focus
|
||||
# before the suite runs. It is the IDENTICAL mechanism the e2e-preview job and the local
|
||||
# preflight runners invoke, so the fix cannot drift between jobs. Non-fatal (`|| true`),
|
||||
# preserving #454's guarantee that the unlock never aborts the boot.
|
||||
python3 .github/scripts/emulator_focus_gate.py --label "api${{ matrix.api-level }}" || true
|
||||
|
||||
# WEDGE (hang) smoking-gun capture (#404, restored to the matrix by #421). On the wrapper
|
||||
# `timeout` below (exit 124), grab the smoking gun WHILE this hand-provisioned emulator is
|
||||
@@ -968,7 +976,13 @@ jobs:
|
||||
for attempt in 1 2; do boot_emulator "$attempt" && { booted=1; break; }; done
|
||||
[ "$booted" = "1" ] || { echo "::error::API 37 preview emulator failed to boot after 2 attempts"; exit 1; }
|
||||
|
||||
adb shell input keyevent 82 || true
|
||||
# Focus/readiness gate (issue #468) -- the IDENTICAL shared mechanism the `e2e` matrix job
|
||||
# (and the local preflight runners) invoke, so the fix can't drift: wake the display,
|
||||
# dismiss + disable the keyguard, keep the screen on, disable animations, then WAIT until a
|
||||
# real window holds input focus before the suite runs. This replaces the lone `input
|
||||
# keyevent 82` and, applied UNIFORMLY, also gives this preview job the animation-disable
|
||||
# the matrix already had. Non-fatal (`|| true`) -- the unlock must never abort the boot.
|
||||
python3 .github/scripts/emulator_focus_gate.py --label "api37-shard${{ matrix.shard }}" || true
|
||||
|
||||
# WEDGE (hang) smoking-gun capture (#404). On the wrapper `timeout` below (exit 124), grab
|
||||
# the smoking gun WHILE this hand-provisioned emulator is still alive (it stays up until the
|
||||
|
||||
Reference in New Issue
Block a user