SafPickerRoundTripTest's save tests have two failure shapes still unexplained after #269 #270

Open
opened 2026-09-07 22:08:33 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-09-07 22:08:33 +00:00 (Migrated from github.com)

Split out of the #102 census (docs/ci-failure-modes.md). #102's mode 6 — the SAF save tests
failing on API 33-36 — turned out to be six failures with five different messages, not one
mode. One of them has a traced mechanism and a fix. Two do not, and this is those two.

Where they came from

The two save tests (#226, #250) landed on 2026-09-06 and account for every gating failure on API
33-36 since. Seven leg-attempts failed in the 88 that ran them. Sorting by the message under the
FAILED marker rather than by the test name:

run / attempt head message status
34041593697 a1 (35) fa10d94, the commit that added the test No compose hierarchies found, thrown directly fixed by b23ff0f
34056545386 a1 (35) cbbaf74 ComposeTimeoutException ... after 120000 ms fixed by 19e3539; not a system-service failure at all
34161043035 a1 (35) 9fd96d0 NPE: Cannot run onActivity since Activity has been destroyed already traced, fixed
34146936252 a2 (35) 73482520 waited 300000ms ...; last composition error: No compose hierarchies the permission-dialog helper #269 replaced; re-count
34057706195 a1 (34) 19e3539 No compose hierarchies found ×2, thrown directly shape A, open
34067653670 a1 (35), 34146936252 a1 (35) d45abe7, 73482520 waited 300000ms for a node tagged action.saveFile, no composition error shape B, open

Shape A — b23ff0f did not close the "no compose hierarchies" case

b23ff0f ("Wait for the app to come back before asking Compose about it") was written for exactly
this message. 34057706195's head is 19e3539, and
git merge-base --is-ancestor b23ff0f 19e3539 says it contains that fix — and both save tests
failed on it anyway, with the exception thrown directly rather than through awaitNode.

So either the wait is in the wrong place or there is a second way for the composition to go away.
The direct throw is the clue worth starting from: awaitNode catches and re-labels, so a raw
IllegalStateException: No compose hierarchies found came from a call site that is not behind
that helper.

Shape B — the app is alive and well, and the conversion never starts

This is the more interesting one and it is not an Activity-lifecycle failure at all.

34067653670's per-test logcat for aSaveWritesToTheDocumentTheSystemPickerCreated:

23:49:49.769  MainActivity RESUMED
23:49:53.514  ffmpeg-kit-next: Generated fd 141 for SAF id 14.   <- pickTheFixture's probe
23:49:53.599  UiDevice: Waiting 5000ms for ... com.google.android.permissioncontroller
   ... 305 seconds with nothing from this app at all ...
23:54:58.684  MainActivity PAUSED / STOPPED / DESTROYED          <- the Compose rule tearing down
23:54:59.152  TestRunner: failed

MainActivity is RESUMED for the whole 300 s, and awaitNode appends its last composition error only when fetchSemanticsNodes actually threw — it did not, so the composition was
readable throughout
. The app was fine. What never happened is the conversion.

The green attempt of the same test on the same run (34146936252 a3) is the control:

18:01:51.949  WM-WorkerWrapper: Starting work for org.libremediaconverter.work.ConversionWorker
18:01:51.995  ConversionWorker: Routing lmc-r38-fixture.mp4 -> OutputSpec(...) via FFMPEG
18:01:51.997  FFmpegEngine: ffmpeg -hide_banner -y -i ffkitsaf:16.mp4 -c:v libx265 ...

None of those three lines appears anywhere in the failing run. So action.saveFile never
appeared because no conversion ever ran, not because the UI was unreachable.

A hypothesis, explicitly unproved

The Waiting 5000ms for ... permissioncontroller immediately after the Convert tap is the
permission-dialog helper as it stood before #269 — tap Convert, wait for the dialog, press back on
it. ConverterScreen wires Convert to requestNotifications.launch(POST_NOTIFICATIONS) and starts
the conversion from the permission result, so a back press on that dialog is a denial, and a
denial would leave the screen exactly where this logcat leaves it. That fits, and it is a
hypothesis, not a finding
— I have not shown the dialog was up on that run.

What to do first, and it is not a fix

Re-count over post-#269 leg-attempts before touching anything. #269 replaced that helper with
pm grant plus an assertion that the permission is held, so shape B's most plausible mechanism may
already be gone; and shape A's two occurrences both predate #269 as well. At the time of writing
there are zero post-#269 gating leg-attempts on main — the only run on #269's own head is
34161043035, whose attempt 1 is the mode that has now been fixed separately.

docs/ci-failure-modes.md has the counting rules: per leg-attempt, and with these tests'
own denominator (API 33-36 only — they carry @FailsOnEmulatorApi37 — and only from
2026-09-06T15:12).

Done means

Each of the two shapes either reproduced and fixed, or re-counted to zero over a stated number
of post-#269 leg-attempts and closed with that number written down
. Not "has not been seen
lately".

Split out of the #102 census (`docs/ci-failure-modes.md`). #102's mode 6 — the SAF *save* tests failing on API 33-36 — turned out to be **six failures with five different messages**, not one mode. One of them has a traced mechanism and a fix. **Two do not, and this is those two.** ## Where they came from The two save tests (#226, #250) landed on 2026-09-06 and account for every gating failure on API 33-36 since. Seven leg-attempts failed in the 88 that ran them. Sorting by the message under the `FAILED` marker rather than by the test name: | run / attempt | head | message | status | |---|---|---|---| | `34041593697` a1 (35) | `fa10d94`, the commit that added the test | `No compose hierarchies found`, thrown directly | fixed by `b23ff0f` | | `34056545386` a1 (35) | `cbbaf74` | `ComposeTimeoutException ... after 120000 ms` | fixed by `19e3539`; not a system-service failure at all | | `34161043035` a1 (35) | `9fd96d0` | `NPE: Cannot run onActivity since Activity has been destroyed already` | traced, fixed | | `34146936252` a2 (35) | `73482520` | `waited 300000ms ...; last composition error: No compose hierarchies` | the permission-dialog helper #269 replaced; **re-count** | | **`34057706195` a1 (34)** | **`19e3539`** | **`No compose hierarchies found` ×2, thrown directly** | **shape A, open** | | **`34067653670` a1 (35), `34146936252` a1 (35)** | `d45abe7`, `73482520` | **`waited 300000ms for a node tagged action.saveFile`, no composition error** | **shape B, open** | ## Shape A — `b23ff0f` did not close the "no compose hierarchies" case `b23ff0f` ("Wait for the app to come back before asking Compose about it") was written for exactly this message. `34057706195`'s head is `19e3539`, and `git merge-base --is-ancestor b23ff0f 19e3539` says it **contains** that fix — and both save tests failed on it anyway, with the exception thrown directly rather than through `awaitNode`. So either the wait is in the wrong place or there is a second way for the composition to go away. The direct throw is the clue worth starting from: `awaitNode` catches and re-labels, so a raw `IllegalStateException: No compose hierarchies found` came from a call site that is **not** behind that helper. ## Shape B — the app is alive and well, and the conversion never starts This is the more interesting one and it is not an Activity-lifecycle failure at all. `34067653670`'s per-test logcat for `aSaveWritesToTheDocumentTheSystemPickerCreated`: ``` 23:49:49.769 MainActivity RESUMED 23:49:53.514 ffmpeg-kit-next: Generated fd 141 for SAF id 14. <- pickTheFixture's probe 23:49:53.599 UiDevice: Waiting 5000ms for ... com.google.android.permissioncontroller ... 305 seconds with nothing from this app at all ... 23:54:58.684 MainActivity PAUSED / STOPPED / DESTROYED <- the Compose rule tearing down 23:54:59.152 TestRunner: failed ``` `MainActivity` is `RESUMED` for the whole 300 s, and `awaitNode` appends its `last composition error` only when `fetchSemanticsNodes` actually threw — it did not, so **the composition was readable throughout**. The app was fine. What never happened is the conversion. The green attempt of the same test on the same run (`34146936252` a3) is the control: ``` 18:01:51.949 WM-WorkerWrapper: Starting work for org.libremediaconverter.work.ConversionWorker 18:01:51.995 ConversionWorker: Routing lmc-r38-fixture.mp4 -> OutputSpec(...) via FFMPEG 18:01:51.997 FFmpegEngine: ffmpeg -hide_banner -y -i ffkitsaf:16.mp4 -c:v libx265 ... ``` **None of those three lines appears anywhere in the failing run.** So `action.saveFile` never appeared because no conversion ever ran, not because the UI was unreachable. ### A hypothesis, explicitly unproved The `Waiting 5000ms for ... permissioncontroller` immediately after the Convert tap is the permission-dialog helper as it stood before #269 — tap Convert, wait for the dialog, press back on it. `ConverterScreen` wires Convert to `requestNotifications.launch(POST_NOTIFICATIONS)` and starts the conversion from the permission result, so a back press on that dialog is a *denial*, and a denial would leave the screen exactly where this logcat leaves it. That fits, and **it is a hypothesis, not a finding** — I have not shown the dialog was up on that run. ## What to do first, and it is not a fix **Re-count over post-#269 leg-attempts before touching anything.** #269 replaced that helper with `pm grant` plus an assertion that the permission is held, so shape B's most plausible mechanism may already be gone; and shape A's two occurrences both predate #269 as well. At the time of writing there are **zero** post-#269 gating leg-attempts on `main` — the only run on #269's own head is `34161043035`, whose attempt 1 is the mode that has now been fixed separately. `docs/ci-failure-modes.md` has the counting rules: per leg-attempt, and with these tests' own denominator (API 33-36 only — they carry `@FailsOnEmulatorApi37` — and only from 2026-09-06T15:12). ## Done means Each of the two shapes either reproduced and fixed, or **re-counted to zero over a stated number of post-#269 leg-attempts and closed with that number written down**. Not "has not been seen lately".
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMediaConverter#270