Four coverage waves have been steered by JaCoCo, which measures testDebugUnitTest only and cannot see app/src/androidTest at all. So nothing had ever asked what the 60 device tests pin, only that they were green. This is that read: a triage, not a test push, in the shape of docs/coverage-read-findings.md. Six findings (E1-E6) are things a test would not fix, and five of those six are prose rather than code — the suite itself is in good condition. Eight tickets carry the rest (#223-#230), each naming the mutation that has to go red rather than a coverage delta. The one that matters is #223. HardwareFallbackTest is the only automated check of the hardware->software fallback against a real codec failure, and it has never attempted the hardware path. Measured on run 34004304566: the API 33, 34, 35 and 37 legs each log Routing sample_h264_444.mp4 -> ... via FFMPEG (NO_HARDWARE_ENCODER) because emulators expose no hardware encoder, so the router sends the job straight to FFmpeg and runMedia3OrFallBack's catch is never entered. Its two assertions — succeeded, output non-empty — are true anyway. It finishes in 448 ms, which is not long enough to fail a hardware export and then software-encode a three-second clip. Deleting that catch reddens nothing anywhere. Two things generalise. A test can assert and still not reach, which neither a coverage number nor a "does it assert something" review can see; the filter that works is whether the test's premise holds on the machine running it. And the codebase already knew — ForcedFailureTest pins DeviceCodecs.PERMISSIVE against this exact hazard and says why, as does ConversionWorkerTest. Their assertions are about the path, so without the pin they fail loudly; HardwareFallbackTest's are about the output, so it passes quietly. That asymmetry is why nobody noticed. E5 records the structural reason this document is separate: F7 in the coverage findings calls probeWithExtractor's catch uncovered when RemuxTest drives it on a device every leg. A JaCoCo-derived document cannot see androidTest, so it will keep re-deriving that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
18 KiB
E2E-read findings
Status: six findings, none fixed, none urgent — plus one confirmed vacuous test, which is a
ticket rather than an entry here (see Not covered here). E1–E6 came from
the 2026-09-05 read of the instrumented suite. Every entry here is a test-suite observation —
something a new test would not fix, because the test already exists and the problem is what it
claims rather than what it runs.
Scope: what reading all 60 instrumented tests turned up that writing a 61st would not fix.
Last verified: main at 4b02294, 2026-09-05. 60 @Test methods in 12 classes, three
carrying @FailsOnEmulatorApi37, gating API 37 leg 57.
Why this document exists, and why it is separate from the other two
docs/coverage-read-findings.md (F1–F10) came from reading a JaCoCo report, and JaCoCo
measures testDebugUnitTest only. So four waves of coverage work have been shaped by a number that
cannot see app/src/androidTest at all. The instrumented suite has never had the equivalent
read: nothing has asked what those 60 tests actually pin, only that they are green.
That is the gap this read is in. It is a triage, not a test push — the same shape as wave 4's read, which "moved no number at all, and that is its result".
docs/defect-audit.md (D1–D16) is the record of things wrong at runtime. Nothing here is
wrong at runtime. These are tests whose names, KDoc or reputation overstate what they execute.
Entry ids are E1–E6 so they cannot be confused with F1–F10 or D1–D16.
How to read the confidence labels
Same vocabulary as the other two documents, deliberately:
- Confirmed by inspection — the control flow is fully readable and the finding follows from it.
- Confirmed by measurement — observed in a CI artifact, with the run id recorded.
- No action — recorded because it looks like a finding and is not.
The method, and the one filter that found everything
A coverage number is useless here by construction, so the read used a different question, applied to every one of the 60 tests:
If the behaviour this test is named for stopped working, would it go red?
Three answers, and only the third is a gap:
- yes — the test bites. Most of the suite.
- no, and that is deliberate and written down —
RealMediaBenchmarkasserts nothing on purpose (E2);transcodesH264ToH265AndReportsProgressdeclines to assert progress for a stated reason (E3). These are entries here, not tickets. - no, and nothing says so — the gap. One test, and it is the most important one in the suite.
The reusable part is the second filter, because "does it assert something?" would have cleared the vacuous test — it asserts two things. What it does not do is reach the code it names:
Does the test's own premise hold on the machine that runs it?
HardwareFallbackTest asserts SUCCEEDED and a non-empty output, and both are true of a
conversion that never went near the path it exists to prove (#223). See
Not covered here; it is filed rather than recorded here because a test fixes it.
E1 — RemuxTest's class KDoc argues for engine assertions three of its tests do not make, and they are right not to
Severity: low · Confirmed by inspection · the KDoc is what is wrong, not the tests
app/src/androidTest/java/org/libremediaconverter/convert/RemuxTest.kt:31-42
The class KDoc is headed "Why these assert the engine, not just the file" and makes a specific argument:
A remux routed to FFmpeg produces a perfectly correct file —
-c copymoves the same samples into the same container. So an output-only assertion passes whether the hardware transmux path ran or never executed at all […] which makes "silently always FFmpeg" the most likely way for this feature to regress.
Five of its seven tests run a conversion. Three assert no engine at all:
| test | output container | asserts engine? |
|---|---|---|
mkvToMp4RemuxesOnHardware |
MP4 | yes — MEDIA3 |
mp4ToMkvRemuxesOnFFmpeg |
MKV | yes — FFMPEG |
webmToMkvKeepsVp9WithoutReencoding |
MKV | no |
audioOnlySourceRemuxesIntoMka |
MKV (.mka) |
no |
mp4ToMpegTsAndAviProduceTheirOwnContainers |
MPEG-TS, then AVI | TS only; the AVI half does not |
Why this is not a gap
ConversionRouter.MEDIA3_CONTAINERS = setOf(Container.MP4) (ConversionRouter.kt:37), and every
one of the three produces MKV or AVI. They can only ever be FFmpeg, so the regression the KDoc
names — "silently always FFmpeg" — is not a thing that can happen to them. The two tests where the
hardware path is genuinely at risk are exactly the two that assert it.
An engine assertion on the other three would be near-tautological given today's router. It would
catch one thing: somebody adding MKV or AVI to MEDIA3_CONTAINERS without a muxer to match — which
is what Media3MuxersTest is for, on the JVM, where it does not need a device.
Why it is recorded rather than dropped
This was the strongest-looking candidate of the whole read and it dissolved on tracing, which
is the same shape as F5 in the coverage document (filed as a test gap, and only stopped being one
when someone went looking for its callers). Recorded so the next read does not re-file it.
The fix is one line of KDoc, not three tests: the class asserts the engine where the engine is in doubt, which is a better rule than the one it currently states.
E2 — three of the 60 instrumented tests assert nothing, and two of them never run
Severity: n/a · No action — deliberate, documented, and load-bearing as documentation
app/src/androidTest/java/org/libremediaconverter/bench/RealMediaBenchmark.kt:25-53
reportDeviceEncoderCapabilities logs and asserts nothing. hardwareVersusSoftwareOnRealVideo and
av1InputRoutesAccordingToDeviceDecodeSupport are assumeTrue-guarded on media that is not
committed and must be staged by hand into the app's internal filesDir, so they skip in every
automated run — they are the "2 skipped" every green leg reports, and docs/local-emulator.md:305
says so.
The class KDoc is unambiguous: "This is a benchmark, not part of the automated suite […] Not a correctness test — the assertions are deliberately loose."
No action. Recorded for one reason: the suite's headline number is 60, and three of those 60 are not tests. Any future statement of the form "60 instrumented tests cover X" is off by three, and two of the three have never executed on CI at all.
It is the opposite of E-nothing, though — reportDeviceEncoderCapabilities runs on every leg
and logs BENCH can-encode:, and that log line is what confirmed the vacuous test this read
found (#223). An assertion-free test that prints the machine's capabilities turned out to be
the only oracle in the suite. See Not covered here.
E3 — transcodesH264ToH265AndReportsProgress does not assert that progress was reported
Severity: low · No action on the test; the name is the inaccurate part
app/src/androidTest/java/org/libremediaconverter/convert/Media3EngineTest.kt:73, :90-93
// Deliberately NOT asserting that progress fired. Polling is on a 250 ms tick,
// and a 3 s 320x240 clip can finish inside one tick on fast hardware, which
// would make the assertion fail intermittently for no real defect.
seen.forEach { assertTrue("progress out of range: $it", it in 0..100) }
seen is empty-safe: forEach on an empty list asserts nothing, so replacing onProgress with a
no-op reddens nothing here. The reasoning is sound and the alternative really is a flaky test.
No action on the body. The name says AndReportsProgress and the body says it does not check
that, which is the probeForConcat shape from CLAUDE.md — a passing test with a wrong
explanation is its own failure mode — in its mildest form, since here the KDoc immediately corrects
the name.
Contrast the FFmpeg side, which is a real gap and is filed as #229: FFmpegEngine's percentage
arithmetic is executed by every FFmpeg test and observed by none, because every call site omits
onProgress entirely. Media3's is unasserted; FFmpeg's is unobserved. Only the second is a ticket.
E4 — the marker's KDoc says removing it grows the gating leg by two; three tests carry it
Severity: low · Confirmed by inspection · one line
app/src/androidTest/java/org/libremediaconverter/FailsOnEmulatorApi37.kt:20
Delete the annotation from the tests, and the advisory job goes empty and the gating one grows by two.
Three tests carry it — Media3EngineTest:72, Media3EngineTest:135, SafPickerRoundTripTest:320 —
and FAILS_ON_EMULATOR_API37_BASELINE = 3 eleven lines further down the same file, where the count
is machine-checked by .github/scripts/e2e-report-shape.sh.
The third marker was added when the SAF rotation test was excluded; the sentence was not updated with it. Everything that is checked is consistent at three; only the prose says two, which is exactly why it drifted — and a good argument for the baseline const being a const.
E5 — coverage-read-findings.md's F7 calls covered code uncovered
Severity: low · Confirmed by inspection · half of F7 is stale
F7 says probeWithExtractor's catch (MediaProbe.kt:180-182) is unreachable on Robolectric and
"stays device-only", measured across four URI shapes. The unreachability claim is correct and
stands. The implication readers take from it — that nothing exercises it — does not:
app/src/androidTest/java/org/libremediaconverter/convert/RemuxTest.kt:111
probeDistinguishesAudioFromImagesFromRubbish feeds it a file of random bytes and asserts
InputKind.UNPARSEABLE, on a device, on every gating leg.
"Device-only" holds; "uncovered" does not — and the difference matters, because F7 is one of the six entries that document calls "no action", on the grounds that a test would not help. A test already exists. The entry should say so.
This is the failure mode the split between the two documents was meant to prevent, and it caught
this repo out: a JaCoCo-derived document cannot see androidTest, so it will keep re-deriving
"uncovered" for anything the instrumented suite covers. That is a structural reason for this
document to exist, not a one-off correction.
E6 — the suite's one device-capability assertion derives its expectation from the call it is testing
Severity: low · Confirmed by inspection · no independent oracle exists
app/src/androidTest/java/org/libremediaconverter/work/ConversionWorkerTest.kt:151-152
val hasHardwareHevc = AndroidDeviceCodecs.get().canEncode(VideoCodec.H265)
and then the expectation is if (hasHardwareHevc) MEDIA3 else FFMPEG. The test asks
AndroidDeviceCodecs what to expect and then checks that the router agreed with
AndroidDeviceCodecs. If the whole enumeration returned empty, this would still pass — and
empty is precisely what the runCatching fallback returns (the reason #194 was worth cutting;
it logs "assuming permissive" while making canEncode answer no for everything).
Its KDoc defends the choice, and the defence is good:
Asserting MEDIA3 unconditionally tests the test machine, not the router.
That is true, and there is no third source of truth on a device: MediaCodecList is what
AndroidDeviceCodecs reads, so any oracle built from it is the same oracle.
No action, but read it with #223. It is the same missing oracle that makes the vacuous-test fix a judgement call rather than a one-liner — you cannot assert "this device has hardware HEVC" from inside the suite without asking the class under test. The honest options are a visible skip or a red test, and that decision is the ticket's.
Summary
| ID | Finding | Severity | Evidence | Action |
|---|---|---|---|---|
| E1 | RemuxTest's KDoc claims engine assertions three of its tests correctly omit |
low | confirmed by inspection; traced through MEDIA3_CONTAINERS |
one line of KDoc — the tests are right |
| E2 | Three of the 60 instrumented tests assert nothing; two never run | n/a | confirmed by inspection; docs/local-emulator.md:305 |
no action — deliberate; but 60 ≠ 60 |
| E3 | …AndReportsProgress does not assert progress fired |
low | confirmed by inspection; reason inline | no action — the name overstates, the KDoc corrects it |
| E4 | The API 37 marker's KDoc says "two"; three tests carry it | low | confirmed by inspection; baseline const says 3 | fix the sentence |
| E5 | coverage-read-findings.md F7's "uncovered" half is stale |
low | confirmed by inspection; RemuxTest.kt:111 drives it |
amend F7 — "device-only" stands, "uncovered" does not |
| E6 | The device-capability assertion asks the class under test what to expect | low | confirmed by inspection; no third oracle exists on a device | no action — read with #223 |
Five of the six are prose, not code, and that is the shape of this read. The instrumented suite is in good condition: 57 of its 60 tests bite, the fixtures are committed with their generation recipes, and the one class that asserts nothing says so in its first line. What this read found is that the suite's self-description has drifted from the suite in five small places and one large one.
The large one is not in this table, because a test fixes it: #223.
Not covered here
The vacuous test. HardwareFallbackTest.aFileMedia3CannotDecodeStillConvertsViaFfmpeg passes on
every CI leg without ever entering the fallback it exists to prove. It is #223, not an entry
here, because a test fixes it — and it is the reason this read happened rather than an aside from it.
Measured, not inferred, on run 34004304566 (all legs green), from each leg's own
e2e-diagnostics-api* logcat:
I/AndroidDeviceCodecs: Hardware video encoders: []
I/RealMediaBenchmark: BENCH can-encode: COPY=true, H264=false, H265=false, VP9=false, VP8=false, AV1=false
I/ConversionWorker: Routing sample_h264_444.mp4 -> OutputSpec(container=MP4, videoCodec=H265,
audioCodec=AAC) via FFMPEG (NO_HARDWARE_ENCODER)
Identical on API 33, 34, 35 and 37. (API 36's logcat artifact on that run is truncated to 838 KB
and carries no test output at all, so it is unread rather than different.) The job is routed
straight to FFmpeg before Media3 is attempted, the catch in runMedia3OrFallBack is never
entered, and the test's two assertions — SUCCEEDED, output non-empty — are true anyway. It ran in
448 ms.
The repository already knew. ForcedFailureTest.hardwareFailureFallsBackToSoftware, in the same
package, pins ConversionDependencies.deviceCodecs = { DeviceCodecs.PERMISSIVE } and says why:
most emulators expose no hardware video encoder at all -- so the router would legitimately send the job straight to FFmpeg and the hardware path would never be attempted. Without this the test passes on a Pixel and fails on every emulator, which says nothing about the code under test.
ConversionWorkerTest.routesAFastMp4JobByDeviceCapability records the same fact a third time. The
knowledge is in two sibling files; HardwareFallbackTest is the one that walked into it — and
because its assertions are about the output rather than the path, it passes where
ForcedFailureTest would have failed. That asymmetry is why nobody noticed.
State it precisely. The fallback wiring is covered on every leg by ForcedFailureTest, with
fakes. What has never run on any emulator is a fallback triggered by a real mid-export codec
failure — which is the case HardwareFallbackTest exists for, and the only reason
sample_h264_444.mp4 is committed at all. That fixture, generated with x264 because Fedora's
ffmpeg ships openh264 and cannot produce High 4:4:4, does nothing on any CI leg today.
The fix is not one assertion. KEY_ENGINE_USED is FFMPEG whether the fallback fired or the
router went straight there — asserting it changes nothing. The vacuity guard is two facts
together: the router chose MEDIA3 for this request on this device, and the worker reported
FFMPEG. Whether to reach that with assumeTrue (a visible skip on emulators, and the "2 skipped"
becomes 3) or with an assertion (red on emulators, announcing it cannot test what it claims) is a
decision, not a detail — see E6 for why no third option exists — and #223 leaves it open.
The other e2e gaps this read found are tickets too, and are not repeated here:
| # | Gap |
|---|---|
| #223 | HardwareFallbackTest never attempts the hardware path on any emulator leg |
| #224 | Cancelling a running native session, in any of the three engines |
| #225 | No content:// input has reached a successful conversion — the ffkitsaf bridge |
| #226 | OutputPublisher.publish against a real DocumentsProvider, and the SAF premise it rests on |
| #227 | The notification's Cancel action has never been fired |
| #228 | encodesFlacLosslessAudio and encodesOpus pass on any non-empty file |
| #229 | FFmpeg's progress percentage is computed everywhere and asserted nowhere |
| #230 | (spike) whether a running conversion's process can be killed under instrumentation |
Nothing here was filed as a coverage delta. Each names the mutation that has to go red, which is the acceptance criterion wave 4 established and which caught two vacuous tests in that wave before they shipped. #223 is the one that shows why the criterion matters: it has two passing assertions and still tests nothing.