Cut a seam through the codec enumeration, and say what a failed one actually does #210

Merged
JMR-dev merged 4 commits from test/device-codec-enumeration-seam into main 2026-09-06 00:59:59 +00:00
JMR-dev commented 2026-09-02 23:16:13 +00:00 (Migrated from github.com)

Closes #194. Touches production, unlike the four test-only PRs in this wave.

This is not a third run of the #86/#133 spike

probe() was 20 never-executed lines, the biggest single block on the report. It has been looked at twice and left out twice, and both closes were right about what they closed:

  • #86 ruled it device-bound.
  • #133 re-checked with ShadowMediaCodecList in hand and still declined, because MediaCodecInfoBuilder has no setIsAlias and no setCanonicalName — "so the alias skip and the canonical-name dedup, the two things the class's KDoc calls out as easy to get wrong, are not reachable through it."

That objection is about the shadow. It does not apply to a function taking its own entry type, which #133 did not evaluate. capabilitiesFrom(enumerate: () -> Sequence<CodecEntry>) holds every rule; the edge keeps only the mapping from MediaCodecList onto CodecEntry.

Why the parameter is a Sequence

runCatching has always wrapped the iteration, so a MediaCodecInfo whose properties throw partway leaves the codecs already read in place. A List parameter would move that throw outside the loop and turn a partial answer into an empty one — a behaviour change smuggled in as a refactor. There is now a test for the partial case, and swapping the Sequence for an eager toList() reddens it.

The one behaviour change, which is a log line

The fallback said "Codec enumeration failed; assuming permissive" and returned empty sets. But "video/avc" in emptySet() is false, so canEncode and canDecode answer no to everything and every job routes to FFmpeg. That is the restrictive answer — and the right one, since FFmpeg does whatever Media3 does, only slower.

Per the decision on #194: the code stays, the message changes. The class KDoc gains a third bullet saying so, since it implied the opposite.

One test was wrong, and the mutation is what said so

The alias case originally listed the alias after the codec it aliases, and passed with the skip deleted — canonicalName is shared, so the dedup catches the second entry either way. The two rules overlap, and a fixture that does not separate them tests neither.

Order separates them. An alias arriving first claims the canonical name in seen, has its own supportedTypes credited, and the real codec is then dropped by the dedup. That is now the test — and it also pins what the rule is worth: with a Set accumulator an alias declaring the same types changes nothing, so the skip earns its place only when the two disagree.

Acceptance: seven mutations, each run and restored

mutation red
drop !isSoftwareOnly from the encoder predicate 1
remove the alias skip 1 — 0 before the fixture was fixed
remove the canonical-name dedup 1
remove the video/ prefix filter 1
apply the hardware predicate to decoders too 1
make the failure fallback permissive 2
eager toList() instead of the lazy Sequence 1

Each reddens the test that owns it, so no test is riding on another's rule.

Verification

assembleDebug + testDebugUnitTest (full suite) + compileDebugAndroidTestKotlin + ktlintCheck + detekt + lintDebug — green. Two formatting complaints came up and were fixed with ktlintFormat, not by hand.

🤖 Generated with Claude Code

Closes #194. **Touches production**, unlike the four test-only PRs in this wave. ## This is not a third run of the #86/#133 spike `probe()` was 20 never-executed lines, the biggest single block on the report. It has been looked at twice and left out twice, and **both closes were right about what they closed**: - **#86** ruled it device-bound. - **#133** re-checked with `ShadowMediaCodecList` in hand and still declined, because `MediaCodecInfoBuilder` has no `setIsAlias` and no `setCanonicalName` — *"so the alias skip and the canonical-name dedup, the two things the class's KDoc calls out as easy to get wrong, are not reachable through it."* That objection is about **the shadow**. It does not apply to a function taking its own entry type, which #133 did not evaluate. `capabilitiesFrom(enumerate: () -> Sequence<CodecEntry>)` holds every rule; the edge keeps only the mapping from `MediaCodecList` onto `CodecEntry`. ## Why the parameter is a `Sequence` `runCatching` has always wrapped the **iteration**, so a `MediaCodecInfo` whose properties throw partway leaves the codecs already read in place. A `List` parameter would move that throw outside the loop and turn a partial answer into an empty one — a behaviour change smuggled in as a refactor. There is now a test for the partial case, and swapping the `Sequence` for an eager `toList()` reddens it. ## The one behaviour change, which is a log line The fallback said *"Codec enumeration failed; assuming permissive"* and returned empty sets. But `"video/avc" in emptySet()` is `false`, so `canEncode` and `canDecode` answer **no** to everything and every job routes to FFmpeg. That is the *restrictive* answer — and the right one, since FFmpeg does whatever Media3 does, only slower. Per the decision on #194: **the code stays, the message changes.** The class KDoc gains a third bullet saying so, since it implied the opposite. ## One test was wrong, and the mutation is what said so The alias case originally listed the alias **after** the codec it aliases, and **passed with the skip deleted** — `canonicalName` is shared, so the dedup catches the second entry either way. The two rules overlap, and a fixture that does not separate them tests neither. **Order separates them.** An alias arriving *first* claims the canonical name in `seen`, has its own `supportedTypes` credited, and the real codec is then dropped by the dedup. That is now the test — and it also pins what the rule is worth: with a `Set` accumulator an alias declaring the *same* types changes nothing, so the skip earns its place only when the two disagree. ## Acceptance: seven mutations, each run and restored | mutation | red | |---|---| | drop `!isSoftwareOnly` from the encoder predicate | 1 | | remove the alias skip | 1 — **0 before the fixture was fixed** | | remove the canonical-name dedup | 1 | | remove the `video/` prefix filter | 1 | | apply the hardware predicate to decoders too | 1 | | make the failure fallback permissive | 2 | | eager `toList()` instead of the lazy `Sequence` | 1 | Each reddens the test that owns it, so no test is riding on another's rule. ## Verification `assembleDebug` + `testDebugUnitTest` (full suite) + `compileDebugAndroidTestKotlin` + `ktlintCheck` + `detekt` + `lintDebug` — green. Two formatting complaints came up and were fixed with `ktlintFormat`, not by hand. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.