Files
LibreMail/.github/scripts/test_emulator_focus_gate.py
JMR-dev 58447d7d12 ci(e2e): gate the emulator on window focus to fix the RootViewPicker flake (#468)
Root cause: intermittently the launched activity window has has-window-focus=false
for the WHOLE instrumented 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 semantics tests
(which don't need window focus) pass. A failing E2E (35) leg's logcat (PR #470, run
28985259521) shows has-window-focus=true ZERO times across the whole session and both
the first attempt and the once-retry fail identically -- a persistent environmental
state, not a per-test transient. The prior mitigation, a single fire-and-forget
`adb shell input keyevent 82` (MENU) right after boot, is too weak: MENU no longer
dismisses the modern (API 30+) keyguard and, delivered before SystemUI/keyguard comes
up, is simply dropped -- so the insecure keyguard / non-interactive display persists
and no app window ever takes focus.

Fix: a single shared helper, .github/scripts/emulator_focus_gate.py, invoked
identically by BOTH E2E jobs (the e2e API 29-36 matrix AND e2e-preview API 37) and by
the local preflight runners (local_instrumented.py / api37_e2e.py), so it cannot drift.
It wakes the display (KEYCODE_WAKEUP), dismisses + disables the keyguard
(wm dismiss-keyguard, locksettings set-disabled true), keeps the screen on
(svc power stayon true + max screen_off_timeout), zeroes the animation scales, then
polls dumpsys power/window until the device is interactive AND a real window holds
input focus (mCurrentFocus non-null) -- re-nudging each iteration -- before the suite
runs. Applied uniformly, this also gives e2e-preview the animation-disable the matrix
already had. The gate is soft (bounded wait, then proceeds with a ::warning:: and the
final device state) and non-fatal (`|| true`), preserving #454's guarantee that the
unlock never aborts the boot; it leaves #454's manual boot, #460's path-filter and
#464's wedge-capture untouched.

The pure readiness parser is unit-tested by test_emulator_focus_gate.py (run by the
traffic-control-tests job). Determinism is validated by this PR's own matrix run.

Closes #468
2026-07-08 20:32:16 -05:00

112 lines
4.7 KiB
Python

# 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()