Three commits back I gave `disable_region_sampling` the `adb root` it needed, on the strength of `Must be root` appearing in every API 37 leg's log. That part was right and the conclusion drawn from it was not. api37-debug run 34010167885, with the restart finally real: pm attempt 1: Package com.android.systemui new state: disabled-user restarting the framework adbd is running as root system_server down after 2 s NOT DISABLED after the restart -- the package state did not survive three rounds of it, `final state: SystemUI STILL ENABLED`, and the leg reported `expected: 0, received: 0`. Making the restart work cost the leg every test it had. Bisected locally on android-37.0: a `stop` 2 s after `pm disable-user` kills system_server before PackageManager flushes its delayed write, and a 15 s pause makes the state survive. That repairs the wrong thing. With the package verified disabled before AND after a clean restart, `com.android.systemui` comes up 3 s after `system_server` regardless -- and CI's own logcat says the same with no restart at all: run 34006456986 verifies the package disabled at 02:29:33 and has SystemUI pid 4275 alive from 02:28:52 for the whole run. So `pm disable-user` does not stop SystemUI starting on this image, with or without a restart, and the restart is removed from all three copies rather than repaired. What is kept is the 45-second window with no new aborts, which is what was always doing the work: the boot aborts land at 02:28:18 and 02:28:43 and the wait is what puts instrumentation at 02:32:42, after them rather than inside one. `pm disable-user` is kept too, because every green leg and every number quoted about this row was measured with it applied. The prose the earlier commits got wrong is corrected in place, and one of the corrections is good news: status_check.yml's caveat that this row runs a configuration no other leg or Pixel run uses, so nothing depending on system UI may trust it, describes a state that has never existed. The row is more comparable to API 33-36 than it has been claiming, not less. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
646 lines
30 KiB
Bash
Executable File
646 lines
30 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Runs the instrumented suite on a local emulator, on this workstation, for one or more
|
|
# API levels.
|
|
#
|
|
# Usage: tools/local-emulator/run-e2e.sh [API ...] # default: 33 34 35 36 37
|
|
#
|
|
# API levels are the labels below, not SDK ints: 33-36, plus `37` (= `37.0`) and `37.1`.
|
|
#
|
|
# GPU_MODE= force one renderer on every level; unset means per-API (gpu_for_api)
|
|
# EMULATOR_PORT=5560 console port, so the serial is deterministic
|
|
# BOOT_TIMEOUT=300 seconds to wait for sys.boot_completed
|
|
# KEEP_AVD=1 do not delete an AVD this script created
|
|
#
|
|
# EXIT CODE: 0 only if every level was green; 1 if any level failed, wedged or could not be
|
|
# set up; 2 if it refused to start at all. **A bare `run-e2e.sh` therefore exits 1 by design.**
|
|
# API 37 is in the default list on purpose -- leaving it out is what left the level unlooked-at
|
|
# for as long as it was -- and it is permanently short of green, on the emulator image rather
|
|
# than on anything this app does. Since 2026-08-24 it does not even FINISH: one of its expected
|
|
# failures kills the framework, so the totals come back short with an arbitrary tail. The summary
|
|
# names every failure it expects, so an unnamed one is visibly new, and the last line printed
|
|
# says the same thing. Anything that reads a non-zero exit as breakage should name the levels it
|
|
# wants: `run-e2e.sh 33 34 35 36` is the sweep that can be green.
|
|
# docs/api-37-emulator-crash.md has the measurements.
|
|
#
|
|
# WHY THIS EXISTS, AND WHAT IT DELIBERATELY DOES NOT DO
|
|
#
|
|
# It is a *launcher*, not a second test harness. The diagnostics -- the FAILED-vs-WEDGED
|
|
# split, the SIGQUIT thread dump, the streamed logcat, `|| true` on every probe -- already
|
|
# exist in .github/scripts/e2e-run.sh and are the valuable part. That script assumes only
|
|
# an already-booted emulator and an `adb` that resolves to it, so it runs here unmodified;
|
|
# this file boots the emulator, points adb and Gradle at it, and calls it. Forking it would
|
|
# have produced two copies that drift, and the CI copy is the one exercised every day.
|
|
#
|
|
# Its GitHub-isms (`::group::`, `::error::`) are harmless noise in a local terminal, and
|
|
# RUNNER_TEMP already falls back to /tmp.
|
|
#
|
|
# THE ONE THING THIS HOST NEEDS THAT CI DOES NOT: a renderer that is not SwiftShader's
|
|
# GLES. Fedora's SELinux policy denies `execheap` to unconfined processes
|
|
# (`selinuxuser_execheap` is off), SwiftShader's Reactor JIT emits code onto the heap and
|
|
# mprotects it executable, the mprotect is refused, and the emulator segfaults the moment
|
|
# it calls into the generated routine -- exit 139, every time, before boot completes.
|
|
#
|
|
# That makes CI's own `-gpu swiftshader_indirect` exactly wrong here, and -- less obviously
|
|
# -- so are `auto`, `off` and `guest`, which all resolve to SwiftShader GLES under
|
|
# `-no-window` on this host. The refusal list below is not a style preference; each of
|
|
# those modes was measured crashing. docs/local-emulator.md has the backtrace, the faulting
|
|
# page's RW-without-E segment flags, and the full mode matrix.
|
|
#
|
|
# AND THE ONE THING API 37 NEEDS THAT 33-36 DO NOT: the opposite renderer. On the API 37
|
|
# images the guest's Gralloc5 mapper aborts surfaceflinger from RegionSamplingThread
|
|
# (`Assertion failed: !rcEnc->featureInfo()->hasReadColorBufferDma`). Under `-gpu host` that
|
|
# repeats every few seconds and the device never boots; under ANGLE it fires a handful of
|
|
# times and the boot survives. So `host` is required below 37 and forbidden at 37, which is
|
|
# why the renderer is chosen per level in gpu_for_api rather than set once.
|
|
# docs/api-37-emulator-crash.md has that matrix.
|
|
#
|
|
# THE OTHER LOCAL-ONLY HAZARD: a physical Pixel is usually plugged into this machine, so
|
|
# `adb` is ambiguous in a way it never is on a runner, and an unpinned run would install
|
|
# and execute this suite on the phone. Every path below pins the emulator serial.
|
|
#
|
|
# Gradle is pinned with ANDROID_SERIAL and NOT with the `--serial` task option, which reads
|
|
# like the better tool -- it is documented to fail when the device is missing, where the env
|
|
# var just falls back. It is unusable: `--serial` is broken in AGP 9.3.1, which calls
|
|
# `remove()` on an ImmutableList and dies before it reaches any device.
|
|
#
|
|
# java.lang.UnsupportedOperationException
|
|
# at com.google.common.collect.ImmutableCollection.remove(ImmutableCollection.java:280)
|
|
# at DeviceProviderInstrumentTestTask.getFilteredDevices(...:519)
|
|
#
|
|
# (Measured with two devices attached and one filtered out, which is the case that reaches
|
|
# the `remove()`. A single matching device probably never does -- untested, and irrelevant
|
|
# here, since two devices is exactly the situation that needed pinning.)
|
|
#
|
|
# ANDROID_SERIAL takes an entirely different path -- ConnectedDeviceProvider splits it and
|
|
# keeps devices by `Set.contains(serial)`, building a new list rather than mutating one --
|
|
# so it works. summarise_results below re-checks the device from the report afterwards,
|
|
# since the env var offers no up-front guarantee.
|
|
#
|
|
set -uo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
cd "$REPO_ROOT" || exit 1
|
|
|
|
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
|
|
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
|
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
|
|
|
# Empty means "let each level pick" -- see gpu_for_api. Setting GPU_MODE forces one renderer
|
|
# on every level, which is what you want when measuring a mode, not when running the suite.
|
|
GPU_MODE="${GPU_MODE:-}"
|
|
EMULATOR_PORT="${EMULATOR_PORT:-5560}"
|
|
BOOT_TIMEOUT="${BOOT_TIMEOUT:-300}"
|
|
SERIAL="emulator-${EMULATOR_PORT}"
|
|
APIS=("$@")
|
|
[ "${#APIS[@]}" -eq 0 ] && APIS=(33 34 35 36 37)
|
|
|
|
# Matches CI. `disk-size: 8G` because the FFmpeg libraries do not fit the default userdata
|
|
# partition; `ram-size: 2560M` because the emulator's own floor varies by API level and
|
|
# 2560 is the highest of them, so no level ends up with less than it had. Both are
|
|
# explained at length in status_check.yml -- keep them in step with it.
|
|
DISK_SIZE_BYTES=8589934592
|
|
RAM_SIZE_MB=2560
|
|
|
|
RESULTS_DIR="app/build/outputs/androidTest-results"
|
|
LOG_DIR="${TMPDIR:-/tmp}/lmc-local-e2e"
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
# The two things that outlive a level, declared here rather than where they are first
|
|
# assigned, because the cleanup trap below can fire before either has been reached.
|
|
EMU_PID=""
|
|
CREATED_AVDS=()
|
|
|
|
# ---------------------------------------------------------------- renderer preflight ---
|
|
if [ -n "$GPU_MODE" ]; then
|
|
case "$GPU_MODE" in
|
|
swiftshader_indirect | auto | off | guest)
|
|
echo "REFUSING to launch with -gpu $GPU_MODE."
|
|
echo "On this host that resolves to SwiftShader's GLES, whose JIT is denied execheap by"
|
|
echo "SELinux; the emulator segfaults (exit 139) before boot. See docs/local-emulator.md."
|
|
echo "Working modes: host, angle_indirect, swangle_indirect."
|
|
exit 2
|
|
;;
|
|
host | angle_indirect | swangle_indirect) ;;
|
|
*)
|
|
echo "Unrecognised GPU_MODE '$GPU_MODE'. Known-good: host, angle_indirect, swangle_indirect."
|
|
exit 2
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# A courtesy, not a gate: the boolean being on means SwiftShader would work too, and the
|
|
# refusal list above could be relaxed. It is off on a stock Fedora.
|
|
if command -v getsebool > /dev/null 2>&1; then
|
|
if [ "$(getsebool selinuxuser_execheap 2> /dev/null | awk '{print $3}')" = "on" ]; then
|
|
echo "note: selinuxuser_execheap is ON, so SwiftShader modes would work here too."
|
|
fi
|
|
fi
|
|
|
|
# --------------------------------------------------------------------------- helpers ---
|
|
emu_adb() { adb -s "$SERIAL" "$@"; }
|
|
|
|
# The two probes that turned the original diagnosis around. CI has no use for them -- a
|
|
# runner has neither systemd-coredump nor SELinux in enforcing mode -- but on this host they
|
|
# are the difference between "it failed" and knowing why.
|
|
host_forensics() {
|
|
local since="$1"
|
|
echo "--- host: qemu coredumps since $since ---"
|
|
coredumpctl list --since "$since" --no-pager 2> /dev/null | grep -i qemu || echo " (none)"
|
|
echo "--- host: SELinux denials since $since ---"
|
|
journalctl --since "$since" --no-pager 2> /dev/null | grep -E 'avc: .*denied' | tail -10 || echo " (none)"
|
|
}
|
|
|
|
# The API 37 counterpart of host_forensics. `-gpu host` there aborts surfaceflinger in a loop
|
|
# and the device never boots; the working renderers abort it a few times and survive. Either way
|
|
# the count is the number to look at, and the crash buffer is where it lives -- so print it on
|
|
# every 37 level, not only on the failure path, because a level that passed with 40 aborts is
|
|
# telling you something a level that passed with 1 is not.
|
|
guest_forensics() {
|
|
local api="$1" n
|
|
case "$api" in 37 | 37.*) ;; *) return 0 ;; esac
|
|
n="$(emu_adb logcat -d -b crash 2> /dev/null | grep -c 'hasReadColorBufferDma')"
|
|
echo " surfaceflinger hasReadColorBufferDma aborts: ${n:-?} (docs/api-37-emulator-crash.md)"
|
|
}
|
|
|
|
# API label -> system image. API 33-36 are plain integers with a `google_apis` image. API 37
|
|
# is not: its SDK directories are dotted minor versions (`android-37.0`, `android-37.1`), there
|
|
# is no `android-37`, and from 37.1 onwards Google ships only 16 KB-page (`ps16k`) images for
|
|
# x86_64. `37` is accepted as a spelling of `37.0` because that is what people type.
|
|
image_pkg_for_api() {
|
|
case "$1" in
|
|
37 | 37.0) echo "system-images;android-37.0;google_apis;x86_64" ;;
|
|
37.1) echo "system-images;android-37.1;google_apis_ps16k;x86_64" ;;
|
|
*) echo "system-images;android-$1;google_apis;x86_64" ;;
|
|
esac
|
|
}
|
|
|
|
# The renderer requirement is per-API and the two levels want OPPOSITE things, which is why this
|
|
# is a function and not a constant.
|
|
#
|
|
# 33-36: must NOT be SwiftShader GLES (host-side SELinux/execheap segfault) -- `host` is right.
|
|
# 37.x: must NOT be the host GL translator. With `-gpu host` the guest's Gralloc5 mapper
|
|
# aborts surfaceflinger in a loop and the device never boots; under ANGLE the same
|
|
# assertion fires a handful of times and the boot survives it. Measured, not guessed --
|
|
# docs/api-37-emulator-crash.md has the matrix.
|
|
#
|
|
# `swangle_indirect` rather than `angle_indirect` for 37: both boot, and swangle names its
|
|
# renderer outright instead of resolving through `auto`'s path.
|
|
gpu_for_api() {
|
|
if [ -n "$GPU_MODE" ]; then
|
|
echo "$GPU_MODE"
|
|
return
|
|
fi
|
|
case "$1" in
|
|
37 | 37.*) echo "swangle_indirect" ;;
|
|
*) echo "host" ;;
|
|
esac
|
|
}
|
|
|
|
# `lmc_e2e_api37.0` would be a legal AVD name but an awkward one to type and to grep for.
|
|
# `37` and `37.0` therefore give two AVD names (`lmc_e2e_api37`, `lmc_e2e_api37_0`) for the one
|
|
# image. Harmless -- two AVDs off the same system image cost only disk -- and deliberately not
|
|
# normalised, so that `run-e2e.sh 37 37.0` does not have both levels fight over one AVD.
|
|
avd_for_api() { echo "lmc_e2e_api${1//./_}"; }
|
|
|
|
# Where avdmanager actually put the AVD. `$HOME/.android/avd` is only the default:
|
|
# ANDROID_AVD_HOME, ANDROID_USER_HOME and ANDROID_SDK_HOME each move it, and hardcoding the
|
|
# default meant a machine that sets any of them silently ran every level at stock RAM and
|
|
# userdata size. Rather than encode a precedence that cannot be verified from here, look in
|
|
# every location avdmanager honours and let the existence check pick.
|
|
avd_config_path() {
|
|
local avd="$1" base cfg
|
|
for base in "${ANDROID_AVD_HOME:-}" \
|
|
"${ANDROID_USER_HOME:+$ANDROID_USER_HOME/avd}" \
|
|
"${ANDROID_SDK_HOME:+$ANDROID_SDK_HOME/.android/avd}" \
|
|
"$HOME/.android/avd"; do
|
|
[ -n "$base" ] || continue
|
|
cfg="$base/${avd}.avd/config.ini"
|
|
if [ -f "$cfg" ]; then
|
|
echo "$cfg"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
ensure_avd() {
|
|
local api="$1" avd="$2"
|
|
local pkg
|
|
pkg="$(image_pkg_for_api "$api")"
|
|
local img_dir="$ANDROID_HOME/system-images/${pkg#system-images;}"
|
|
img_dir="${img_dir//;//}"
|
|
|
|
if avdmanager list avd -c 2> /dev/null | grep -qx "$avd"; then
|
|
echo " reusing existing AVD $avd"
|
|
else
|
|
if [ ! -d "$img_dir" ]; then
|
|
echo " installing $pkg"
|
|
# Read sdkmanager's own status, not the pipeline's. `yes` never ends, so the moment
|
|
# sdkmanager exits and closes the pipe, `yes` dies of SIGPIPE with 141 -- and this
|
|
# script runs under `pipefail`, which takes the rightmost NON-ZERO status. A package
|
|
# that installed perfectly therefore reported "FAILED to install".
|
|
#
|
|
# Measured rather than reasoned: under `set -o pipefail`, `yes | true` exits 141 on
|
|
# every run, and `yes | sh -c 'exit 3'` exits 3 -- so the pipeline status cannot tell
|
|
# a clean install from a broken one, while ${PIPESTATUS[1]} reports 0 and 3.
|
|
#
|
|
# The `echo no | avdmanager` below is deliberately NOT changed. One line fits the pipe
|
|
# buffer, so echo has already exited before the close and there is no signal to
|
|
# receive; `echo no | true` measured 0 on every run. Only an unbounded producer is
|
|
# exposed to this.
|
|
yes | sdkmanager --install "$pkg" > /dev/null 2>&1
|
|
if [ "${PIPESTATUS[1]}" -ne 0 ]; then
|
|
echo " FAILED to install $pkg"
|
|
return 1
|
|
fi
|
|
fi
|
|
echo " creating AVD $avd from $pkg"
|
|
echo no | avdmanager create avd -n "$avd" -k "$pkg" -d pixel_6 --force > /dev/null 2>&1 || {
|
|
echo " FAILED to create $avd"
|
|
return 1
|
|
}
|
|
CREATED_AVDS+=("$avd")
|
|
fi
|
|
|
|
# Written into config.ini rather than passed on the command line, which is how
|
|
# reactivecircus/android-emulator-runner applies the same two settings in CI. On the reuse
|
|
# path too, so an AVD left over from an older run gets today's pins.
|
|
#
|
|
# A level that cannot be pinned FAILS rather than running at the defaults. Unpinned, it
|
|
# dies much later with "not enough space", which reads as a device problem -- CI's own
|
|
# history is where that lesson comes from -- and nothing points back to a `sed` that
|
|
# edited a path this script guessed wrong.
|
|
local cfg
|
|
if ! cfg="$(avd_config_path "$avd")"; then
|
|
echo " FAILED: no config.ini for $avd in any directory avdmanager uses"
|
|
echo " (ANDROID_AVD_HOME=${ANDROID_AVD_HOME:-unset}, ANDROID_USER_HOME=${ANDROID_USER_HOME:-unset},"
|
|
echo " ANDROID_SDK_HOME=${ANDROID_SDK_HOME:-unset}, HOME=$HOME)"
|
|
return 1
|
|
fi
|
|
if ! sed -i -e '/^disk\.dataPartition\.size=/d' -e '/^hw\.ramSize=/d' "$cfg"; then
|
|
echo " FAILED to rewrite $cfg"
|
|
return 1
|
|
fi
|
|
if ! printf 'disk.dataPartition.size=%s\nhw.ramSize=%s\n' \
|
|
"$DISK_SIZE_BYTES" "$RAM_SIZE_MB" >> "$cfg"; then
|
|
echo " FAILED to write the RAM/disk pins into $cfg"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
boot_emulator() {
|
|
local avd="$1" api="$2" gpu="$3"
|
|
local boot_log="$LOG_DIR/emulator-api${api}.log"
|
|
|
|
emulator -avd "$avd" -port "$EMULATOR_PORT" \
|
|
-no-window -gpu "$gpu" -noaudio -no-boot-anim -camera-back none -no-snapshot \
|
|
> "$boot_log" 2>&1 &
|
|
EMU_PID=$!
|
|
|
|
local waited=0
|
|
while [ "$waited" -lt "$BOOT_TIMEOUT" ]; do
|
|
sleep 5
|
|
waited=$((waited + 5))
|
|
if ! kill -0 "$EMU_PID" 2> /dev/null; then
|
|
wait "$EMU_PID"
|
|
local rc=$?
|
|
echo " emulator DIED after ${waited}s (exit $rc)"
|
|
[ "$rc" -eq 139 ] && echo " exit 139 is the SwiftShader/execheap segfault -- docs/local-emulator.md"
|
|
echo " emulator log: $boot_log"
|
|
tail -20 "$boot_log"
|
|
return 1
|
|
fi
|
|
if [ "$(emu_adb shell getprop sys.boot_completed 2> /dev/null | tr -d '\r\n')" = "1" ]; then
|
|
echo " booted in ${waited}s"
|
|
return 0
|
|
fi
|
|
done
|
|
echo " emulator NEVER BOOTED within ${BOOT_TIMEOUT}s -- log: $boot_log"
|
|
return 1
|
|
}
|
|
|
|
# API 37 only, and the reason API 37 can be run at all.
|
|
#
|
|
# The abort that breaks these images is reached from SurfaceFlinger's RegionSamplingThread,
|
|
# which exists only because SystemUI registers a nav-bar luma-sampling listener. Each abort
|
|
# kills surfaceflinger, and init responds by SIGKILLing zygote -- so the whole framework
|
|
# restarts underneath the test run, which arrives as `Can't find service: package` and
|
|
# `INSTRUMENTATION_ABORTED: System has crashed`. Under the host GL renderer that repeats
|
|
# forever; under ANGLE it is roughly one every fifteen seconds, which a five-minute suite does
|
|
# not survive either.
|
|
#
|
|
# Removing the listener removes the whole chain. Measured on android-37.0 under
|
|
# swangle_indirect: 10-11 aborts per 150 s idle with SystemUI running, and 0 in 180 s with it
|
|
# disabled, framework services up throughout.
|
|
#
|
|
# THIS IS A DEVIATION, and it is deliberately loud rather than silent. The API 37 leg does not
|
|
# run the same device configuration as API 33-36 or as the Pixel. It is defensible only
|
|
# because nothing in this suite touched SystemUI -- Media3, FFmpeg and WorkManager tests --
|
|
# and because the alternative is no API 37 coverage at all. Anything that ever does depend on
|
|
# system UI must not trust this leg. docs/api-37-emulator-crash.md explains why.
|
|
#
|
|
# "Touched", past tense, since 2026-08-24. SafPickerRoundTripTest drives DocumentsUI and rotates
|
|
# the display, and both reach the gralloc mapper these images abort in -- disabling SystemUI
|
|
# removes the IDLE trigger, not those. Measured per method on android-37.0: the ROTATION test
|
|
# takes the framework down (INSTRUMENTATION_ABORTED) and carries @FailsOnEmulatorApi37; the
|
|
# picker test passes.
|
|
#
|
|
# THIS SCRIPT APPLIES NO ANNOTATION FILTER, unlike CI, so a local `run-e2e.sh 37` runs the
|
|
# rotation test anyway -- and because that test kills the framework rather than merely failing,
|
|
# THE LEVEL DOES NOT FINISH. Its totals come back short and which later tests ran is arbitrary.
|
|
# CI's gating leg never sees it.
|
|
#
|
|
# The retry loop is not defensive padding: at the moment boot_completed flips, the framework
|
|
# may be in one of its restarts and `pm` is simply not published yet. The first attempt at this
|
|
# failed exactly that way, with `cmd: Can't find service: package`.
|
|
#
|
|
# This used to end with a framework restart, described here as "not optional". It was neither
|
|
# optional nor happening -- see the block inside the function. What the first attempt's
|
|
# `Starting 0 tests` and four more aborts actually showed is that a `pm disable-user` on its own
|
|
# buys nothing, which is still true; what was wrong is the conclusion that a restart would.
|
|
disable_region_sampling() {
|
|
local api="$1" out i before after ready
|
|
case "$api" in 37 | 37.*) ;; *) return 0 ;; esac
|
|
|
|
out=""
|
|
for i in $(seq 1 20); do
|
|
out="$(emu_adb shell pm disable-user --user 0 com.android.systemui 2>&1 | tr -d '\r')"
|
|
case "$out" in
|
|
*"new state: disabled"*)
|
|
echo " SystemUI disabled on attempt $i"
|
|
break
|
|
;;
|
|
esac
|
|
out=""
|
|
sleep 5
|
|
done
|
|
if [ -z "$out" ]; then
|
|
echo " WARNING: could not disable SystemUI after 20 attempts."
|
|
echo " Expect INSTRUMENTATION_ABORTED -- docs/api-37-emulator-crash.md"
|
|
return 0
|
|
fi
|
|
|
|
# NO FRAMEWORK RESTART, and the two lines that used to be here are why this comment is long.
|
|
# They were `emu_adb shell stop` and `emu_adb shell start`, both redirected to /dev/null, and
|
|
# both root-only -- so what they printed there was `Must be root` and what they did was nothing,
|
|
# here and in the two CI copies alike. Making them real (2026-09-05) is what established that
|
|
# the disable never worked in the first place: with the package verified `disabled-user` before
|
|
# AND after a clean restart on android-37.0, `com.android.systemui` comes up 3 s after
|
|
# `system_server` regardless, and the same is visible in CI's own logcat. The restart also loses
|
|
# the package state to PackageManager's delayed write if it lands too soon after the `pm` call,
|
|
# which cost api37-debug run 34010167885 every test in the leg.
|
|
#
|
|
# So the useful part of this function is the quiet window below, not the disable. See
|
|
# .github/scripts/e2e-run.sh's header, and docs/api-37-emulator-crash.md.
|
|
ready=0
|
|
for i in $(seq 1 30); do
|
|
if emu_adb shell service check package 2> /dev/null | grep -q ': found' \
|
|
&& emu_adb shell service check activity 2> /dev/null | grep -q ': found'; then
|
|
ready=1
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
if [ "$ready" -ne 1 ]; then
|
|
echo " WARNING: package and activity services still absent 150 s after the restart."
|
|
echo " Expect INSTRUMENTATION_ABORTED -- docs/api-37-emulator-crash.md"
|
|
fi
|
|
|
|
# Prove it worked rather than assume it. Zero new aborts over this window is what makes the
|
|
# difference between a run that completes and one that reports `Starting 0 tests`.
|
|
before="$(emu_adb logcat -d -b crash 2> /dev/null | grep -c 'hasReadColorBufferDma')"
|
|
emu_adb shell 'sleep 45' > /dev/null 2>&1
|
|
after="$(emu_adb logcat -d -b crash 2> /dev/null | grep -c 'hasReadColorBufferDma')"
|
|
echo " quiet check: $((after - before)) new surfaceflinger aborts in 45 s (want 0)"
|
|
if [ "$((after - before))" -ne 0 ]; then
|
|
echo " WARNING: region sampling is still live; the run may not survive."
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# CI gets this from the action's `disable-animations: true`.
|
|
disable_animations() {
|
|
local s
|
|
for s in window_animation_scale transition_animation_scale animator_duration_scale; do
|
|
emu_adb shell settings put global "$s" 0.0 > /dev/null 2>&1 || true
|
|
done
|
|
}
|
|
|
|
# `${EMU_PID:-0}` used to guard these three calls, and it guarded the wrong thing: EMU_PID
|
|
# is *empty*, not unset, if the background launch never produced a job, and `kill` reads pid
|
|
# 0 as "the sender's whole process group" -- this script and, on a terminal, everything else
|
|
# in the foreground group with it. The `kill -0` wait loop had the same shape and would have
|
|
# spent its full grace period testing the group. Nothing to stop is now a return, never a
|
|
# guess. (boot_emulator's own `kill -0 "$EMU_PID"` is unguarded and cannot reach that form:
|
|
# it runs only after the assignment.)
|
|
#
|
|
# max_wait is a parameter so the interrupt path need not sit through the full grace period.
|
|
stop_emulator() {
|
|
local max_wait="${1:-30}" waited=0
|
|
[ -n "${EMU_PID:-}" ] || return 0
|
|
emu_adb emu kill > /dev/null 2>&1
|
|
while kill -0 "$EMU_PID" 2> /dev/null && [ "$waited" -lt "$max_wait" ]; do
|
|
sleep 2
|
|
waited=$((waited + 2))
|
|
done
|
|
kill -9 "$EMU_PID" 2> /dev/null
|
|
wait "$EMU_PID" 2> /dev/null
|
|
EMU_PID=""
|
|
}
|
|
|
|
delete_created_avds() {
|
|
local avd
|
|
[ "${KEEP_AVD:-0}" = "1" ] && return 0
|
|
for avd in ${CREATED_AVDS[@]+"${CREATED_AVDS[@]}"}; do
|
|
# A SIGKILLed emulator does not get to remove its own lock files, and avdmanager can
|
|
# refuse over them. Staying silent there would leak the very thing this exists to clean.
|
|
avdmanager delete avd -n "$avd" > /dev/null 2>&1 \
|
|
|| echo " WARNING: could not delete AVD $avd -- 'avdmanager delete avd -n $avd' by hand"
|
|
done
|
|
CREATED_AVDS=()
|
|
}
|
|
|
|
# What an interrupted sweep used to leave behind: a headless emulator holding console port
|
|
# $EMULATOR_PORT, and an lmc_e2e_apiNN AVD. The next run's `emulator -port` then collides
|
|
# with the orphan, and `emu_adb` can resolve to it -- on a workstation that also has the
|
|
# Pixel plugged in, exactly the ambiguity the ANDROID_SERIAL pinning exists to prevent. A
|
|
# sweep is up to five boots long, so the window for one Ctrl-C is not small.
|
|
#
|
|
# Idempotent, and called explicitly on the normal path so its output cannot land after the
|
|
# summary; the EXIT trap then finds nothing left to do. The emulator logs are deliberately
|
|
# NOT removed -- they live in $LOG_DIR and are the only evidence a failed boot leaves.
|
|
CLEANED=0
|
|
cleanup() {
|
|
[ "$CLEANED" = "1" ] && return 0
|
|
CLEANED=1
|
|
stop_emulator "${1:-30}"
|
|
delete_created_avds
|
|
}
|
|
|
|
# 6 s, not 30: Ctrl-C has already reached the emulator through the foreground process group,
|
|
# so this is only waiting for it to finish writing, and `kill -9` follows regardless. The
|
|
# EXIT trap is disarmed before exiting so the status below is the one that survives.
|
|
#
|
|
# bash runs a trap only between commands, so this starts when whatever was in the foreground
|
|
# returns -- which for Ctrl-C is immediately, because the same interrupt reached that command
|
|
# too. `kill -INT` aimed at this script alone waits for the foreground command to finish.
|
|
# Invoked indirectly -- installed as the INT and TERM trap a few lines below. Both codes,
|
|
# because shellcheck 0.9.0 reports this as unreachable commands (SC2317) and 0.11.0 as an
|
|
# uninvoked function (SC2329); CI pins 0.11.0 but a local install may be either.
|
|
# shellcheck disable=SC2317,SC2329
|
|
on_signal() {
|
|
echo
|
|
echo "interrupted (SIG$1) -- stopping the emulator and removing the AVDs this run created"
|
|
echo " emulator logs kept in $LOG_DIR"
|
|
cleanup 6
|
|
trap - EXIT
|
|
exit "$2"
|
|
}
|
|
|
|
trap 'on_signal INT 130' INT
|
|
trap 'on_signal TERM 143' TERM
|
|
trap cleanup EXIT
|
|
|
|
# The XML is authoritative. The console counter double-counts skips, so a run that reports
|
|
# "42 tests" on stdout can be 40 in the report.
|
|
#
|
|
# It also names the device it ran on, in the file name and in the suite's `hostname`. That is
|
|
# printed rather than just counted, because it is the only after-the-fact proof that this
|
|
# level ran fresh and ran on the emulator -- see the ANDROID_SERIAL note in the header.
|
|
summarise_results() {
|
|
local api="$1"
|
|
python3 - "$api" "$RESULTS_DIR" << 'PY'
|
|
import glob, os, sys, xml.etree.ElementTree as ET
|
|
api, results_dir = sys.argv[1], sys.argv[2]
|
|
t = f = e = s = 0
|
|
devices = set()
|
|
files = sorted(glob.glob(os.path.join(results_dir, "**", "TEST-*.xml"), recursive=True))
|
|
for path in files:
|
|
try:
|
|
root = ET.parse(path).getroot()
|
|
except ET.ParseError:
|
|
continue
|
|
suites = [root] if root.tag == "testsuite" else list(root.iter("testsuite"))
|
|
for suite in suites:
|
|
t += int(suite.get("tests", 0)); f += int(suite.get("failures", 0))
|
|
e += int(suite.get("errors", 0)); s += int(suite.get("skipped", 0))
|
|
if suite.get("hostname"):
|
|
devices.add(suite.get("hostname"))
|
|
devices.add(os.path.basename(path))
|
|
if not files:
|
|
print(f"API {api}: no result XML found under {results_dir}")
|
|
else:
|
|
print(f"API {api}: tests={t} failures={f} errors={e} skipped={s}")
|
|
for d in sorted(devices):
|
|
print(f" ran on/report: {d}")
|
|
PY
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------ main ---
|
|
SUMMARY=()
|
|
overall=0
|
|
|
|
# Whether the red exit is the expected one depends on which level produced it, and only the
|
|
# loop knows that -- so it is recorded where `overall` is set rather than guessed from the
|
|
# summary afterwards. A note at the end claiming a genuine API 34 failure was "by design"
|
|
# would be the same defect it is there to prevent, one layer up.
|
|
NON37_RED=0
|
|
mark_red() {
|
|
overall=1
|
|
case "$1" in 37 | 37.*) ;; *) NON37_RED=1 ;; esac
|
|
}
|
|
|
|
for api in "${APIS[@]}"; do
|
|
avd="$(avd_for_api "$api")"
|
|
gpu="$(gpu_for_api "$api")"
|
|
started="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo "=============================================================="
|
|
echo "API $api (avd=$avd gpu=$gpu serial=$SERIAL)"
|
|
echo " image: $(image_pkg_for_api "$api")"
|
|
echo "=============================================================="
|
|
|
|
if ! ensure_avd "$api" "$avd"; then
|
|
SUMMARY+=("API $api: AVD SETUP FAILED")
|
|
mark_red "$api"
|
|
continue
|
|
fi
|
|
|
|
if ! boot_emulator "$avd" "$api" "$gpu"; then
|
|
host_forensics "$started"
|
|
guest_forensics "$api"
|
|
SUMMARY+=("API $api: BOOT FAILED")
|
|
mark_red "$api"
|
|
stop_emulator
|
|
continue
|
|
fi
|
|
|
|
disable_region_sampling "$api"
|
|
disable_animations
|
|
rm -rf "$RESULTS_DIR"
|
|
|
|
# ANDROID_SERIAL steers both e2e-run.sh's own bare `adb` calls and Gradle's device choice.
|
|
# --rerun because each level must actually re-execute: without it a task Gradle considers
|
|
# up-to-date would leave the previous level's XML in place, and every row of the summary
|
|
# would report the same numbers.
|
|
export ANDROID_SERIAL="$SERIAL"
|
|
export E2E_EXTRA_GRADLE_ARGS="--rerun"
|
|
bash .github/scripts/e2e-run.sh "$api"
|
|
rc=$?
|
|
unset ANDROID_SERIAL E2E_EXTRA_GRADLE_ARGS
|
|
|
|
line="$(summarise_results "$api")"
|
|
guest_forensics "$api"
|
|
# API 37 is in the default list on purpose, and it is expected to be red. Leaving it out would
|
|
# put the level back where this whole exercise found it -- untested and unlooked-at -- but a
|
|
# summary that just says "N failures" with no explanation trains people to ignore the exit
|
|
# code. So the row NAMES the expected ones, and anything else is then obviously new.
|
|
#
|
|
# The list grew on 2026-08-24 and the shape of the row changed with it. The two
|
|
# Media3EngineTest failures are a codec; the third is the gralloc bug reached through system
|
|
# UI, and it takes the framework DOWN rather than merely failing -- so the level does not
|
|
# finish, and the totals come back SHORT (50 of 59 when this was written) with the later
|
|
# tests never run. A run whose totals do not add up is expected here now, which it never
|
|
# was before.
|
|
case "$api" in
|
|
37 | 37.*)
|
|
line="$line
|
|
expected here: 2 Media3EngineTest failures on c2.goldfish.h264.decoder, plus
|
|
SafPickerRoundTripTest.thePickedInputSurvivesARealRotation -- which kills the framework
|
|
rather than merely failing, so the run ABORTS partway and the total comes back SHORT with
|
|
an arbitrary tail. That is expected here too, and never was before. Anything else is new.
|
|
CI's gating leg sees only the first two: the rotation test carries @FailsOnEmulatorApi37
|
|
and this script, unlike CI, applies no annotation filter.
|
|
docs/api-37-emulator-crash.md"
|
|
;;
|
|
esac
|
|
if [ "$rc" -ne 0 ]; then
|
|
line="$line [gradle exit $rc]"
|
|
mark_red "$api"
|
|
host_forensics "$started"
|
|
fi
|
|
SUMMARY+=("$line")
|
|
stop_emulator
|
|
done
|
|
|
|
cleanup
|
|
|
|
echo
|
|
echo "===================== LOCAL E2E SUMMARY ======================"
|
|
printf '%s\n' ${SUMMARY[@]+"${SUMMARY[@]}"}
|
|
echo "=============================================================="
|
|
|
|
# An unexplained red exit trains people to stop reading exit codes, and this one is expected
|
|
# whenever API 37 is in the sweep -- which the default list makes the common case. Said here
|
|
# rather than only in the docs, because this is where it is actually read. Only when 37.x is
|
|
# the ONLY thing that went red: a note calling a real failure elsewhere "by design" would be
|
|
# worse than no note at all.
|
|
if [ "$overall" -ne 0 ] && [ "$NON37_RED" -eq 0 ]; then
|
|
echo "note: the only level that went red is API 37, which exits non-zero by design -- it is"
|
|
echo " permanently short of green, and since 2026-08-24 it does not even finish. Confirm"
|
|
echo " its row above names every failure it shows; docs/api-37-emulator-crash.md says why"
|
|
echo " each of them is the image rather than this app."
|
|
fi
|
|
|
|
exit "$overall"
|