Compare commits

...
Author SHA1 Message Date
JMR-devandClaude Opus 5 e7c3e5688f Stop the match line claiming a failure count nobody measured
This PR's own advisory leg caught it. With five markers and a truncated run it
printed

  failed:            4
  ...
  baseline: matches (5 expected, 5 failed)

three lines apart. The match line has always printed the baseline twice, which
was true while `failed` had to equal it to get there -- and the previous commit
removed that requirement for truncated runs without noticing this line depended
on it.

So the truncated spelling says what happened: `matches (5 expected; 4 of 5
failed, on a run the abort truncated -- not compared)`. Pinned by a third case
beside the two from that commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 09:03:21 -05:00
Jason Ross b3d4318273 Merge pull request #245 from JMR-dev/fix/api37-task-snapshot-crash
Take the picker test off the API 37 gating leg, and stop the SystemUI disable pretending
2026-09-06 09:01:56 -05:00
JMR-devandClaude Opus 5 07f7ed4259 Merge #221, and make the two counts derived rather than remembered
#221 landed while this was in review, adding one instrumented test: 68 -> 69, and
64 on the gating leg. Its own commit was "Move CLAUDE.md's instrumented counts
with the test that changes them", and it still arrived stale -- it says three
markers and 61 tests, both true of the main it was branched from and neither true
of the main it merged into.

That is the third time these two numbers have gone stale in a day, so the
paragraph now says where they come from: a grep for @Test over app/src/androidTest
minus the marker count, cross-checkable against any run's shape, since a leg below
37 reports the first as `expected` and the API 37 gating leg reports the second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 08:53:59 -05:00
Jason Ross dc6ee3dc9b Merge pull request #221 from JMR-dev/test/join-failure-message-on-device
Assert the join failure message against a real FFmpeg session
2026-09-06 08:50:59 -05:00
JMR-devandClaude Opus 5 7fd95ddede Merge main, and re-derive every count it moved
main landed 25 commits while this branch was open, including a third
@FailsOnEmulatorApi37 on Media3EngineTest.cancellingARunningExportStopsIt and a
batch of new instrumented tests. Every number this branch touches moved with them.

Re-derived rather than adjusted, and cross-checked against run 34020234606: the
API 34 leg (no filter) reports 68 tests and the API 37 gating leg 64, which is
68 minus main's four markers. With the picker test marked that is five markers,
baseline 5, and 63 on the gating leg.

The conflict in FailsOnEmulatorApi37.kt is resolved main's way: it had replaced
the hardcoded "grows by two" with a reference to the constant, which is the same
drift this file exists to prevent and a better fix than the number I put there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 08:49:59 -05:00
Jason Ross 350b179c9e Merge branch 'main' into test/join-failure-message-on-device 2026-09-06 08:43:20 -05:00
Jason Ross 0cc4c4f3a3 Merge pull request #244 from JMR-dev/docs/e2e-read-findings-e7
Record what working the e2e tickets found
2026-09-06 03:32:19 -05:00
JMR-devandClaude Opus 5 557b3edab4 Compare the advisory failure count only on a run that finished
The verification dispatch of the reworked harness (34011072884) came back
4 expected / 3 received / 3 failed, where the one before it (34008889182) had
been 4/4/4 on the identical configuration. Nothing about the test list changed
between them: the abort landed one test earlier and the picker test never
started.

The baseline check would have called that "one now passes", which is the wrong
reading and the kind of notice #120 is about -- a deviation that is wrong often
enough to teach everyone to skim past deviation notices. So `failed` is compared
only when `completed cleanly` is yes, and `expected` is compared always, because
`Starting N tests` is printed before anything can abort and is what actually
answers "is the marked set the size the baseline says".

Two cases in e2e-report-shape-test.sh, as a pair: a truncated run short by one is
not a deviation, and a CLEAN run short by one still is -- so the first cannot have
bought its quiet by disabling the check.

This is a consequence of adding the fourth marker rather than a pre-existing bug
worth its own ticket: with three, the advisory leg had been completing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 23:26:56 -05:00
JMR-devandClaude Opus 5 97558c259f The root fix was wrong, and this is what it found: the disable does nothing
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>
2026-09-05 23:15:57 -05:00
JMR-devandClaude Opus 5 745c4f62ce The local runner had the same missing root, and hid it in /dev/null
Third copy of the same defect. tools/local-emulator/run-e2e.sh sends `adb shell
stop` and `start` to /dev/null, so its `Must be root` was never printed and the
framework restart it credits has never happened either.

That matters for what docs/api-37-emulator-crash.md's abort numbers are evidence
of, so the caveat goes next to them rather than in a commit message: on API 37
the image restarts its own framework every minute or so, and a restart landing
after a successful `pm disable-user` brings back a SystemUI-less zygote on its
own. That produces the recorded rate collapse by accident, and it is why the same
code bought nothing on CI's much quieter swiftshader legs, where the logcat shows
SystemUI alive for the whole run.

Also verified, because the previous commit asserted it: the advisory leg really
does run thePickedInputSurvivesARealRotation before the picker test -- run
34008889182 logs the four in the order Media3, Media3, rotation, picker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 22:58:10 -05:00
JMR-devandClaude Opus 5 e2f8ef2918 Cite the two measurements the last two commits asserted
The advisory-leg count (4 expected, 4 received, 4 failed with the new marker) is
api37-debug run 34008889182, dispatched with the annotation as its filter. The
force-stop recovery was made to go red before it was believed: on a local API 36
emulator, with the picker left open and the back presses removed, the test passes
with forceStopThePicker() and fails with exactly the API 37 message without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 22:55:42 -05:00
JMR-devandClaude Opus 5 393b931fff Give api37-debug's own SystemUI disable the same root, and say why there are two
The debug workflow's header says it does not fork e2e-run.sh, and it does not --
but it drives the SystemUI disable from its own probe step, so `disable_system_ui`
can be turned off for a dispatch. That is a second copy of the same logic, and run
34008889182 showed it carrying the same defect the real leg had: `Must be root`
twice, and `system_server down after 40 s` printed for a stop that did nothing.

Same fix, and a header note so the next person changing one knows to change both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 22:54:27 -05:00
JMR-devandClaude Opus 5 d759ef32f1 Take the picker test off the API 37 gating leg, and give the SystemUI disable root
The gating `E2E API 37` leg failed three of the last ten status_check runs. Four
runs were read logcat-first -- 34006456986, 34001744574, 34001377499 and the
green 34002313300 -- and each carries exactly two `hasReadColorBufferDma` aborts
before the suite (surfaceflinger, during boot and the SystemUI disable) and
exactly one during it: `system_server`, thread `TaskSnapshotPer`, always inside
`pickingAFileThroughTheSystemPickerFillsInTheFileCard`'s window. Nothing else in
the gating set reaches the mapper.

So that test kills the framework on this image whether it passes or not, and
whether the leg goes red is luck: 34001377499 passed it and lost the leg anyway
with `failed: 0`, 34002313300 passed it 0.6 s after the abort and went green.
That is #108. The test now carries `@FailsOnEmulatorApi37` and the baseline goes
3 -> 4; the marker's own wording widens from "does not pass on this image" to
"cannot be run on this image", because this carrier passes about half the time.

`docs/api-37-emulator-crash.md` had counted those aborts on 2026-08-24, put them
in its table, and then read the pass/fail column alone. The correction is
recorded beside the original rather than replacing it. Probed on the same image
and recorded there too: there is no shell knob for task snapshots -- not in
`getprop`, `settings`, `device_config` or `cmd window` -- so the marker is the
available answer rather than the lazy one.

Two separate defects came out of the same logcats.

`disable_region_sampling` has never restarted the framework on CI. `adb shell
stop` and `start` are root-only and every API 37 leg has printed `Must be root`
for both, so SystemUI stayed up for the whole run -- visible directly as
`WindowManagerShell ... app=com.android.systemui` minutes after "final state:
SystemUI disabled". Both waits also printed their own exhaustion as an elapsed
time, so "system_server down after ~40 s" is what a stop that did nothing looks
like. Measured on the local android-37.0 AVD, same fingerprint as CI: `adb root`
makes `stop` return 0 with `pidof system_server` empty. Root is dropped again
before Gradle runs, and both waits now say whether they observed anything.

And when the picker test does fail, the abort is the coda rather than the cause:
`InputDispatcher: No new touched window at (539.0, 525.0)` is in both reds and
absent from the green, so the tap on the root is discarded, the picker is never
navigated, and all four back presses land on an activity WindowManager says has
not added a window yet. `forceStopThePicker` goes around input entirely so
`pickTheFixture`'s whole-picker retry -- which exists for exactly this -- becomes
reachable. That one is a fix on every API level, not just 37.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 22:23:33 -05:00
JMR-devandClaude Opus 5 4d090d9a81 Move CLAUDE.md's instrumented counts with the test that changes them
The suite goes 60 -> 61 and the gating leg 57 -> 58, because the new join-failure
test carries no @FailsOnEmulatorApi37 and so runs on every level including 37.

FAILS_ON_EMULATOR_API37_BASELINE stays at 3, checked rather than assumed:
e2e-report-shape.sh counts lines matching '^[[:space:]]*@FailsOnEmulatorApi37',
which is three annotations -- the fourth occurrence in the tree is the KDoc at
Media3EngineTest.kt:195 saying a test is deliberately NOT marked, and the anchor
excludes it. Nothing here adds a marker.

Carried by this PR rather than the docs one so the file never states a count main
does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 21:25:44 -05:00
JMR-devandClaude Opus 5 39327beea7 Assert the join failure message against a real FFmpeg session
#217 closed by noting the join legs had not been run locally. Running them would
not have answered it: nothing on either source set drove a real join *failure*,
so the message unified in #203 was asserted only against values a JVM test hands
to sessionOutcome directly. CI had already run those legs green on the merged
commit, so the outstanding item was a gap in coverage rather than a gap in
execution.

The new case forces a failure with an input that does not exist -- rejected the
same way by every FFmpeg build, unlike malformed media -- and asserts the message
names the operation, carries the return code, and has detail after it.

That last clause is the device-only half. getReturnCode, getFailStackTrace and
getAllLogsAsString are native reads; if the log tail came back empty on a device
the user would see "Joining failed (1): " with nothing after the colon, and every
JVM test would still pass.

It deliberately does not pin which detail source wins. On an ordinary non-zero
return code FFmpegKit reports no fail stack trace, so stack-trace-first and
log-tail-first produce identical text and no assertion here could tell them
apart. SessionOutcomeTest pins that, where both sources can be non-blank at once.
Claiming it here would be a KDoc asserting more than the test checks.

Measured on the local API 33 emulator: 61/61 green with the test, and with both
detail sources nulled in ConcatEngine it fails on the intended assertion --
"the message stopped at the return code and told the user nothing, was:
'Joining failed (1): '" -- so the prefix and return code survive the mutation and
only the device-only claim goes red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 21:25:05 -05:00
11 changed files with 564 additions and 148 deletions
+40
View File
@@ -184,6 +184,46 @@ out="$(run_report "$root")"
assert_contains "same-line annotation removed: counts 2, so it was worth 1" "$out" \
" baseline DEVIATION: the tree carries 2 tests marked \`@FailsOnEmulatorApi37\` but the baseline says 3 — update FAILS_ON_EMULATOR_API37_BASELINE"
# ---------------------------------------------------------------------------
# 4. A run the abort truncated, with fewer failures than the baseline: NOT a deviation.
#
# `expected` comes from `Starting N tests`, printed before anything can abort, so it still
# answers "is the marked set the size the baseline says". `failed` is a tally of what actually
# ran, and on a truncated run the tests after the abort never start. Measured on 2026-09-05, two
# api37-debug dispatches of the same four marked tests: 4/4/4 and then 4/3/3. Announcing the
# second as "one now passes" is the wrong reading, and #120 is the standing lesson about a notice
# that is wrong often enough to be skimmed past.
# ---------------------------------------------------------------------------
root="$(make_root "$FIXTURE_DIR" 3)"
cat > "$root/gradle.log" <<'TRUNCATED'
> Task :app:connectedDebugAndroidTest
Starting 3 tests on test(AVD) - 16
There was 2 failure(s).
Test run failed to complete. Expected 3 tests, received 2. onError: commandError=false message=INSTRUMENTATION_ABORTED: System has crashed.
TRUNCATED
out="$(run_report "$root")"
assert_contains "truncated run: the truncation is reported" "$out" ' completed cleanly: no'
assert_absent "truncated run: the short failure count is not a deviation" "$out" 'tests failed, the baseline is'
# And the match line has to say what actually happened rather than repeat the baseline: PR #245's
# advisory leg printed `failed: 4` three lines above `matches (5 expected, 5 failed)`.
assert_contains "truncated run: the match line does not claim the baseline's failure count" "$out" \
' baseline: matches (3 expected; 2 of 3 failed, on a run the abort truncated — not compared)'
# ---------------------------------------------------------------------------
# 5. The same short failure count on a run that finished IS a deviation.
#
# The pair is the point: case 4 must not have bought its quiet by disabling the check outright.
# ---------------------------------------------------------------------------
root="$(make_root "$FIXTURE_DIR" 3)"
cat > "$root/gradle.log" <<'CLEAN'
> Task :app:connectedDebugAndroidTest
Starting 3 tests on test(AVD) - 16
There was 2 failure(s).
CLEAN
out="$(run_report "$root")"
assert_contains "clean run, short by one: the deviation fires" "$out" \
'2 tests failed, the baseline is 3'
echo
if [ "$failures" -eq 0 ]; then
echo "e2e-report-shape-test.sh: all checks passed"
+25 -2
View File
@@ -252,8 +252,22 @@ if [ -n "$baseline" ]; then
if [ "$expected" != "unknown" ] && [ "$expected" != "$baseline" ]; then
deviations+=("the runner started $expected tests, the baseline is $baseline")
fi
# `expected` is compared on every run and `failed` only on a run that finished, and the
# difference is the truncation this file already records rather than compares. `expected`
# comes from `Starting N tests`, which is printed before anything can abort, so it answers
# "is the marked set the size the baseline says" whatever happens afterwards. `failed` is a
# tally of what actually ran: on a truncated run the tests after the abort never start, so
# comparing it to the baseline announces a deviation about the framework dying rather than
# about the test list. Measured on 2026-09-05, two api37-debug dispatches of the same four
# marked tests: 4/4/4 and then 4/3/3, the second having lost the last test to the abort.
# Announcing that as "one now passes" is exactly the wrong reading, and #120 is the standing
# lesson about a notice that is wrong often enough to be skimmed past.
if [ "$failed" != "unknown" ] && [ "$failed" != "$baseline" ]; then
deviations+=("$failed tests failed, the baseline is $baseline — every test carrying the marker is expected to fail on this image, so fewer means one now passes and more means a new one joined")
if [ "$completed" = "**no**" ]; then
echo "::debug::$failed of $baseline marked tests failed, on a run the abort truncated — not compared"
else
deviations+=("$failed tests failed, the baseline is $baseline — every test carrying the marker is expected to fail on this image, so fewer means one now passes and more means a new one joined")
fi
fi
fi
if [ -n "$marked" ] && [ "$marked" != "$baseline" ]; then
@@ -283,7 +297,16 @@ if [ -n "$failed_names" ]; then
fi
if [ "$advisory" = "yes" ]; then
if [ "${#deviations[@]}" -eq 0 ]; then
echo " baseline: matches ($baseline expected, $baseline failed)"
# Two spellings, because one of them would be a lie half the time. `$baseline expected,
# $baseline failed` is only true of a run that finished; on a truncated one `failed` is a
# tally of the tests that got to run before the framework died, and printing the baseline in
# its place claims a number nobody measured. Seen on PR #245's advisory leg, which reported
# `failed: 4` three lines above `matches (5 expected, 5 failed)`.
if [ "$failed" != "unknown" ] && [ "$failed" != "$baseline" ]; then
echo " baseline: matches ($baseline expected; $failed of $baseline failed, on a run the abort truncated — not compared)"
else
echo " baseline: matches ($baseline expected, $baseline failed)"
fi
else
printf ' baseline DEVIATION: %s\n' "${deviations[@]}"
fi
+66 -50
View File
@@ -52,40 +52,72 @@ WEDGE_TIMEOUT=1200
# the same shape as E2E_EXTRA_GRADLE_ARGS below. The other four E2E legs run byte-identical
# commands with it unset.
#
# WHY IT RUNS HERE, BEFORE THE LOGCAT STREAM: `adb shell stop` ends the `adb logcat` started
# below, and nothing restarts it, so a disable performed after that point would cost this leg
# its whole diagnostic story for the part of the run that matters. Everything this function
# counts comes from `adb logcat -d -b crash`, which is a fresh read each time and independent
# of the stream.
# WHY IT RUNS HERE, BEFORE THE LOGCAT STREAM: it is a 45-second wait, and the stream below is
# meant to cover the suite rather than the wait. Everything this function counts comes from
# `adb logcat -d -b crash`, a fresh read each time and independent of the stream. (The original
# reason was stronger and no longer applies: `adb shell stop` would have ended the streamed
# `adb logcat` and nothing restarts it. There is no `stop` here any more -- see below.)
#
# WHAT IT IS FOR: the android-37.x images abort surfaceflinger from RegionSamplingThread inside
# their own gralloc mapper (docs/api-37-emulator-crash.md). surfaceflinger is a critical service,
# so init SIGKILLs zygote with it and the framework restarts under the run -- Gradle then reports
# WHAT IT IS FOR -- AND THE NAME IS NOW WRONG, WHICH IS WHY THIS PARAGRAPH IS LONG.
# The android-37.x images abort surfaceflinger from RegionSamplingThread inside their own gralloc
# mapper (docs/api-37-emulator-crash.md). surfaceflinger is a critical service, so init SIGKILLs
# zygote with it and the framework restarts under the run -- Gradle then reports
# `cmd: Can't find service: package` and `Starting 0 tests`. RegionSamplingThread exists only
# because SystemUI registers a nav-bar luma-sampling listener, so removing the package removes
# the whole chain. Measured cadence of those kills: 20-90 s apart, median 60-70 s, three to five
# in a four-minute window -- fast enough that install and instrumentation start-up do not fit
# inside one gap.
# because SystemUI registers a nav-bar luma-sampling listener, so this was written to remove the
# package and with it the whole chain. Measured cadence of those kills on `-gpu host`: 20-90 s
# apart, median 60-70 s, three to five in a four-minute window.
#
# **THE DISABLE HALF OF THAT HAS NEVER WORKED, AND THE QUIET WINDOW IS WHAT THE LEG ACTUALLY
# GETS.** Measured 2026-09-05, two ways that agree:
#
# - On CI, in the gating leg of run 34006456986: `pm disable-user` is accepted at 02:28:37.9 and
# `com.android.systemui` really is in `pm list packages -d` at 02:29:33 -- and SystemUI is
# started anyway at 02:28:39.5 and again at 02:28:52.3, the second of which (pid 4275) is
# alive for the whole instrumentation run, logging `WindowManagerShell ...
# app=com.android.systemui` minutes after this function prints its final line.
# - Locally on android-37.0, with the package verified disabled before AND after a deliberate
# `stop; start`: `com.android.systemui` comes up 3 s after `system_server` regardless.
#
# So `pm disable-user --user 0 com.android.systemui` does not stop SystemUI starting on this
# image, whatever else happens. The name `E2E_DISABLE_SYSTEM_UI` and the name of this function are
# kept because the matrix row, both workflows and two documents refer to them, and a rename would
# touch all of that to no benefit -- read this comment, not the name.
#
# WHAT IS LEFT IS LOAD-BEARING, so do not delete the function as dead weight. It is the 45-second
# window with zero new `hasReadColorBufferDma` aborts. The boot-time aborts land close together --
# 02:28:18 and 02:28:43 in that same run -- and the wait is what puts instrumentation (02:32:42)
# after them rather than inside one. That is what stops a leg reporting `Starting 0 tests`, and it
# is why the three-round retry stays.
#
# THE `pm disable-user` CALL STAYS TOO, for a narrower reason than it was written for: every green
# leg and every measurement quoted anywhere about this row was taken with it applied and SystemUI
# running. Removing it would change the configuration the numbers came from, which is not a change
# to make while fixing a flake.
#
# AND THE FRAMEWORK RESTART IS GONE, having been measured to be worse than nothing. It was written
# as `adb shell stop; adb shell start`, which are root-only; adbd is not root, so every leg printed
# `Must be root` twice and restarted nothing. Adding `adb root` made it real, and api37-debug run
# 34010167885 is what that looks like: `pm disable-user` reports success, the stop lands ~2 s later
# and kills system_server before PackageManager has flushed its delayed write of package
# restrictions, so the state is gone on the way back up -- `NOT DISABLED after the restart`, three
# rounds, `final state: SystemUI STILL ENABLED`, and the leg then reported `expected: 0,
# received: 0`. A 15 s pause before the stop does make the state survive (bisected locally), and it
# still does not help, because of the two measurements above. So the restart is removed rather than
# repaired: it cost the leg every test it had, and there is nothing for it to buy.
#
# NOTHING HERE TRUSTS A COMMAND'S OWN REPORT, and that is not paranoia: of four runs of an
# earlier one-shot version, one (32646029143) reported `new state: disabled-user` and then
# started SystemUI eight more times, with ten more aborts. `pm disable-user` can be accepted by
# a system_server that is SIGKILLed before the state is written, and `pm disable-user` does not
# retract SystemUI's existing region-sampling registration either -- by the time boot completes
# it has already registered, so only a framework restart brings back a SystemUI-less
# surfaceflinger. Hence: disable, take the framework DOWN and confirm system_server is really
# gone (an earlier probe asked `service check` 0.3 s after `stop` and got `found` from the
# system_server that was still exiting, so its wait was not a wait), bring it back, verify the
# package against `pm list packages -d`, and require a 45 s window with zero new aborts.
# Three rounds, because one is not reliable and the failure is silent.
# started SystemUI eight more times. So this reports what `pm list packages -d` says AND what
# `pidof` says, side by side, rather than one line implying both.
# ---------------------------------------------------------------------------
count_aborts() { adb logcat -d -b crash 2> /dev/null | grep -c 'hasReadColorBufferDma'; }
systemui_disabled() { adb shell pm list packages -d 2> /dev/null | grep -q 'com.android.systemui'; }
systemui_pid() { adb shell pidof com.android.systemui 2> /dev/null | tr -d '\r\n'; }
disable_region_sampling() {
local round=1 i out before after
local round=1 i out pid before after
while [ "$round" -le 3 ]; do
echo "--- SystemUI disable, round $round ---"
echo "--- round $round ---"
for i in $(seq 1 10); do
out="$(adb shell pm disable-user --user 0 com.android.systemui 2>&1 | tr -d '\r')"
echo " pm attempt $i: $out"
@@ -93,36 +125,20 @@ disable_region_sampling() {
sleep 5
done
echo " restarting the framework"
adb shell stop
for i in $(seq 1 20); do
[ -z "$(adb shell pidof system_server 2> /dev/null | tr -d '\r\n')" ] && break
sleep 2
done
echo " system_server down after ~$((i * 2)) s"
adb shell start
for i in $(seq 1 30); do
if adb shell service check package 2> /dev/null | grep -q ': found' \
&& adb shell service check activity 2> /dev/null | grep -q ': found' \
&& [ -n "$(adb shell pidof system_server 2> /dev/null | tr -d '\r\n')" ]; then
echo " services back after ~$((i * 5)) s"
break
fi
sleep 5
done
if systemui_disabled; then
echo " verified: com.android.systemui is in pm list packages -d"
echo " pm list packages -d: com.android.systemui is in it"
else
echo " NOT DISABLED after the restart -- the package state did not survive"
round=$((round + 1))
continue
echo " pm list packages -d: com.android.systemui is NOT in it"
fi
# Printed next to the line above precisely because the two disagree on this image, and a
# reader who sees only the first will believe something that is not true.
pid="$(systemui_pid)"
echo " com.android.systemui pid: ${pid:-none} (expected: a pid -- see the header)"
before="$(count_aborts)"
sleep 45
after="$(count_aborts)"
echo " abort rate, SystemUI disabled: $((after - before)) new in 45 s (total ${after:-0})"
echo " aborts: $((after - before)) new in 45 s (total ${after:-0})"
[ "$((after - before))" -eq 0 ] && break
echo " still aborting after round $round"
round=$((round + 1))
@@ -131,16 +147,16 @@ disable_region_sampling() {
# A warning rather than an exit. If the disable did not take, the run is about to report
# `Starting 0 tests` and fail on its own -- and it will do so with the logcat, the crash
# buffer and the diagnostics attached, which is more useful than dying here with none of it.
if systemui_disabled; then
echo " final state: SystemUI disabled"
if [ "$((after - before))" -eq 0 ]; then
echo " final state: 45 s with no new aborts -- the suite starts here"
else
echo "::warning::E2E api${LABEL}: SystemUI is still enabled -- expect INSTRUMENTATION_ABORTED"
echo "::warning::E2E api${LABEL}: still aborting after three rounds -- expect INSTRUMENTATION_ABORTED"
fi
return 0
}
if [ "${E2E_DISABLE_SYSTEM_UI:-}" = "1" ]; then
echo "::group::E2E api${LABEL} -- removing the region-sampling listener"
echo "::group::E2E api${LABEL} -- waiting out the boot-time gralloc aborts"
disable_region_sampling
echo "::endgroup::"
fi
+22 -28
View File
@@ -28,6 +28,15 @@ name: API 37 debug
# - It does not fork .github/scripts/e2e-run.sh. That script owns the FAILED-vs-WEDGED
# split, the SIGQUIT thread dump and the streamed logcat, and it is the copy CI
# exercises every day. This calls it, exactly as status_check.yml does.
#
# The SystemUI disable below is the exception, and it is a real one: this workflow
# drives it from its own probe step so `disable_system_ui` can be turned off for a
# dispatch, where the real leg gets it through `E2E_DISABLE_SYSTEM_UI`. Two copies of
# that logic therefore exist and must be changed together. **This instrument is also
# what established that the disable half of it does nothing** -- run 34010167885, in
# which making its framework restart real cost the leg every test it had. Read
# .github/scripts/e2e-run.sh's header for the measurements; the restart is gone from
# both copies and what remains is the 45-second quiet window.
# - It does not change status_check.yml. If a configuration here turns out to work,
# the change to the real matrix is proposed separately.
#
@@ -291,45 +300,30 @@ jobs:
sleep 5
done
# pm disable-user does not retract SystemUI's existing region-sampling
# registration -- by the time boot completes it has already registered. Only a
# framework restart brings back a SystemUI-less SurfaceFlinger. See
# disable_region_sampling in tools/local-emulator/run-e2e.sh.
echo " restarting the framework"
adb shell stop
for i in $(seq 1 20); do
[ -z "$(adb shell pidof system_server 2> /dev/null | tr -d '\r\n')" ] && break
sleep 2
done
echo " system_server down after $((i * 2)) s"
adb shell start
for i in $(seq 1 30); do
if adb shell service check package 2> /dev/null | grep -q ': found' \
&& adb shell service check activity 2> /dev/null | grep -q ': found' \
&& [ -n "$(adb shell pidof system_server 2> /dev/null | tr -d '\r\n')" ]; then
echo " services back after $((i * 5)) s"
break
fi
sleep 5
done
# NO FRAMEWORK RESTART. There was one here, and making it work (it needed
# `adb root`) is what proved the whole disable is ineffective on this image:
# SystemUI starts anyway, measured on CI and locally, and the restart itself
# loses the package state to PackageManager's delayed write and leaves the leg
# reporting `Starting 0 tests`. e2e-run.sh's header carries the measurements.
# What is left, and what is load-bearing, is the quiet window below.
if systemui_disabled; then
echo " verified: com.android.systemui is in pm list packages -d"
echo " pm list packages -d: com.android.systemui is in it"
else
echo " NOT DISABLED after the restart -- the package state did not survive"
round=$((round + 1))
continue
echo " pm list packages -d: com.android.systemui is NOT in it"
fi
# Beside it, because the two disagree on this image and the first line alone
# reads as a claim about the process that is not true.
echo " com.android.systemui pid: $(adb shell pidof com.android.systemui 2> /dev/null | tr -d '\r\n')"
before="$(count_aborts)"
sleep 45
after="$(count_aborts)"
echo "--- abort rate, SystemUI disabled: $((after - before)) new in 45 s (total ${after:-0}) ---"
echo "--- aborts: $((after - before)) new in 45 s (total ${after:-0}) ---"
[ "$((after - before))" -eq 0 ] && break
echo " still aborting after round $round"
round=$((round + 1))
done
systemui_disabled && echo "final state: SystemUI disabled" || echo "final state: SystemUI STILL ENABLED -- expect Starting 0 tests"
systemui_disabled && echo "final state: com.android.systemui is disabled in pm (it still runs)" || echo "final state: com.android.systemui is not even disabled in pm"
fi
echo "--- crash buffer (tail 60) ---"
+23 -23
View File
@@ -254,31 +254,31 @@ jobs:
api-level: "36"
# API 37, and it is NOT the same device as the four rows above it.
#
# CAVEAT, read this before trusting a green here: this leg runs with
# SystemUI disabled and the framework restarted under it. No other leg
# and no Pixel run uses that configuration. It is defensible only because
# nothing THIS LEG RUNS touches system UI -- Media3, FFmpeg and
# WorkManager tests -- and because the alternative is no CI coverage of
# the level this app targets. **Anything that ever does depend on system
# UI must not trust this row.** E2E_DISABLE_SYSTEM_UI is what does it;
# .github/scripts/e2e-run.sh explains the mechanism and why every step of
# it is verified rather than assumed.
# THE CAVEAT THAT USED TO BE HERE IS WITHDRAWN, 2026-09-05, and the
# withdrawal is good news. It said this leg "runs with SystemUI disabled
# and the framework restarted under it", that no other leg or Pixel run
# uses that configuration, and that anything depending on system UI must
# not trust this row. **None of that was ever true.** Measured: the
# framework restart is two root-only adb commands that answered `Must be
# root` on every leg ever run, and `pm disable-user` does not stop SystemUI
# starting on this image anyway -- in run 34006456986 the package is
# verified disabled at 02:29:33 and SystemUI (pid 4275) is up from 02:28:52
# for the whole run. So this row's device configuration is the same as the
# other four's, and a green here means what a green on 33-36 means.
#
# "this leg" and not "this suite", since 2026-08-24, and the difference is
# now load-bearing: SafPickerRoundTripTest DOES touch system UI. It drives
# DocumentsUI and rotates the display, and both reach the gralloc mapper
# this image aborts 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, so notAnnotation below keeps it off this row; the
# PICKER test passes and runs here like anything else. A rotation rebuilds
# every surface at once, and starting another app's activity does not.
# E2E_DISABLE_SYSTEM_UI still exists and still runs, because what it
# actually buys is a 45-second window with no new gralloc aborts before the
# suite starts -- the boot-time ones land close together and instrumentation
# has to begin after them, not between them. The name is stale and kept:
# read .github/scripts/e2e-run.sh's header, which carries the measurements.
#
# So this row does now run one test that depends on system UI, and the
# caveat above still applies to it: a green here is not evidence the picker
# works on a device with SystemUI running -- the Pixel release check is.
# docs/api-37-emulator-crash.md has the per-method measurements, and the
# correction that produced them.
# notAnnotation below keeps five tests off this row, and one of
# them is new. SafPickerRoundTripTest's PICKER test was measured on
# 2026-08-24 as passing here and was left on the leg; four gating logcats
# read on 2026-09-05 show it aborting system_server from the task-snapshot
# path on every single run, pass or fail, which is what had been failing
# unrelated PRs (#108). Both of that class's tests now carry the marker.
# docs/api-37-emulator-crash.md has the timings and the correction.
#
# api-level must be a POINT release. A bare 37 is not an SDK package and
# fails during setup, which cost a run to discover. `37.0` is the choice
+35 -7
View File
@@ -76,17 +76,45 @@ days. Read it as the current answer, and see the git history if you need the old
`angle_indirect` and `swangle_indirect` all boot, while `auto`, `off`, `guest` and
`swiftshader_indirect` do not. `docs/local-emulator.md` has the evidence and the per-API renderer
table.
- **CI runs API 37, and it gates.** The matrix is 33/34/35/36/37. **Three** of the 60 instrumented
tests cannot pass on that image, for two unrelated reasons: two Media3 hardware transcodes fail
inside the emulator's own `c2.goldfish.h264.decoder`, and one SAF test takes the framework down
when it rotates the display. All three carry `@FailsOnEmulatorApi37` and run in a separate
`continue-on-error` job; the gating leg runs the other 57.
- **CI runs API 37, and it gates.** The matrix is 33/34/35/36/37. **Five** of the 69 instrumented
tests cannot be *run* on that image, for three unrelated reasons: three Media3 tests fail inside
the emulator's own `c2.goldfish.h264.decoder`, one SAF test takes the framework down when it
rotates the display, and its sibling — the SAF picker round trip — aborts `system_server` from
the task-snapshot path whether it passes or not. All five carry `@FailsOnEmulatorApi37` and run
in a separate `continue-on-error` job; the gating leg runs the other 64.
**These two numbers move with the suite and are derived, not remembered.** `grep -cE
'^\s*@Test' ` over `app/src/androidTest` is the first; the second is that minus the marker
count `.github/scripts/e2e-report-shape.sh` greps. Cross-check against any run's shape rather
than trusting the sentence: a leg below 37 reports the first as `expected`, and the API 37
gating leg reports the second.
**That third reason is why "cannot pass" became "cannot be run" on 2026-09-05.** Four gating
runs were read logcat-first — 34006456986, 34001744574, 34001377499 and the green 34002313300 —
and each carries exactly two `hasReadColorBufferDma` aborts before the suite (surfaceflinger,
during boot and the SystemUI disable) and exactly **one** during it: `system_server`, thread
`TaskSnapshotPer`, always inside the picker test's window, and nothing else in the gating set
reached the mapper at all. Whether the leg went red was luck — one run passed the test and lost
the leg anyway with `failed: 0`, another passed it 0.6 s after the abort and went green. That is
#108, it cost roughly a third of the gating legs over the wave-4 landings (#190), and a marker
is what it needed. `docs/api-37-emulator-crash.md` has the timings.
**A second thing came out of those logcats, and it withdraws a caveat rather than adding one.**
The API 37 row was documented as the one leg running "with SystemUI disabled and the framework
restarted under it", which nothing else does. Neither half was ever happening: `adb shell stop`
and `start` are root-only and answered `Must be root` on every leg ever run, and `pm
disable-user` does not stop SystemUI starting on this image anyway — measured on CI and locally,
with and without a real restart. **So this row's device configuration is the same as the other
four's, and a green here means what a green at 33–36 means.** `E2E_DISABLE_SYSTEM_UI` is kept
under its now-stale name because what it really buys is a 45-second window with no new gralloc
aborts before the suite starts, which is load-bearing; `.github/scripts/e2e-run.sh`'s header is
where that is written down.
That job is still called `E2E API 37 Media3 hardware transcode (advisory)`, which no longer
describes everything in it. The name is kept deliberately — it is not a required context and
people have learned to look for it — so **read the marker, not the name**, for what it holds.
**It is red on every PR, by design**: do not read it as your change breaking something, and do
not read a green run as evidence those three tests pass.
not read a green run as evidence those five tests pass.
`docs/api-37-emulator-crash.md` has the measurements.
**That instruction is also why nobody looks, so the job now reports its own shape** — expected,
@@ -106,7 +134,7 @@ days. Read it as the current answer, and see the git history if you need the old
is gradle never returning, so the log it left says nothing about it.
Still true, and the reason the advisory job is not simply deleted: **API 37 needs a manual check on
the Pixel 10 Pro XL before each release.** Those three tests are the one thing CI cannot answer
the Pixel 10 Pro XL before each release.** Those five tests are the one thing CI cannot answer
for.
On a device or emulator, build only the ABI it can execute:
@@ -1,7 +1,7 @@
package org.libremediaconverter
/**
* Marks an instrumented test that does not pass on the `android-37.x` **emulator** system images.
* Marks an instrumented test that cannot be run on the `android-37.x` **emulator** system images.
*
* This is a marker, not a skip. Nothing reads it except CI, and CI reads it twice — once with
* `notAnnotation` to build the gating API 37 leg, and once with `annotation` to build the advisory
@@ -9,6 +9,15 @@ package org.libremediaconverter
* That is the whole reason there is one annotation rather than a pair of test lists: two lists
* drift, and the drift is silent in both directions (a test that runs nowhere reads as green).
*
* **"Cannot be run" covers two things, and it said only the first until 2026-09-05.** Four of the
* five carriers simply fail: three Media3 tests die in the image's own `c2.goldfish.h264.decoder`,
* and the SAF rotation test takes the framework down with it. The fifth —
* `SafPickerRoundTripTest.pickingAFileThroughTheSystemPickerFillsInTheFileCard` — **passes about
* half the time and aborts `system_server` every time**, which is worse for a gating leg than an
* honest failure: it fails the leg from the teardown, with no failing test to point at (#108).
* The wording was widened rather than the test excused; that test's own KDoc has the four-run
* measurement.
*
* It says only what has been measured: **on the emulator, at API 37.** The same tests pass on a
* physical Pixel 10 Pro XL at API 37 and at API 33–36 on the same runner under the same renderer,
* so this must never be read as "this test is allowed to fail at API 37" — only as "the API 37
@@ -37,11 +46,28 @@ annotation class FailsOnEmulatorApi37
* keep printing with nothing to compare to, so it announces that it could not read the baseline
* rather than falling quiet. If you see that notice, this line is what it means.
*
* **One number, both checks, and that is what the marker means.** A test carrying it cannot pass
* **One number, both checks, and that is what the marker means.** A test carrying it cannot be run
* on this image, so the count is simultaneously how many the advisory leg runs and how many fail.
* A *smaller* failure count is the interesting direction: it means one of them now passes, which
* is the trigger the KDoc above names for deleting the annotation.
*
* **The picker test is the one to read that sentence carefully for.**
* `pickingAFileThroughTheSystemPickerFillsInTheFileCard` was marked on 2026-09-05 for aborting
* `system_server` rather than for failing (#108), and on the gating leg it passed two runs of
* four. It fails on the advisory leg because the rotation test runs before it and takes the
* framework down first — measured, `api37-debug.yml` run 34008889182, which reports
* `expected: 4, received: 4, failed: 4` with the four in the order Media3, Media3, rotation,
* picker. (Those dispatches predate the third Media3 marker landing on `main`, so their totals
* are four rather than five; the ordering they establish is what matters here.)
*
* **But a second dispatch of the identical configuration reported 4/3/3**, having lost the last
* test to the abort rather than to anything about the test list, and that is why
* `e2e-report-shape.sh` compares `failed` only on a run that finished. `expected` is compared
* always — it comes from `Starting N tests`, which is printed before anything can abort, so it is
* the field that answers "is the marked set the size this number says". Read a *clean* run
* reporting fewer failures than this as one of them now passing; read a truncated one as the
* framework having died, which is this job's normal.
*
* So: adding or removing a [FailsOnEmulatorApi37] means changing this number, in this file, in
* the same diff. The report says so on the run itself if you forget — it prints the tree's own
* `grep` count beside this one.
@@ -52,4 +78,4 @@ annotation class FailsOnEmulatorApi37
* `INSTRUMENTATION_ABORTED`, so the count is a number taken from a partial run. The report
* records the truncation next to the counts for that reason.
*/
const val FAILS_ON_EMULATOR_API37_BASELINE = 4
const val FAILS_ON_EMULATOR_API37_BASELINE = 5
@@ -25,6 +25,7 @@ import org.junit.runner.RunWith
import org.libremediaconverter.convert.MediaProbe
import org.libremediaconverter.convert.StagingNames
import org.libremediaconverter.ffmpeg.ConcatEngine
import org.libremediaconverter.ffmpeg.FFmpegEngine
import org.libremediaconverter.model.ConcatStrategy
import org.libremediaconverter.work.ConcatWorker
import java.io.File
@@ -236,6 +237,55 @@ class ConcatEngineTest {
)
}
/**
* A failed join tells the user the return code and what FFmpeg said.
*
* **This is the device half of #203/#217**, whose PR closed by noting the join legs had not
* been run. Running them would not have answered it: nothing on either source set drove a real
* join *failure*, so the unified message was asserted only against values a JVM test hands to
* `sessionOutcome` directly.
*
* What is device-only here is that the three reads behind that message work against a real
* native session at all — `getReturnCode`, `getFailStackTrace` and `getAllLogsAsString`. If
* the log tail came back null or empty on a device, the user would get `Joining failed (1): `
* with nothing after the colon and every JVM test would still pass.
*
* **What this deliberately does not pin is the preference between the two detail sources.** On
* an ordinary non-zero return code FFmpegKit reports no fail stack trace, so the stack-trace-
* first rule and the log-tail-first rule produce the same text and no assertion here can tell
* them apart. That ordering is [SessionOutcomeTest][org.libremediaconverter.ffmpeg.SessionOutcomeTest]'s
* job, where both sources can be non-blank at once. Asserting it here would be a test whose
* KDoc claims more than it checks — the `probeForConcat` mistake wave 3 caught.
*
* The failure is forced with an input that does not exist, which the concat demuxer rejects
* the same way on every FFmpeg build, rather than with malformed media whose handling varies.
*/
@Test
fun aFailedJoinReportsTheReturnCodeAndWhatFFmpegSaid(): Unit = runBlocking {
val missing = File(context.cacheDir, "no_such_clip.mp4").also { it.delete() }
val out = output("joined_failure.mp4")
val failure = runCatching {
engine.join(listOf(Uri.fromFile(clipA), Uri.fromFile(missing)), out)
}.exceptionOrNull()
assertTrue(
"a join over a missing input must fail, got $failure",
failure is FFmpegEngine.FFmpegException,
)
val message = failure?.message.orEmpty()
assertTrue(
"the message must name the operation and carry the return code, was: '$message'",
message.startsWith("Joining failed ("),
)
// The half a JVM test cannot reach: a real session actually produced detail to show.
val detail = message.substringAfter("): ", "")
assertTrue(
"the message stopped at the return code and told the user nothing, was: '$message'",
detail.isNotBlank(),
)
}
@Test
fun theListFileIsCleanedUpAfterJoining(): Unit = runBlocking {
val out = output("joined_cleanup.mp4")
@@ -298,7 +298,35 @@ class SafPickerRoundTripTest {
device.waitForIdle()
}
/**
* **Marked for API 37 because of what it does to the image, not because it fails there.**
*
* This is the one place the marker's KDoc phrase "cannot pass on this image" does not fit, and
* the distinction is worth keeping rather than smoothing over. Across the four gating API 37
* runs whose logcats were read on 2026-09-05 — 34006456986, 34001744574, 34001377499 and the
* green 34002313300 — the leg carries exactly two `hasReadColorBufferDma` aborts before the
* suite starts (both `surfaceflinger`, during boot and the SystemUI disable) and then exactly
* **one** during it. Every time, that one is `system_server` on the `TaskSnapshotPer` thread,
* and every time it lands inside this test's window. No other test in the gating set reaches
* the mapper at all.
*
* So this test kills the framework on that image whether it passes or not, and whether the leg
* goes red is luck: 34001377499 passed it and lost the leg anyway (`failed: 0`, teardown
* broken), 34002313300 passed it 0.6 s after the abort and went green. That is #108, and it is
* why the leg was failing on unrelated PRs.
*
* `docs/api-37-emulator-crash.md` measured this test on 2026-08-24, recorded "passes, 4 aborts
* in the window", and concluded that a rotation reaches the mapper where starting DocumentsUI
* does not. The aborts were seen; what was not drawn out is that they are this test's own and
* are not intermittent.
*
* The marker is what routes it off the gating leg and into the advisory job beside its
* rotation sibling. **It is not a statement about the picker**: the same test passes on API
* 33–36 on the same runner and on the Pixel 10 Pro XL, which is where API 37's answer comes
* from.
*/
@Test
@FailsOnEmulatorApi37
fun pickingAFileThroughTheSystemPickerFillsInTheFileCard() {
pickTheFixture()
@@ -604,6 +632,9 @@ class SafPickerRoundTripTest {
* It is also why this counts backs rather than pressing a fixed number of them. One back is
* enough from Recent and two are needed from inside the root, but a third from Recent would
* finish `MainActivity` and take the rest of the test with it.
*
* **[forceStopThePicker] is the escalation after the presses, and it exists because a back
* press is not always deliverable.** See its own KDoc for the measurement.
*/
private fun dismissThePicker() {
repeat(BACK_PRESSES) {
@@ -618,15 +649,50 @@ class SafPickerRoundTripTest {
// The check after the last press, and not a spare one: `repeat` presses on its final
// iteration too, so without this a dismissal that worked on the last press would still be
// reported as a failure to close.
if (awaitAppFocus()) return
forceStopThePicker()
if (!awaitAppFocus()) {
throw AssertionError(
"the system picker would not close: after $BACK_PRESSES back presses the app " +
"still does not have the window focus, and ${device.currentPackageName} is " +
"in front. What could be seen: " + describeWindows(),
"the system picker would not close: after $BACK_PRESSES back presses and a " +
"force-stop of $DOCUMENTS_UI_PACKAGE the app still does not have the window " +
"focus, and ${device.currentPackageName} is in front. What could be seen: " +
describeWindows(),
)
}
}
/**
* Kills the picker's process, for when no back press can reach it.
*
* **The failure this exists for cannot be answered with input, and that is the whole point.**
* Measured on the gating API 37 legs of runs 34006456986 and 34001744574, which fail this way
* and whose logcats say the same thing in the same order. `UiObject2.click()` on the fixture's
* root is injected at the node's centre and the framework discards it —
* `InputDispatcher: No new touched window at (539.0, 525.0) in display 0` — because
* `PickActivity` has published accessibility nodes but has no touchable window there yet.
* `click()` cannot see that and returns normally, so the walk goes on to wait out
* [PICKER_TIMEOUT_MS] for a fixture that was never navigated to. By the time this function's
* caller starts pressing back, WindowManager is still saying
* `no window has focus but ...PickActivity may eventually add a window when it finishes
* starting up` — and goes on saying it for another 63 s. Every one of the four presses is
* dropped, and DocumentsUI ANRs on `Input dispatching timed out`.
*
* So the picker is in front, unreachable by key or by touch, and [pickTheFixture]'s whole
* point — that a second `PickActivity` rebuilds every window and list in it — is unreachable
* with it. `am force-stop` goes around input entirely: `UiAutomation` runs shell commands as
* uid 2000, which holds `FORCE_STOP_PACKAGES`, so the picker's process is killed, its
* activity leaves the task it was launched into, and `MainActivity` — the activity below it in
* that same task — is resumed with the focus.
*
* **Only on the failure path**, after every back press has been spent, so a picker that closes
* the ordinary way never reaches this and is not altered by it. If the framework itself is
* gone, this cannot help either, and the caller still reports what it could see.
*/
private fun forceStopThePicker() {
device.executeShellCommand("am force-stop $DOCUMENTS_UI_PACKAGE")
device.waitForIdle()
}
/** True once [MainActivity] has the window focus, false if it does not take it in time. */
private fun awaitAppFocus(): Boolean = try {
composeRule.waitUntil("the app has the window focus back", FOCUS_TIMEOUT_MS) {
+189 -18
View File
@@ -315,25 +315,90 @@ clean zero. Its own post-disable check on the run recorded below printed
So what is reliably achieved is a **rate collapse** — from roughly one abort every fourteen
seconds to one every forty-five — which a 47-second Gradle run survives and a five-minute one
might not. The 180-second zero above is one measurement on a device that had been up for twelve
minutes and had already cycled its framework several times. The harness prints the quiet-check
delta on every run precisely so this is visible rather than assumed.
might not.
One ordering detail cost a whole run and is now encoded in `disable_region_sampling`: by the time
`sys.boot_completed` flips, SystemUI has **already registered**, and `pm disable-user` does not
retract an existing registration — it only stops the package being started again. Disabling it
and proceeding straight to the tests fails exactly as before. The harness therefore does
`stop; start` afterwards, so the framework that comes back never starts SystemUI at all.
**And that restart has never happened — which is how the disable turned out not to work either.**
Corrected 2026-09-05; this replaces the two paragraphs above rather than qualifying them.
`adb shell stop` and `start` are root-only, adbd is not root on a booted emulator, and all three
copies of this logic called them without `adb root`. On CI both printed `Must be root`, between
lines that read as if the restart had happened; `run-e2e.sh` sent them to `/dev/null`, so its
`Must be root` was never even visible. Neither number in those logs was an observation either —
the `pidof` loop breaks when the process is gone and otherwise falls out at its last iteration,
and the old code printed the iteration count either way, so `system_server down after ~40 s` is
what a stop that did nothing looks like.
Adding `adb root` made the restart real, and **that is what proved the disable ineffective**.
`api37-debug` run 34010167885, `disable_system_ui=true`:
```
--- disable round 1 ---
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
services back after 10 s
NOT DISABLED after the restart -- the package state did not survive
```
Three rounds of that, then `final state: SystemUI STILL ENABLED`, and the leg reported
`expected: 0, received: 0` — `Starting 0 tests`, the exact failure this function exists to
prevent.
Bisected locally on `android-37.0`, which explains the lost state and nothing else:
| arm | sequence | disabled after the restart? |
|---|---|---|
| A | `pm disable-user`, then `stop` at once | **no** |
| B | `pm disable-user`, wait 15 s, then `stop` | **yes** |
That is PackageManager's delayed write of package restrictions: the stop kills `system_server`
before the settings are flushed, and arm A is what CI did. **Arm B does not help either**, which
is the measurement that matters. With the package verified `disabled-user` before *and* after a
further clean restart:
```
package still disabled? YES
processes:
9275 00:17 system_server
9695 00:14 com.android.systemui <- started 3 s after system_server
```
CI's own logcat says the same without any restart at all. In the gating leg of run 34006456986,
`pm disable-user` is accepted at 02:28:37.9 and the package really is in `pm list packages -d` at
02:29:33 — and SystemUI is started at 02:28:39.5 and again at 02:28:52.3, the second of which
(pid 4275) is alive for the whole instrumentation run.
**So `pm disable-user --user 0 com.android.systemui` does not stop SystemUI starting on this
image**, with or without a framework restart, on CI or locally. The premise this section was
built on — "the framework that comes back never starts SystemUI at all" — is false.
Two things follow, pointing in opposite directions.
- **The restart is removed rather than repaired**, in all three copies. It cost a leg every test
it had and there is nothing for it to buy. What is kept is the 45-second window with zero new
aborts, which was always the part doing the work: in that same run 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. The `pm disable-user` call is kept too, for a narrower reason than it
was written for: every green leg and every number quoted about this row was measured with it
applied, and changing the configuration while fixing a flake is not a trade worth making.
- **The rate collapse recorded above is not evidence of what it says.** Both arms of that
comparison had SystemUI running. What it measured is a device twelve minutes into its uptime
against one that had just booted — a real difference, and a different claim. The quiet gate is
still worth having on exactly that reading.
### The two deviations, stated plainly
1. **The renderer is ANGLE, not the host GPU.** Shared with nothing else in the matrix — API
33–36 run `-gpu host` locally, and CI runs `swiftshader_indirect`.
2. **SystemUI is disabled.** The API 37 leg does not run the same device configuration as any
other leg or as the Pixel. It was defensible here because nothing in this suite touched
system UI — Media3, FFmpeg and WorkManager tests — and because the alternative is no local
API 37 coverage at all. **Anything that ever does depend on system UI must not trust this
leg.** Something now does; see the section below.
2. **SystemUI is asked to be disabled, and runs anyway.** This was written as the deviation that
mattered — "anything that ever does depend on system UI must not trust this leg" — and the
measurements above say the deviation does not exist: the package is marked `disabled-user` and
`com.android.systemui` is up for the whole leg regardless. **The correction is good news
rather than bad.** This row is *more* comparable to API 33–36 and to the Pixel than it has
been claiming, not less, and the test that depends on system UI (see the section below) was
never running in the exotic configuration this bullet describes. What `pm disable-user` leaves
behind is a package-manager flag nothing acts on.
### Something does depend on system UI now, and half of it is excluded
@@ -341,12 +406,13 @@ Added 2026-08-24, and the first entry on this page that is not a codec.
`SafPickerRoundTripTest` drives the real system file picker and rotates the display. Both reach
the gralloc mapper — DocumentsUI is another app's windows, and a rotation rebuilds every surface
on screen — and **disabling SystemUI does not help**, because it removes the *idle* trigger
(RegionSamplingThread's nav-bar luma sampling) and not this one.
on screen — and **disabling SystemUI does not help**. Two reasons now, and only the first was
known when this was written: it removes the *idle* trigger (RegionSamplingThread's nav-bar luma
sampling) and not this one, and — see the section above — it does not remove SystemUI either.
Measured one method per fresh emulator, `android-37.0`, `swangle_indirect`, SystemUI disabled and
verified quiet — separately, because inferring the second from the first is the mistake this
page's opening correction is about:
Measured one method per fresh emulator, `android-37.0`, `swangle_indirect`, with the disable
applied and verified quiet — separately, because inferring the second from the first is the
mistake this page's opening correction is about:
| test | result on android-37.0 | `hasReadColorBufferDma` aborts in the window |
|---|---|---|
@@ -357,6 +423,111 @@ So a rotation, which rebuilds every surface at once, is what the mapper does not
starting DocumentsUI is not. Only the rotation test carries `@FailsOnEmulatorApi37`; the picker
test runs on the gating leg like anything else.
#### That last sentence was wrong for twelve days, and the aborts in the table said so
**Corrected 2026-09-05.** Read the second row again: the picker test passes *and takes four
`hasReadColorBufferDma` aborts with it*. This section counted them, put them in the table, and then
drew the conclusion from the pass/fail column alone. The right question is not "does the test
pass" but "does the image survive it", and the answer had been printed in the right-hand column
from the day it was written.
Four gating API 37 runs read logcat-first — 34006456986, 34001744574, 34001377499, and the **green**
34002313300 — say it without ambiguity. Each carries exactly two aborts before the suite starts
(both `surfaceflinger`, during boot and the SystemUI disable) and then exactly **one** during it:
| run | picker test window | the run's only in-suite abort | leg |
|---|---|---|---|
| 34006456986 | 02:33:04.2 → 02:34:46.9, **failed** | 02:34:46.845 | red, `failed: 1` |
| 34001744574 | 00:55:41.4 → 00:57:23.9, **failed** | 00:57:23.794 | red, `failed: 1` |
| 34001377499 | 00:35:53.3 → 00:36:00.6, passed | 00:35:59.662 | red, `failed: 0` |
| 34002313300 | 00:58:12.7 → 00:58:19.8, passed | 00:58:19.218 | green |
Every one is `system_server`, thread `TaskSnapshotPer`, and every one lands inside that test's
window. Nothing else in the gating set reached the mapper at all. So the picker test is
**deterministic** in what it does to the image and a coin flip in what the leg reports: 34001377499
passed it and lost the leg from teardown with no failing test to name, and 34002313300 passed it
0.6 s after the abort and went green.
That is #108, which had been filed against this behaviour in August and left open because the
trigger was unknown. The trigger is this test. It now carries `@FailsOnEmulatorApi37` too, and the
marker's KDoc had to widen from "does not pass on this image" to "cannot be run on this image" to
say so honestly.
The stack, for the record, is a different caller from either of the two above:
```
Cmdline: system_server name: TaskSnapshotPer
Abort message: 'Assertion failed: !rcEnc->featureInfo()->hasReadColorBufferDma'
#04 mapper.ranchu.so GoldfishMapper::readFromHost(cb_handle_t const&) const+543
#06 libui.so android::Gralloc5Mapper::lock(...)+63
#10 libandroid_runtime.so android::lockImageFromBuffer(...)+374
#15 framework.jar android.media.ImageReader$SurfaceImage.getPlanes+50
#17 services.jar com.android.server.wm.TaskSnapshotConvertUtil.copyToSwBitmapDirect+56
#28 services.jar com.android.server.wm.SnapshotPersistQueue$StoreWriteQueueItem.writeBuffer+66
#32 services.jar com.android.server.wm.SnapshotPersistQueue$1.run+186
```
WindowManager writing a task snapshot to disk, which needs the buffer as a software bitmap, which
is the non-DMA readback path. `PickActivity` is started **into the app's own task** (`Task #11
A=10234:org.libremediaconverter` in the logcat), so the snapshot being persisted is that task's,
and the churn at the end of the pick is what schedules it.
#### There is no shell knob for task snapshots, and that was checked rather than assumed
#108 asks whether `TaskSnapshotPersister` is suppressible the way the region-sampling listener was.
Probed on a local `android-37.0 google_apis x86_64` AVD, 2026-09-05:
```
getprop | grep -i snapshot # nothing but apexd-snapshotde
settings list global | grep -iE 'snapshot|recents' # empty
device_config list window_manager | grep -i snapshot # empty
cmd window help # no snapshot or screenshot command
dumpsys window | grep -i snapshot # mSnapshotEnabled=true, for Task and Activity
```
`mSnapshotEnabled` is real state and there is nothing that sets it from outside. The only
`device_config` hits anywhere in the tree are aconfig flags — e.g.
`windowing_frontend/com.android.window.flags.respect_requested_task_snapshot_resolution` — which
tune the snapshot rather than disable it. So the marker is the available answer, not the lazy one.
#### When the picker test does fail, the abort is the coda and not the cause
Worth separating, because the failure message points the wrong way. In both runs where the test
itself went red, it had been broken for 98 seconds before the abort landed. The discriminator is
one line, present in both reds and absent from the green:
```
I/InputDispatcher: No new touched window at (539.0, 525.0) in display 0
```
(539, 525) is the centre of the fixture's root row — the same coordinates the green run clicks.
The touch reaches no window and is discarded; `UiObject2.click()` cannot see that and returns
normally. DocumentsUI then logs nothing at all, where the green run logs `DocumentStack` and
`Creating new directory loader` 40 ms after its click. The walk waits out its timeout twice for a
fixture it never navigated to, and by the time the back presses start, WindowManager is still
saying `no window has focus but ...PickActivity may eventually add a window when it finishes
starting up` — for another 63 s. All four presses are dropped, DocumentsUI ANRs on
`Input dispatching timed out`, and only *then* does the abort fire and make the failure message
read `no windows at all`.
`SafPickerRoundTripTest.forceStopThePicker` is the answer to that half: `am force-stop` goes around
input entirely, so the picker's process can be removed from a task no key press can reach and
`pickTheFixture`'s whole-picker retry — which exists for exactly this — becomes reachable again.
That is a fix to the test on every level, not to API 37.
**It was made to bite before it was believed.** On a local API 36 emulator, with the walk cut short
so the picker is left open and in front and with `device.pressBack()` removed, so that nothing but
the force-stop can close it:
| | result |
|---|---|
| with `forceStopThePicker()` | **passes** — `ActivityManager: Force stopping com.google.android.documentsui ... from pid 5334`, `Killing 5269:com.google.android.documentsui (adj 0)`, a second `PickActivity` opens, the retry completes the pick |
| with the one call removed | **fails** — `the system picker would not close: after 4 back presses ... com.google.android.documentsui is in front`, which is the API 37 failure verbatim |
The unmutated class passes on that emulator either way, which is the point of running the mutation
at all: the recovery path is unreachable on a healthy device, so a green suite says nothing about it.
#### The correction that produced that table
**The first version of this section said both tests failed, and put the marker on the class.** The
+16 -14
View File
@@ -355,12 +355,10 @@ boot_emulator() {
# 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.
# 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
@@ -383,14 +381,18 @@ disable_region_sampling() {
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.
# 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' \