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
112 lines
4.7 KiB
Python
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()
|