The API 37 emulator images abort surfaceflinger inside the guest's Gralloc5 mapper,
init SIGKILLs zygote with it, and the framework restarts under the run. run-e2e.sh
and the CI leg disable SystemUI to remove the trigger -- but that removes the IDLE
one, RegionSamplingThread's nav-bar luma sampling. Driving DocumentsUI and rotating
the display are not idle. They are the first things in this suite that generate
surface traffic of their own.
Both tests were measured on android-37.0 under swangle_indirect with SystemUI
disabled and verified quiet, and measured SEPARATELY -- inferring the second from
the first is the mistake docs/api-37-emulator-crash.md opens by correcting. They
fail in the two shapes a framework restart produces:
thePickedInputSurvivesARealRotation
INSTRUMENTATION_ABORTED: System has crashed.
Expected 59 tests, received 50
(5 hasReadColorBufferDma aborts; the framework dies DURING the test, so six
later tests never run and the XML carries a failure with no text at all)
pickingAFileThroughTheSystemPickerFillsInTheFileCard
androidx.test.uiautomator.StaleObjectException
at androidx.test.uiautomator.UiObject2.click(UiObject2.java:526)
(3 aborts; the picker's root node was rebuilt between finding it and tapping it)
Both pass on API 33 and API 36 locally -- whole suite, 59/0/0/2 on each -- which is
the same evidence pattern that made the Media3EngineTest pair the image rather than
the app.
So the class carries @FailsOnEmulatorApi37 and runs on the advisory leg.
THREE PLACES SAID "nothing in this suite touches system UI", and that is what makes
the SystemUI-disable deviation defensible. It is no longer true of the suite, and all
three are corrected rather than left to rot -- the workflow comment, run-e2e.sh's
header, and the doc. The rule they state is being APPLIED, not broken: the thing that
depends on system UI is excluded from the leg that cannot be trusted for it.
Two consequences stated rather than left to be discovered:
- run-e2e.sh applies no annotation filter, unlike CI, so a local `run-e2e.sh 37`
reports these two on top of the Media3 pair AND DOES NOT FINISH. Its totals come
back short and which later tests ran is arbitrary. The summary row now says so;
it previously promised "exactly two failures", which would have read as a
regression in someone else's diff.
- The advisory job is still named "E2E API 37 Media3 hardware transcode", and half
of what it now runs is neither. Renaming a check touches branch protection, so it
is deliberately not done here; the doc records the staleness and the revisit
trigger now says the marker covers two unrelated bugs that can go green apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
625 lines
28 KiB
Bash
Executable File
625 lines
28 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 two failures short of green, on the emulator's
|
|
# own c2.goldfish.h264.decoder rather than on anything this app does. The summary names the two,
|
|
# so a third 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"
|
|
yes | sdkmanager --install "$pkg" > /dev/null 2>&1 || {
|
|
echo " FAILED to install $pkg"
|
|
return 1
|
|
}
|
|
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 that one. Unlike CI, THIS SCRIPT APPLIES NO ANNOTATION FILTER, so
|
|
# a local `run-e2e.sh 37` runs it and reports a third and fourth failure on top of the two
|
|
# Media3EngineTest ones the summary names. Measured 2026-08-24: the rotation test takes the
|
|
# framework down outright (INSTRUMENTATION_ABORTED, and six later tests never run), the picker
|
|
# test dies on a StaleObjectException. CI's gating leg does not see either -- they carry
|
|
# @FailsOnEmulatorApi37 and it filters on notAnnotation.
|
|
#
|
|
# 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`.
|
|
#
|
|
# The framework restart at the end is not optional, and finding that out cost a run. By the
|
|
# time `sys.boot_completed` flips, SystemUI has already registered its region-sampling listener,
|
|
# and `pm disable-user` does not retract a registration that already happened -- it only stops
|
|
# the package being started again. So the first attempt disabled SystemUI, reported success, and
|
|
# then died exactly as before with `Starting 0 tests` and four more aborts. `stop; start` cycles
|
|
# zygote deliberately, and the framework that comes back up does not start SystemUI at all.
|
|
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
|
|
|
|
echo " restarting the framework so the region-sampling listener goes with it"
|
|
emu_adb shell stop > /dev/null 2>&1
|
|
emu_adb shell start > /dev/null 2>&1
|
|
# There is no property worth waiting on here, and an earlier version of this only looked
|
|
# like it was waiting on one: `stop` does not clear sys.boot_completed, so it still reads
|
|
# `1` throughout the restart and any loop over it returns at once. The loop below is the
|
|
# wait -- and it polls the better thing anyway, since `Can't find service: package` is the
|
|
# failure it exists to prevent.
|
|
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; SafPickerRoundTripTest is the gralloc bug reached
|
|
# through system UI, and its rotation test takes the framework down rather than merely
|
|
# failing -- so that level does not finish, and the totals come back SHORT (50 of 59 at the
|
|
# time of writing) with the later tests never run. A run whose totals do not add up is
|
|
# therefore expected here too, which it never was before, and is the reason this says
|
|
# "at least".
|
|
case "$api" in
|
|
37 | 37.*)
|
|
line="$line
|
|
expected here, at least: 2 Media3EngineTest failures on c2.goldfish.h264.decoder, plus
|
|
both SafPickerRoundTripTest tests -- and the run ABORTS partway, so the total is short
|
|
and which later tests ran is arbitrary. Anything else is new.
|
|
CI's gating leg sees only the first two: the SAF class 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"
|