ci(e2e): gate the emulator on window focus to fix the RootViewPicker flake (#468) #474

Merged
JMR-dev merged 1 commits from fix-468-emulator-focus into main 2026-07-09 02:19:38 +00:00
JMR-dev commented 2026-07-09 01:32:59 +00:00 (Migrated from github.com)

Root cause

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 with RootViewWithoutFocusException. When it happens it fails every window-focus-dependent test at once while the ~280 pure-Compose semantics tests (which don't need window focus) pass.

Evidence from a failing E2E (35) leg (PR #470, run 28985259521):

  • Across the entire captured logcat, has-window-focus=true appears ZERO times — the app window never gains focus for the session.
  • Both the first attempt (test pid 4393) and the once-retry (pid 6278) fail identically → a persistent environmental state, not a per-test transient.
  • The failures are test-type-correlated, not time- or class-correlated: within each affected class the non-RootViewPicker tests pass (e.g. AccountPickerScreenTest 4 tests / 1 failure), and failures span the whole run (accountsetup early → reporting late).
  • The stack traces confirm the mechanism — even Intents.intended() funnels through ViewInteraction.check() → RootViewPicker.

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 no longer dismisses the keyguard, and when 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. (The [EmulatorConsole]: Failed to start Emulator console for 5554 line is the unrelated telnet-5554 auth-console warning, not an input/window-focus issue.)

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 between them. After boot it:

  1. Wakes the display (KEYCODE_WAKEUP — never toggles it off the way POWER would).
  2. Dismisses + disables the keyguard (wm dismiss-keyguard, locksettings set-disabled true).
  3. Keeps it awake (svc power stayon true + max screen_off_timeout).
  4. Disables animations (parity with the matrix's old inline lines — and now applied to e2e-preview too, uniformly).
  5. Gates: polls dumpsys power/dumpsys window until the device is interactive (mWakefulness=Awake) and a real window holds input focus (mCurrentFocus non-null) — the exact precondition RootViewPicker needs — re-nudging each iteration so a lost race self-heals.

The gate is soft (bounded wait, then proceeds with a ::warning:: + final device state so a genuine environmental failure is diagnosable from the step log) 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.

Coverage

  • e2e matrix (API 29–36): replaces the inline keyevent 82 + animation-disable with the gate call.
  • e2e-preview (API 37, both shards): replaces the lone keyevent 82 with the gate call (also gains animation-disable).
  • Local preflight (local_instrumented.py, api37_e2e.py): route their keyguard step through the same helper, so local E2E exercises the identical fix.

Tests / validation

  • test_emulator_focus_gate.py unit-tests the pure readiness parser (19 tests, incl. the exact "awake but mCurrentFocus=null" flake signature → not ready), run by the existing traffic-control-tests job.
  • YAML validated (PyYAML parse + actionlint exit 0). Python compiles; SPDX headers on new files.
  • Determinism is self-validated by this PR's own matrix run — the previously-flaky Espresso legs must now pass. Recommend re-running the affected leg(s) 2–3× to confirm the intermittency is gone.

Closes #468

## Root cause 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 with `RootViewWithoutFocusException`. When it happens it fails **every** window-focus-dependent test at once while the ~280 pure-Compose semantics tests (which don't need window focus) pass. Evidence from a failing `E2E (35)` leg (PR #470, run `28985259521`): - Across the entire captured logcat, **`has-window-focus=true` appears ZERO times** — the app window never gains focus for the session. - **Both** the first attempt (test pid 4393) **and** the once-retry (pid 6278) fail identically → a persistent environmental state, not a per-test transient. - The failures are **test-type-correlated, not time- or class-correlated**: within each affected class the non-RootViewPicker tests pass (e.g. `AccountPickerScreenTest` 4 tests / 1 failure), and failures span the whole run (accountsetup early → reporting late). - The stack traces confirm the mechanism — even `Intents.intended()` funnels through `ViewInteraction.check()` → `RootViewPicker`. 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 no longer dismisses the keyguard, and when 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. (The `[EmulatorConsole]: Failed to start Emulator console for 5554` line is the unrelated telnet-5554 auth-console warning, not an input/window-focus issue.) ## 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 between them. After boot it: 1. **Wakes** the display (`KEYCODE_WAKEUP` — never toggles it off the way POWER would). 2. **Dismisses + disables** the keyguard (`wm dismiss-keyguard`, `locksettings set-disabled true`). 3. **Keeps it awake** (`svc power stayon true` + max `screen_off_timeout`). 4. **Disables animations** (parity with the matrix's old inline lines — and now applied to `e2e-preview` too, uniformly). 5. **Gates**: polls `dumpsys power`/`dumpsys window` until the device is interactive (`mWakefulness=Awake`) **and** a real window holds input focus (`mCurrentFocus` non-null) — the exact precondition `RootViewPicker` needs — re-nudging each iteration so a lost race self-heals. The gate is **soft** (bounded wait, then proceeds with a `::warning::` + final device state so a genuine environmental failure is diagnosable from the step log) 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. ## Coverage - `e2e` matrix (API 29–36): replaces the inline `keyevent 82` + animation-disable with the gate call. - `e2e-preview` (API 37, both shards): replaces the lone `keyevent 82` with the gate call (also gains animation-disable). - Local preflight (`local_instrumented.py`, `api37_e2e.py`): route their keyguard step through the same helper, so local E2E exercises the identical fix. ## Tests / validation - `test_emulator_focus_gate.py` unit-tests the pure readiness parser (19 tests, incl. the exact "awake but `mCurrentFocus=null`" flake signature → not ready), run by the existing `traffic-control-tests` job. - YAML validated (PyYAML parse + `actionlint` exit 0). Python compiles; SPDX headers on new files. - **Determinism** is self-validated by this PR's own matrix run — the previously-flaky Espresso legs must now pass. Recommend re-running the affected leg(s) 2–3× to confirm the intermittency is gone. Closes #468
mergify[bot] commented 2026-07-09 01:54:32 +00:00 (Migrated from github.com)

Merge Queue Status

This pull request spent 25 minutes 11 seconds in the queue, including 24 minutes 46 seconds running CI.

Required conditions to merge
<!--- DO NOT EDIT -*- Mergify Payload -*- {"version": 1, "state": "merged", "queue_rule_name": "default", "queued_at": "2026-07-09T01:54:27.700397+00:00", "estimated_time_of_merge": null, "speculative_check_pr": null, "required_conditions": []} -*- Mergify Payload End -*- --> # Merge Queue Status - ✅ **Entered queue** — `2026-07-09 01:54 UTC` · Rule: `default` · triggered by merge protections - ✅ **Checks passed** · on draft #476 - ✅ **Merged** — `2026-07-09 02:19 UTC` · at `58447d7d12f17f2bec660394c93da53b9c7d1b8e` · merge This pull request spent **25 minutes 11 seconds** in the queue, including **24 minutes 46 seconds** running CI. <details> <summary>Required conditions to merge</summary> - `-conflict` - [X] #470 - [X] #474 - `-draft` - [X] #470 - [X] #474 - [X] `base = main` - [X] `check-success = CI passed` - `github-review-approved` [🛡 GitHub repository ruleset rule `main`] - [X] #470 - [X] #474 - `label != broken` - [X] #470 - [X] #474 - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Debug build` - [ ] `check-neutral = Debug build` - [ ] `check-skipped = Debug build` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Unit tests` - [ ] `check-neutral = Unit tests` - [ ] `check-skipped = Unit tests` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = CI passed` - [ ] `check-neutral = CI passed` - [ ] `check-skipped = CI passed` - [X] any of [🛡 GitHub repository ruleset rule `main`]: - [X] `check-success = @github-actions/CI passed` - [ ] `check-neutral = @github-actions/CI passed` - [ ] `check-skipped = @github-actions/CI passed` </details>
Sign in to join this conversation.