Merge main into feat-239-purge-old-reports
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: preflight
|
||||
description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt) plus the latest-API-level emulator E2E before pushing or opening a PR. Mirrors the merge gate and runs the highest-API E2E via its Gradle Managed Device; the full multi-API matrix stays CI-only. Use before treating a change as done.
|
||||
description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt) plus the top-of-matrix emulator E2E (currently API 35 + 36 via Gradle Managed Devices, then API 37 preview via the hand-provisioning script) before pushing or opening a PR. Mirrors the merge gate; the rest of the multi-API matrix stays CI-only. Use before treating a change as done.
|
||||
---
|
||||
|
||||
# /preflight
|
||||
@@ -13,12 +13,20 @@ Run the same fast checks CI enforces on every PR, in order, and report the outco
|
||||
with a JDK/AGP version mismatch, check `java -version` / `JAVA_HOME` and point it at a 17–21
|
||||
JDK (e.g. Android Studio's bundled JBR) before retrying.
|
||||
- PowerShell: invoke the wrapper as `.\gradlew`. Git Bash / the Bash tool: `./gradlew`.
|
||||
- The final E2E step boots an emulator through a Gradle Managed Device, so the host needs
|
||||
hardware acceleration (WHPX on Windows, KVM on Linux, HVF on macOS). Gradle downloads the
|
||||
system image and boots/tears down the AVD itself — no manual emulator setup — but the first
|
||||
run is slow while the image downloads. If the host has no accelerated emulator and the
|
||||
managed device cannot boot, report the E2E step as not run rather than treating the gate as
|
||||
green.
|
||||
- The final three E2E steps each boot an emulator, so the host needs a **free hardware
|
||||
hypervisor** (Intel VT-x / AMD-V, exposed as WHPX on Windows, KVM on Linux, HVF on macOS).
|
||||
Shut down VirtualBox, Hyper-V-based VMs, WSL2, Docker Desktop, or any other emulator first — a
|
||||
VM holding the hypervisor starves the AVD, and it hangs at 0% CPU and never reaches
|
||||
`sys.boot_completed`. The api35/api36 steps run through Gradle Managed Devices (Gradle
|
||||
downloads the image and boots/tears down each AVD itself); the api37 step is hand-provisioned
|
||||
by `api37_e2e.py` (see Steps). The first run per API level is slow while its system image
|
||||
downloads. If the host has no accelerated emulator and a device cannot boot, report the E2E
|
||||
step as not run rather than treating the gate as green.
|
||||
- The api37 step is a stdlib-only, cross-platform **Python 3** script and needs `python3` plus
|
||||
the Android SDK command-line tools (`sdkmanager`/`avdmanager`) and `emulator` on the machine,
|
||||
located via `ANDROID_SDK_ROOT`/`ANDROID_HOME` (or the per-OS default:
|
||||
`%LOCALAPPDATA%\Android\Sdk` on Windows, `~/Library/Android/sdk` on macOS, `~/Android/Sdk` on
|
||||
Linux). The script installs the preview system image itself on first run.
|
||||
|
||||
## Steps
|
||||
|
||||
@@ -30,7 +38,9 @@ Run these, stopping at the first failure:
|
||||
./gradlew :app:compileDebugAndroidTestKotlin
|
||||
./gradlew :app:lintDebug
|
||||
./gradlew :app:ktlintCheck :app:detekt
|
||||
./gradlew :app:api36DebugAndroidTest # latest-API emulator E2E (highest level in the matrix)
|
||||
./gradlew :app:api35DebugAndroidTest # top-of-matrix emulator E2E (2nd-highest stable level)
|
||||
./gradlew :app:api36DebugAndroidTest # top-of-matrix emulator E2E (highest stable level)
|
||||
python3 .claude/skills/preflight/api37_e2e.py # api37 preview E2E (hand-provisioned; on Windows: py or python)
|
||||
```
|
||||
|
||||
`compileDebugAndroidTestKotlin` compiles the `androidTest` source set — the E2E/instrumented
|
||||
@@ -44,24 +54,44 @@ merge gate even when the build and lint are green. Add `--continue` to any comma
|
||||
`:app:ktlintCheck :app:detekt --continue`) to collect every failure in one pass instead of
|
||||
stopping at the first.
|
||||
|
||||
`api36DebugAndroidTest` is last because it is the slowest: it runs the full instrumented/E2E
|
||||
suite on `api36` — the highest API level in the E2E matrix — via its Gradle Managed Device,
|
||||
which Gradle provisions, boots, and tears down automatically. This one latest-API level is the
|
||||
E2E that preflight runs locally and it must pass; CI fans the same suite out across the whole
|
||||
API matrix (plus the API 37 preview job). Keep `api36DebugAndroidTest` in lockstep with the
|
||||
highest managed device in `app/build.gradle.kts` and the E2E matrix in
|
||||
`.github/workflows/ci.yml` — when a newer API level is added there, run that one instead.
|
||||
The three E2E steps run last because they are the slowest. `api35DebugAndroidTest` and
|
||||
`api36DebugAndroidTest` run the full instrumented/E2E suite on `api35`, then `api36` — the top
|
||||
two stable levels in the E2E matrix — each via its own Gradle Managed Device, which Gradle
|
||||
provisions, boots, and tears down automatically.
|
||||
|
||||
`api37_e2e.py` then runs the same suite on the **API 37 preview** emulator. API 37 has no Gradle
|
||||
Managed Device — its only published system image is the nonstandard `android-37.0` /
|
||||
`google_apis_ps16k` pairing, which neither `ManagedVirtualDevice`'s `apiLevel` (Int) nor
|
||||
`apiPreview` (codename) DSL resolves (see the comment above `testOptions.managedDevices` in
|
||||
`app/build.gradle.kts`), so there is no `api37DebugAndroidTest` task. The script hand-provisions
|
||||
it essentially the way CI's `e2e-preview` job does — installs the image with `sdkmanager`,
|
||||
creates the AVD with `avdmanager`, cold-boots it headless, waits for `sys.boot_completed`, runs
|
||||
`:app:connectedDebugAndroidTest`, then kills the emulator and deletes the AVD. The emulator flags
|
||||
match `e2e-preview` with one deliberate local exception: the **GPU mode**. CI uses
|
||||
`-gpu swiftshader_indirect` (software rendering, deterministic on a headless CI runner); the
|
||||
local run uses `-gpu auto-no-window`, which renders on the host GPU — faster, and the mode that
|
||||
boots cleanly on a dev machine.
|
||||
|
||||
All three levels are the E2E that preflight runs locally and all three must pass; CI fans the
|
||||
same suite out across the whole matrix (API 29–36 in `e2e`, plus API 37 in `e2e-preview`). Keep
|
||||
`api35DebugAndroidTest` / `api36DebugAndroidTest` in lockstep with the top of the managed-device
|
||||
list in `app/build.gradle.kts`, and keep `api37_e2e.py` in lockstep with the `e2e-preview` job in
|
||||
`.github/workflows/ci.yml` (same image string + emulator flags, apart from the intentional GPU-mode
|
||||
difference noted above) — when a newer API level is added there, run the new top levels instead.
|
||||
|
||||
## Reporting
|
||||
|
||||
- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api36 E2E").
|
||||
- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api35+api36+api37 E2E").
|
||||
- On failure, surface the actual Gradle error and point at the relevant report:
|
||||
- unit tests → `app/build/reports/tests/testDebugUnitTest/`
|
||||
- lint → `app/build/reports/lint-results-debug.html`
|
||||
- ktlint → `app/build/reports/ktlint/` (per source set, e.g. `ktlintTestSourceSetCheck/`)
|
||||
- detekt → `app/build/reports/detekt/`
|
||||
- E2E → `app/build/reports/androidTests/managedDevice/` (per-device HTML, e.g. `.../api36/`)
|
||||
- Run only the **latest-API** E2E here (`api36DebugAndroidTest`, the highest level in the
|
||||
matrix); the full multi-API matrix and the API 37 preview job stay CI's job. If the host has
|
||||
no accelerated emulator and the managed device cannot boot, report that E2E could not run
|
||||
rather than treating the gate as green.
|
||||
- api35/api36 E2E → `app/build/reports/androidTests/managedDevice/` (per-device HTML, e.g.
|
||||
`.../api35/`, `.../api36/`)
|
||||
- api37 E2E → `app/build/reports/androidTests/connected/` (the `connectedDebugAndroidTest`
|
||||
report the script drives); the emulator's own boot log is at the temp path the script prints.
|
||||
- Run the **top two stable-API** levels (`api35DebugAndroidTest` + `api36DebugAndroidTest`) plus
|
||||
the **API 37 preview** via `api37_e2e.py`; the rest of the multi-API matrix (API 29–34) stays
|
||||
CI's job. If the host has no accelerated emulator and a device cannot boot (see the hypervisor
|
||||
note in Preconditions), report that E2E could not run rather than treating the gate as green.
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Hand-provision an API 37 (Android 17, preview) emulator, run LibreMail's instrumented/E2E
|
||||
suite against it, then tear the emulator and AVD down. Invoked by the /preflight skill as the
|
||||
third (api37) E2E step, alongside the api35/api36 Gradle Managed Devices.
|
||||
|
||||
WHY THIS SCRIPT EXISTS
|
||||
----------------------
|
||||
API 37's only published system image is the nonstandard "android-37.0" / google_apis_ps16k
|
||||
(16 KB page size) pairing. AGP's Gradle Managed Device DSL can only build an
|
||||
"android-<apiLevel:Int>" package id (apiLevel = 37 -> "android-37") or an
|
||||
"android-<apiPreview:codename>" one -- neither resolves to "android-37.0" -- so there is NO
|
||||
api37DebugAndroidTest task to run. This script custom-provisions the emulator with
|
||||
sdkmanager / avdmanager / emulator directly, closely mirroring CI's `e2e-preview` job in
|
||||
.github/workflows/ci.yml (same system image string, same provisioning/boot sequence, same
|
||||
emulator flags EXCEPT the GPU mode -- CI uses `-gpu swiftshader_indirect` for headless
|
||||
determinism, while this local run uses `-gpu auto-no-window` to render on the host GPU, which
|
||||
is faster; see start_emulator). Keep the two in lockstep on everything but that GPU flag: when a
|
||||
stable, GMD-compatible API 37 image ships, delete this script, fold api37 into
|
||||
testOptions.managedDevices, and fold 37 into CI's `e2e` matrix (dropping the `e2e-preview` job).
|
||||
|
||||
Pure standard library, cross-platform (Windows / Linux / macOS): tool paths and executable
|
||||
suffixes are resolved per-OS, and `.bat` launchers are wrapped through `cmd /c` on Windows.
|
||||
|
||||
HYPERVISOR REQUIREMENT
|
||||
----------------------
|
||||
The emulator boots with `-accel on`, so it needs a FREE hardware hypervisor (Intel VT-x / AMD-V,
|
||||
exposed as WHPX on Windows, KVM on Linux, HVF on macOS). If VirtualBox, Hyper-V, WSL2, Docker
|
||||
Desktop, or another emulator is holding it, `-accel on` fails or the AVD hangs at 0% CPU and never
|
||||
reaches sys.boot_completed. Shut those down before running preflight.
|
||||
|
||||
JDK: the final Gradle step needs a JDK 17-21 daemon (AGP 9.2 fails on JDK 25+), same as the rest
|
||||
of preflight -- point JAVA_HOME at a 17-21 JDK before invoking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Defaults mirror .github/workflows/ci.yml e2e-preview (env.API37_IMAGE, env.ANDROID_PLATFORM,
|
||||
# env.ANDROID_BUILD_TOOLS) and its avdmanager invocation. Do not diverge without updating ci.yml.
|
||||
API37_IMAGE = "system-images;android-37.0;google_apis_ps16k;x86_64"
|
||||
PLATFORM_PKG = "platforms;android-37.0"
|
||||
BUILD_TOOLS = "build-tools;37.0.0"
|
||||
AVD_NAME = "api37"
|
||||
DEVICE_PROFILE = "pixel_2"
|
||||
BOOT_TIMEOUT = 300
|
||||
|
||||
IS_WINDOWS = os.name == "nt"
|
||||
BAT = ".bat" if IS_WINDOWS else ""
|
||||
EXE = ".exe" if IS_WINDOWS else ""
|
||||
|
||||
|
||||
def cmd(tool: str, *args: str) -> list[str]:
|
||||
"""Build an argv list, wrapping Windows `.bat`/`.cmd` launchers through `cmd /c`."""
|
||||
if IS_WINDOWS and tool.lower().endswith((".bat", ".cmd")):
|
||||
return ["cmd", "/c", tool, *args]
|
||||
return [tool, *args]
|
||||
|
||||
|
||||
def find_sdk_root() -> str:
|
||||
candidates = [os.environ.get("ANDROID_SDK_ROOT"), os.environ.get("ANDROID_HOME")]
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
local = os.environ.get("LOCALAPPDATA")
|
||||
if local:
|
||||
candidates.append(os.path.join(local, "Android", "Sdk"))
|
||||
elif system == "Darwin":
|
||||
candidates.append(os.path.expanduser("~/Library/Android/sdk"))
|
||||
else:
|
||||
candidates.append(os.path.expanduser("~/Android/Sdk"))
|
||||
for candidate in candidates:
|
||||
if candidate and os.path.isdir(candidate):
|
||||
return os.path.abspath(candidate)
|
||||
raise RuntimeError(
|
||||
"Android SDK not found. Set ANDROID_SDK_ROOT (or ANDROID_HOME) to your SDK location."
|
||||
)
|
||||
|
||||
|
||||
def resolve_tool(sdk_root: str, rel_paths: list[list[str]], name: str) -> str:
|
||||
for rel in rel_paths:
|
||||
path = os.path.join(sdk_root, *rel)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
raise RuntimeError(
|
||||
f"Could not find {name} under {sdk_root}. "
|
||||
"Install the Android SDK command-line tools + emulator."
|
||||
)
|
||||
|
||||
|
||||
def run_sdkmanager(sdkmanager: str, args: list[str]) -> None:
|
||||
# Feed a stream of "y" so any unaccepted (incl. preview) license prompt is auto-accepted; this
|
||||
# is the non-interactive equivalent of the CI runner having licenses pre-accepted.
|
||||
result = subprocess.run(cmd(sdkmanager, *args), input="y\n" * 50, text=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"sdkmanager failed (exit {result.returncode}) for: {' '.join(args)}")
|
||||
|
||||
|
||||
def create_avd(avdmanager: str, emulator: str) -> None:
|
||||
print(f"Creating AVD '{AVD_NAME}' from {API37_IMAGE} (device: {DEVICE_PROFILE})...")
|
||||
# "no" answers avdmanager's "create a custom hardware profile?" prompt, mirroring CI.
|
||||
result = subprocess.run(
|
||||
cmd(avdmanager, "create", "avd", "-n", AVD_NAME, "-k", API37_IMAGE,
|
||||
"-d", DEVICE_PROFILE, "--force"),
|
||||
input="no\n", text=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"avdmanager create avd failed (exit {result.returncode}).")
|
||||
print("AVDs visible to the emulator:")
|
||||
subprocess.run(cmd(emulator, "-list-avds"), check=False)
|
||||
|
||||
|
||||
def start_emulator(emulator: str, emu_log: Path, attempt: int) -> subprocess.Popen:
|
||||
print(f"Starting API 37 emulator (attempt {attempt})...")
|
||||
# Flags mirror .github/workflows/ci.yml e2e-preview (cold headless boot, hardware accel
|
||||
# required, no cameras), with ONE deliberate LOCAL exception -- the GPU mode. CI uses
|
||||
# `-gpu swiftshader_indirect` (software rendering, deterministic on a headless CI runner);
|
||||
# locally we use `-gpu auto-no-window`, which renders on the host GPU: faster, and the mode
|
||||
# that boots cleanly on a dev machine. Keep everything except the GPU mode in lockstep with
|
||||
# that job.
|
||||
flags = [
|
||||
"-avd", AVD_NAME,
|
||||
"-no-window", "-no-audio", "-no-boot-anim", "-no-snapshot", "-accel", "on",
|
||||
"-gpu", "auto-no-window", "-camera-back", "none", "-camera-front", "none",
|
||||
]
|
||||
log = open(emu_log, "wb") # noqa: SIM115 - handed to the child; closed in the parent below
|
||||
try:
|
||||
proc = subprocess.Popen(cmd(emulator, *flags), stdout=log, stderr=subprocess.STDOUT)
|
||||
finally:
|
||||
log.close() # the child has inherited its own fd; the parent's copy is no longer needed
|
||||
return proc
|
||||
|
||||
|
||||
def wait_for_boot(adb: str, proc: subprocess.Popen, timeout: int) -> bool:
|
||||
subprocess.run(cmd(adb, "start-server"), check=False)
|
||||
deadline = time.monotonic() + timeout
|
||||
|
||||
# Phase 1 -- the literal `adb wait-for-device`, bounded so a dead emulator can't hang the run
|
||||
# (returns as soon as the emulator registers). Mirrors CI's `adb wait-for-device`.
|
||||
try:
|
||||
subprocess.run(cmd(adb, "wait-for-device"), timeout=max(1, deadline - time.monotonic()),
|
||||
check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
if proc.poll() is not None:
|
||||
return False
|
||||
|
||||
# Phase 2 -- poll sys.boot_completed until it flips to 1 (mirrors CI's getprop loop).
|
||||
while time.monotonic() < deadline:
|
||||
if proc.poll() is not None:
|
||||
print(f"Emulator process exited during boot (code {proc.returncode}).",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
out = subprocess.run(cmd(adb, "shell", "getprop", "sys.boot_completed"),
|
||||
capture_output=True, text=True, check=False)
|
||||
if out.stdout.strip() == "1":
|
||||
return True
|
||||
time.sleep(2)
|
||||
return False
|
||||
|
||||
|
||||
def stop_emulator(adb: str | None, proc: subprocess.Popen | None) -> None:
|
||||
if adb:
|
||||
subprocess.run(cmd(adb, "emu", "kill"), check=False,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(2)
|
||||
if proc and proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
|
||||
def tail(path: Path, lines: int = 80) -> None:
|
||||
try:
|
||||
with open(path, "r", errors="replace") as handle:
|
||||
content = handle.readlines()[-lines:]
|
||||
print("--- emulator.log (tail) ---")
|
||||
print("".join(content))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Hand-provision + run the API 37 preview E2E suite.")
|
||||
parser.add_argument("--boot-timeout", type=int, default=BOOT_TIMEOUT,
|
||||
help="Seconds to wait for the emulator to reach sys.boot_completed.")
|
||||
args = parser.parse_args()
|
||||
|
||||
sdk_root = find_sdk_root()
|
||||
print(f"Using Android SDK at: {sdk_root}")
|
||||
sdkmanager = resolve_tool(sdk_root, [
|
||||
["cmdline-tools", "latest", "bin", "sdkmanager" + BAT],
|
||||
["cmdline-tools", "bin", "sdkmanager" + BAT],
|
||||
["tools", "bin", "sdkmanager" + BAT],
|
||||
], "sdkmanager")
|
||||
avdmanager = resolve_tool(sdk_root, [
|
||||
["cmdline-tools", "latest", "bin", "avdmanager" + BAT],
|
||||
["cmdline-tools", "bin", "avdmanager" + BAT],
|
||||
["tools", "bin", "avdmanager" + BAT],
|
||||
], "avdmanager")
|
||||
|
||||
# Pin ANDROID_AVD_HOME so avdmanager (writes it) and the emulator (reads it) agree on the AVD
|
||||
# dir -- the same fix CI's e2e-preview applies to avoid "Unknown AVD name [api37]".
|
||||
avd_home = Path.home() / ".android" / "avd"
|
||||
avd_home.mkdir(parents=True, exist_ok=True)
|
||||
os.environ["ANDROID_AVD_HOME"] = str(avd_home)
|
||||
|
||||
emu_log = Path(tempfile.gettempdir()) / "libremail-api37-emulator.log"
|
||||
adb: str | None = None
|
||||
proc: subprocess.Popen | None = None
|
||||
test_exit = 1
|
||||
|
||||
try:
|
||||
# 1. Install the SDK platform, build-tools, platform-tools, emulator, and preview image.
|
||||
print(f"Installing SDK packages + API 37 preview system image ({API37_IMAGE})...")
|
||||
run_sdkmanager(sdkmanager, ["--licenses"])
|
||||
run_sdkmanager(sdkmanager, [PLATFORM_PKG, BUILD_TOOLS, "platform-tools", "emulator",
|
||||
API37_IMAGE])
|
||||
|
||||
# adb + emulator are only guaranteed present after the install above.
|
||||
adb = resolve_tool(sdk_root, [["platform-tools", "adb" + EXE]], "adb")
|
||||
emulator = resolve_tool(sdk_root, [["emulator", "emulator" + EXE]], "emulator")
|
||||
|
||||
# 2. Create the AVD, mirroring CI.
|
||||
create_avd(avdmanager, emulator)
|
||||
|
||||
# 3. Cold-boot headless, retrying once (mirrors CI's two-attempt boot loop).
|
||||
booted = False
|
||||
for attempt in (1, 2):
|
||||
proc = start_emulator(emulator, emu_log, attempt)
|
||||
if wait_for_boot(adb, proc, args.boot_timeout):
|
||||
booted = True
|
||||
break
|
||||
print(f"API 37 emulator did not boot within {args.boot_timeout}s (attempt {attempt}).",
|
||||
file=sys.stderr)
|
||||
tail(emu_log)
|
||||
stop_emulator(adb, proc)
|
||||
proc = None
|
||||
time.sleep(5)
|
||||
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)
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
gradlew = repo_root / ("gradlew.bat" if IS_WINDOWS else "gradlew")
|
||||
print(f"Running :app:connectedDebugAndroidTest against {AVD_NAME}...")
|
||||
test_exit = subprocess.run(
|
||||
cmd(str(gradlew), ":app:connectedDebugAndroidTest", "--stacktrace"),
|
||||
cwd=str(repo_root), check=False,
|
||||
).returncode
|
||||
except Exception as exc: # noqa: BLE001 - top-level guard so teardown always runs
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
test_exit = 1
|
||||
finally:
|
||||
# 5. Always tear the emulator down and delete the AVD, even on failure.
|
||||
print("Tearing down API 37 emulator and AVD...")
|
||||
stop_emulator(adb, proc)
|
||||
subprocess.run(cmd(avdmanager, "delete", "avd", "-n", AVD_NAME), check=False,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
if test_exit != 0:
|
||||
print(f"api37 connectedDebugAndroidTest failed (exit {test_exit}).", file=sys.stderr)
|
||||
return test_exit
|
||||
print("api37 E2E passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -28,15 +28,22 @@ or via Gradle Managed Devices `./gradlew e2eGroupDebugAndroidTest` (whole matrix
|
||||
|
||||
**Before treating a change as done**, run the fast CI gate: `assembleDebug` +
|
||||
`testDebugUnitTest` + `compileDebugAndroidTestKotlin` + `lintDebug` + `ktlintCheck` +
|
||||
`detekt` + the latest-API emulator E2E `api36DebugAndroidTest` (the `/preflight` skill does
|
||||
all of this). `compileDebugAndroidTestKotlin` compiles the `androidTest` source set that the
|
||||
static part of the gate skips, catching E2E/instrumented-test compile errors before they
|
||||
surface only in CI. `ktlintCheck`/`detekt` cover the `test`/`androidTest` source sets that
|
||||
`lintDebug` skips, so they catch style violations that would otherwise fail CI's Static
|
||||
analysis gate. `api36DebugAndroidTest` runs the instrumented/E2E suite on API 36 — the highest
|
||||
API level in the E2E matrix — via its Gradle Managed Device (Gradle boots and tears down the
|
||||
emulator automatically). Running that one latest-API level locally is required; the full
|
||||
multi-API matrix (and the API 37 preview job) stays CI's job.
|
||||
`detekt` + the top-of-matrix emulator E2E `api35DebugAndroidTest` + `api36DebugAndroidTest` +
|
||||
the API 37 preview E2E via `python3 .claude/skills/preflight/api37_e2e.py` (the `/preflight`
|
||||
skill does all of this). `compileDebugAndroidTestKotlin` compiles the `androidTest` source set
|
||||
that the static part of the gate skips, catching E2E/instrumented-test compile errors before
|
||||
they surface only in CI. `ktlintCheck`/`detekt` cover the `test`/`androidTest` source sets that
|
||||
`lintDebug` skips, so they catch style violations that would otherwise fail CI's Static analysis
|
||||
gate. `api35DebugAndroidTest` and `api36DebugAndroidTest` run the instrumented/E2E suite on the
|
||||
top two stable API levels in the E2E matrix, each via its own Gradle Managed Device (Gradle boots
|
||||
and tears down each emulator automatically). API 37 (preview) has no Gradle Managed Device — its
|
||||
only image is the nonstandard `android-37.0` / `google_apis_ps16k` pairing (see the comment above
|
||||
`testOptions.managedDevices` in `app/build.gradle.kts`) — so `api37_e2e.py` hand-provisions it,
|
||||
mirroring CI's `e2e-preview` job (same image + emulator flags, except it uses host-GPU
|
||||
`-gpu auto-no-window` locally vs CI's headless `-gpu swiftshader_indirect`), boots it headless,
|
||||
runs `connectedDebugAndroidTest`, and tears it down. Running all three levels locally is required; the
|
||||
rest of the multi-API matrix (API 29–34) stays CI's job. Emulators need a free hardware
|
||||
hypervisor (VT-x/WHPX), so shut down VirtualBox/other VMs first or the AVD hangs at 0% CPU.
|
||||
|
||||
## Build-config gotchas
|
||||
|
||||
@@ -70,9 +77,11 @@ pulled in as a real dependency for unit tests because `android.jar`'s version is
|
||||
A change is not done until it ships with passing **unit tests** and **E2E/instrumented tests**
|
||||
that exercise the new or changed behaviour. Writing and committing that E2E/instrumented test
|
||||
is a required part of every task — and the test must actually **run and pass**, not merely
|
||||
compile: preflight runs the latest-API-level emulator E2E locally (`api36DebugAndroidTest`, the
|
||||
highest API level in the E2E matrix and its Gradle Managed Device task) and it must be green
|
||||
before the change is done. CI then runs the full multi-API matrix.
|
||||
compile: preflight runs the top-of-matrix emulator E2E locally — `api35DebugAndroidTest` +
|
||||
`api36DebugAndroidTest` (Gradle Managed Devices) plus the API 37 preview via
|
||||
`api37_e2e.py` (hand-provisioned, mirroring CI's `e2e-preview` job) — and all three must be
|
||||
green before the change is done. CI then runs the full multi-API matrix plus the API 37 preview
|
||||
job.
|
||||
|
||||
## Repo etiquette
|
||||
|
||||
|
||||
+17
-2
@@ -146,8 +146,23 @@ android {
|
||||
// `./gradlew api29DebugAndroidTest`. Gradle provisions/boots/tears down the emulators and
|
||||
// downloads the system images on first use. Keep this list in lockstep with the CI matrix in
|
||||
// .github/workflows/ci.yml; when a new Android ships, add it and drop the oldest level that
|
||||
// has fallen outside ~7 years. API 37 (preview) is exercised on the dev emulator until a
|
||||
// stable managed-device image is published, so it is intentionally not listed here.
|
||||
// has fallen outside ~7 years.
|
||||
//
|
||||
// API 37 (preview) is intentionally NOT listed here (re-confirmed 2026-07, see PR that added
|
||||
// API 35/37 to preflight): its only published system image is the nonstandard
|
||||
// "android-37.0" / google_apis_ps16k pairing that the e2e-preview job in
|
||||
// .github/workflows/ci.yml installs directly via sdkmanager. ManagedVirtualDevice only knows
|
||||
// how to build an "android-<apiLevel:Int>" package id (e.g. `apiLevel = 37` → "android-37")
|
||||
// or an "android-<apiPreview:codename>" one — neither produces "android-37.0" — so there is
|
||||
// no DSL path to this image today, the same root cause documented on e2e-preview for why
|
||||
// reactivecircus/android-emulator-runner can't provision it either. issue #124's perf doc
|
||||
// (docs/perf/issue-124-unified-inbox-paging.md) independently corroborates this: its API 37
|
||||
// cross-check used a physical Pixel, not an emulator/AVD. Locally, preflight covers API 37
|
||||
// by hand-provisioning it with .claude/skills/preflight/api37_e2e.py, which mirrors the
|
||||
// e2e-preview job (same image + emulator flags, except it renders on the host GPU via
|
||||
// `-gpu auto-no-window` locally instead of CI's headless `-gpu swiftshader_indirect`). Once
|
||||
// a managed-device-compatible image is published, add `api37` here (and to the CI matrix),
|
||||
// delete that script, and drop the e2e-preview job.
|
||||
managedDevices {
|
||||
localDevices {
|
||||
listOf(29, 30, 31, 32, 33, 34, 35, 36).forEach { api ->
|
||||
|
||||
Reference in New Issue
Block a user