Closes#58. Child 2 of 8 decomposing #52, on top of #57's internal leaves and tag table.
"Size unknown" is the line a previous stream declared untestable while fixing D5. It is two assertTextEquals calls.
What landed
app/src/test/java/org/libremediaconverter/convert/FileCardTest.kt — 13
Robolectric/Compose tests over FileCard and DetailRow: all four InputKind branches, the probe == null early return, and sizeBytes == null asserted twice, once with a probe and
once without, because the independence of the size line from the probe is the actual contract.
app/src/test/java/org/libremediaconverter/convert/ConverterFormattersTest.kt — 13
plain JVM tests over describe, formatDuration, formatBytes and EnginePreference.label().
No Compose rule and no Robolectric: they are String in, String out.
Both classes locate nodes by TestTags.Converter, never by a text matcher, so a reword cannot
redden this PR or its two siblings. Nothing in app/src/main changed — the diff is two new
test files.
Two findings the issue could not have known
1. The second acceptance criterion is not a mutation. It is a compile error.
#58 asks for "delete the return@Column at the probe-null branch -> the Reading… test
goes red". It cannot go red, because it does not build. probe is a local val, so the early
return is what smart-casts it non-null; delete it and every use below stops compiling:
e: ConverterScreen.kt:499:24 Only safe (?.) or non-null asserted (!!.) calls are allowed on a
nullable receiver of type 'InputProbe?'.
Ten of those, one per use of probe across the four kind branches.
The early exit is enforced by the type system, not by a test. That is a stronger guarantee than
the ticket assumed, and it matters for #61, which moves this code wholesale: if that refactor
hoists the probe into a non-null parameter or splits the branch into its own composable, the
compiler stops holding the line and these tests become the only thing that does.
The two compilable regressions someone would actually land in place of it are both covered
(mutations 2 and 3 below).
2. CodecNames.describeAudio has no UNPARSEABLE arm, unlike describeVideo
describeVideo(InputProbe.UNPARSEABLE) answers "Unrecognised". describeAudio has no such arm,
so it falls through to the raw-name fallback and answers the sentinel itself — "\u0000unparseable", a NUL followed by a word, which would render verbatim in a DetailRow.
Not reachable today: a file carrying the sentinel has kind == UNPARSEABLE, and that branch renders
the explanatory line instead of any row. Left alone deliberately — changing CodecNames is
outside this ticket — but #61's owner should know it is there before restructuring the branch
that currently hides it.
What is asserted instead of the divider
The probe == null branch exits before HorizontalDivider, and the divider's absence cannot be
asserted: Material 3 renders it as a Box with no semantics modifier, so it contributes no node
to the semantics tree at all.
What is asserted instead is everything the divider precedes — no detail row for any of the
six labels the four kind branches can emit — plus the card's child count, which catches
content the six-label enumeration cannot see. That second half is not a decorative extra: mutation 8
adds an untagged Text inside the branch and the count assertion is the only thing that reddens.
The class KDoc records the limitation, so it reads as a decision rather than a silent gap.
The mutations
Every assertion in both files has a named mutation. Eight were run; all eight went red, and each red
names the value it saw rather than merely failing.
#
Mutation in ConverterScreen.kt
What went red
1
?: "Size unknown" -> ?: "0 B"
both size tests — with a probe and without
2
early exit dropped, null defaulted away with ?: InputProbe()
while the probe is still running the card shows the reading note and nothing else, on the rows
3
val probe = input.probe ?: InputProbe(), if block deleted
the same test, on the note itself
4
bytes >= 1_000_000 -> bytes > 1_000_000
each unit starts exactly on its threshold rather than one byte past it
5
if (probe.width > 0) guard dropped
an image whose dimensions nothing reported gets the type row alone
6
if (probe.durationMs > 0) guard dropped — both occurrences, one per branch
the audio-only and the video degradation tests, independently
7
tracks.isEmpty() guard dropped in describe; two EnginePreference labels made identical
three ConverterFormattersTest cases
8
an untagged fourth Text added inside the probe-null branch
the child-count assertion, and nothing else
Mutation 1, verbatim:
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [Size unknown])
Semantics of the node:
Node #66 at (l=16.0, t=52.0, r=20.0, b=88.0)px, Tag: 'converter.fileCard.bytes'
Text = '[0 B]'
Both size tests produced that, one with a probe and one without, which is the independence contract
failing in both directions.
Mutation 2, verbatim. Note that #57's ConverterLeafTagsTest stays green through it —
that is what makes this PR's assertions the thing doing the work rather than the tag smoke test:
java.lang.AssertionError: Failed: assertDoesNotExist.
Reason: Did not expect any node but found '1' node that satisfies: (TestTag = 'converter.fileCard.row:Container')
Node found:
Node #120 at (l=16.0, t=141.0, r=35.0, b=177.0)px, Tag: 'converter.fileCard.row:Container'
Text = '[Container: Unknown]'
Mutation 3, verbatim:
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [Reading…])
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.fileCard.note')
Mutation 8, verbatim:
java.lang.AssertionError: Failed to assert count of nodes.
Reason: Expected '3' nodes but found '4' nodes that satisfy: ((TestTag = 'converter.fileCard').children)
Mutations 5 and 6 are why each > 0 guard has a case on both sides. The present side alone
stays green when the guard is deleted; what the deletion produces is Size: 0×0 and Length: 0:00 — invented measurements, the same defect class as "0 B":
Reason: Did not expect any node but found '1' node that satisfies: (TestTag = 'converter.fileCard.row:Size')
Text = '[Size: 0×0]'
Mutation 4, verbatim — the reason formatBytes is pinned at each threshold and one
byte below it, since only a value sitting exactly on the boundary can tell >= from >:
org.junit.ComparisonFailure: expected:<1[.0 M]B> but was:<1[000 k]B>
Notes for the two sibling children
EnginePreference.label() is asserted directly here, since #58 lists it as one of the four pure
helpers. #59 covering it again through EnginePicker's chips is a different bite — rendered
versus returned — and the two are in different files, so they do not collide.
createDrainedComposeRule() from #57 is used throughout, never createComposeRule().
onNodeWithTag(FILE_CARD).onChildren() resolves to exactly the Text nodes: the Card does not
merge its descendants and the Column flattens away. Useful if #59 or #60 wants the same
"and nothing else" shape.
Gate
:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue — green. detekt stays at 0 findings; lintDebug
reports 0 errors, 0 warnings and the one pre-existing UsableSpace hint in OutputPublisher.kt.
Closes #58. Child 2 of 8 decomposing #52, on top of #57's `internal` leaves and tag table.
`"Size unknown"` is the line a previous stream declared untestable while fixing D5. It is two
`assertTextEquals` calls.
## What landed
- **`app/src/test/java/org/libremediaconverter/convert/FileCardTest.kt`** — 13
Robolectric/Compose tests over `FileCard` and `DetailRow`: all four `InputKind` branches, the
`probe == null` early return, and `sizeBytes == null` asserted **twice**, once with a probe and
once without, because the independence of the size line from the probe is the actual contract.
- **`app/src/test/java/org/libremediaconverter/convert/ConverterFormattersTest.kt`** — 13
plain JVM tests over `describe`, `formatDuration`, `formatBytes` and `EnginePreference.label()`.
No Compose rule and no Robolectric: they are `String` in, `String` out.
Both classes locate nodes by `TestTags.Converter`, never by a text matcher, so a reword cannot
redden this PR or its two siblings. Nothing in `app/src/main` changed — the diff is two new
test files.
## Two findings the issue could not have known
### 1. The second acceptance criterion is not a mutation. It is a compile error.
#58 asks for "delete the `return@Column` at the probe-null branch -> the `Reading…` test
goes red". It cannot go red, because it does not build. `probe` is a local `val`, so the early
return is what smart-casts it non-null; delete it and every use below stops compiling:
```
e: ConverterScreen.kt:499:24 Only safe (?.) or non-null asserted (!!.) calls are allowed on a
nullable receiver of type 'InputProbe?'.
```
Ten of those, one per use of `probe` across the four kind branches.
**The early exit is enforced by the type system, not by a test.** That is a stronger guarantee than
the ticket assumed, and it matters for **#61**, which moves this code wholesale: if that refactor
hoists the probe into a non-null parameter or splits the branch into its own composable, the
compiler stops holding the line and these tests become the only thing that does.
The two compilable regressions someone would actually land in place of it are both covered
(mutations 2 and 3 below).
### 2. `CodecNames.describeAudio` has no `UNPARSEABLE` arm, unlike `describeVideo`
`describeVideo(InputProbe.UNPARSEABLE)` answers `"Unrecognised"`. `describeAudio` has no such arm,
so it falls through to the raw-name fallback and answers the sentinel itself —
`"\u0000unparseable"`, a NUL followed by a word, which would render verbatim in a `DetailRow`.
Not reachable today: a file carrying the sentinel has `kind == UNPARSEABLE`, and that branch renders
the explanatory line instead of any row. Left alone deliberately — changing `CodecNames` is
outside this ticket — but #61's owner should know it is there before restructuring the branch
that currently hides it.
## What is asserted instead of the divider
The `probe == null` branch exits before `HorizontalDivider`, and **the divider's absence cannot be
asserted**: Material 3 renders it as a `Box` with no semantics modifier, so it contributes no node
to the semantics tree at all.
What is asserted instead is everything the divider precedes — no detail row for any of the
six labels the four kind branches can emit — plus the card's child count, which catches
content the six-label enumeration cannot see. That second half is not a decorative extra: mutation 8
adds an untagged `Text` inside the branch and the count assertion is the only thing that reddens.
The class KDoc records the limitation, so it reads as a decision rather than a silent gap.
## The mutations
Every assertion in both files has a named mutation. Eight were run; all eight went red, and each red
names the value it saw rather than merely failing.
| # | Mutation in `ConverterScreen.kt` | What went red |
|---|---|---|
| 1 | `?: "Size unknown"` -> `?: "0 B"` | **both** size tests — with a probe and without |
| 2 | early exit dropped, null defaulted away with `?: InputProbe()` | `while the probe is still running the card shows the reading note and nothing else`, on the rows |
| 3 | `val probe = input.probe ?: InputProbe()`, `if` block deleted | the same test, on the note itself |
| 4 | `bytes >= 1_000_000` -> `bytes > 1_000_000` | `each unit starts exactly on its threshold rather than one byte past it` |
| 5 | `if (probe.width > 0)` guard dropped | `an image whose dimensions nothing reported gets the type row alone` |
| 6 | `if (probe.durationMs > 0)` guard dropped — **both** occurrences, one per branch | the audio-only and the video degradation tests, independently |
| 7 | `tracks.isEmpty()` guard dropped in `describe`; two `EnginePreference` labels made identical | three `ConverterFormattersTest` cases |
| 8 | an untagged fourth `Text` added inside the probe-null branch | the child-count assertion, and nothing else |
**Mutation 1**, verbatim:
```
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [Size unknown])
Semantics of the node:
Node #66 at (l=16.0, t=52.0, r=20.0, b=88.0)px, Tag: 'converter.fileCard.bytes'
Text = '[0 B]'
```
Both size tests produced that, one with a probe and one without, which is the independence contract
failing in both directions.
**Mutation 2**, verbatim. Note that #57's `ConverterLeafTagsTest` stays **green** through it —
that is what makes this PR's assertions the thing doing the work rather than the tag smoke test:
```
java.lang.AssertionError: Failed: assertDoesNotExist.
Reason: Did not expect any node but found '1' node that satisfies: (TestTag = 'converter.fileCard.row:Container')
Node found:
Node #120 at (l=16.0, t=141.0, r=35.0, b=177.0)px, Tag: 'converter.fileCard.row:Container'
Text = '[Container: Unknown]'
```
**Mutation 3**, verbatim:
```
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [Reading…])
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'converter.fileCard.note')
```
**Mutation 8**, verbatim:
```
java.lang.AssertionError: Failed to assert count of nodes.
Reason: Expected '3' nodes but found '4' nodes that satisfy: ((TestTag = 'converter.fileCard').children)
```
**Mutations 5 and 6** are why each `> 0` guard has a case on **both** sides. The present side alone
stays green when the guard is deleted; what the deletion produces is `Size: 0×0` and
`Length: 0:00` — invented measurements, the same defect class as `"0 B"`:
```
Reason: Did not expect any node but found '1' node that satisfies: (TestTag = 'converter.fileCard.row:Size')
Text = '[Size: 0×0]'
```
**Mutation 4**, verbatim — the reason `formatBytes` is pinned at each threshold *and* one
byte below it, since only a value sitting exactly on the boundary can tell `>=` from `>`:
```
org.junit.ComparisonFailure: expected:<1[.0 M]B> but was:<1[000 k]B>
```
## Notes for the two sibling children
- `EnginePreference.label()` is asserted directly here, since #58 lists it as one of the four pure
helpers. #59 covering it again through `EnginePicker`'s chips is a different bite — rendered
versus returned — and the two are in different files, so they do not collide.
- `createDrainedComposeRule()` from #57 is used throughout, never `createComposeRule()`.
- `onNodeWithTag(FILE_CARD).onChildren()` resolves to exactly the `Text` nodes: the `Card` does not
merge its descendants and the `Column` flattens away. Useful if #59 or #60 wants the same
"and nothing else" shape.
## Gate
`:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck
:app:detekt :app:lintDebug --continue` — green. detekt stays at 0 findings; `lintDebug`
reports 0 errors, 0 warnings and the one pre-existing `UsableSpace` hint in `OutputPublisher.kt`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #58. Child 2 of 8 decomposing #52, on top of #57's
internalleaves and tag table."Size unknown"is the line a previous stream declared untestable while fixing D5. It is twoassertTextEqualscalls.What landed
app/src/test/java/org/libremediaconverter/convert/FileCardTest.kt— 13Robolectric/Compose tests over
FileCardandDetailRow: all fourInputKindbranches, theprobe == nullearly return, andsizeBytes == nullasserted twice, once with a probe andonce without, because the independence of the size line from the probe is the actual contract.
app/src/test/java/org/libremediaconverter/convert/ConverterFormattersTest.kt— 13plain JVM tests over
describe,formatDuration,formatBytesandEnginePreference.label().No Compose rule and no Robolectric: they are
Stringin,Stringout.Both classes locate nodes by
TestTags.Converter, never by a text matcher, so a reword cannotredden this PR or its two siblings. Nothing in
app/src/mainchanged — the diff is two newtest files.
Two findings the issue could not have known
1. The second acceptance criterion is not a mutation. It is a compile error.
#58 asks for "delete the
return@Columnat the probe-null branch -> theReading…testgoes red". It cannot go red, because it does not build.
probeis a localval, so the earlyreturn is what smart-casts it non-null; delete it and every use below stops compiling:
Ten of those, one per use of
probeacross the four kind branches.The early exit is enforced by the type system, not by a test. That is a stronger guarantee than
the ticket assumed, and it matters for #61, which moves this code wholesale: if that refactor
hoists the probe into a non-null parameter or splits the branch into its own composable, the
compiler stops holding the line and these tests become the only thing that does.
The two compilable regressions someone would actually land in place of it are both covered
(mutations 2 and 3 below).
2.
CodecNames.describeAudiohas noUNPARSEABLEarm, unlikedescribeVideodescribeVideo(InputProbe.UNPARSEABLE)answers"Unrecognised".describeAudiohas no such arm,so it falls through to the raw-name fallback and answers the sentinel itself —
"\u0000unparseable", a NUL followed by a word, which would render verbatim in aDetailRow.Not reachable today: a file carrying the sentinel has
kind == UNPARSEABLE, and that branch rendersthe explanatory line instead of any row. Left alone deliberately — changing
CodecNamesisoutside this ticket — but #61's owner should know it is there before restructuring the branch
that currently hides it.
What is asserted instead of the divider
The
probe == nullbranch exits beforeHorizontalDivider, and the divider's absence cannot beasserted: Material 3 renders it as a
Boxwith no semantics modifier, so it contributes no nodeto the semantics tree at all.
What is asserted instead is everything the divider precedes — no detail row for any of the
six labels the four kind branches can emit — plus the card's child count, which catches
content the six-label enumeration cannot see. That second half is not a decorative extra: mutation 8
adds an untagged
Textinside the branch and the count assertion is the only thing that reddens.The class KDoc records the limitation, so it reads as a decision rather than a silent gap.
The mutations
Every assertion in both files has a named mutation. Eight were run; all eight went red, and each red
names the value it saw rather than merely failing.
ConverterScreen.kt?: "Size unknown"->?: "0 B"?: InputProbe()while the probe is still running the card shows the reading note and nothing else, on the rowsval probe = input.probe ?: InputProbe(),ifblock deletedbytes >= 1_000_000->bytes > 1_000_000each unit starts exactly on its threshold rather than one byte past itif (probe.width > 0)guard droppedan image whose dimensions nothing reported gets the type row aloneif (probe.durationMs > 0)guard dropped — both occurrences, one per branchtracks.isEmpty()guard dropped indescribe; twoEnginePreferencelabels made identicalConverterFormattersTestcasesTextadded inside the probe-null branchMutation 1, verbatim:
Both size tests produced that, one with a probe and one without, which is the independence contract
failing in both directions.
Mutation 2, verbatim. Note that #57's
ConverterLeafTagsTeststays green through it —that is what makes this PR's assertions the thing doing the work rather than the tag smoke test:
Mutation 3, verbatim:
Mutation 8, verbatim:
Mutations 5 and 6 are why each
> 0guard has a case on both sides. The present side alonestays green when the guard is deleted; what the deletion produces is
Size: 0×0andLength: 0:00— invented measurements, the same defect class as"0 B":Mutation 4, verbatim — the reason
formatBytesis pinned at each threshold and onebyte below it, since only a value sitting exactly on the boundary can tell
>=from>:Notes for the two sibling children
EnginePreference.label()is asserted directly here, since #58 lists it as one of the four purehelpers. #59 covering it again through
EnginePicker's chips is a different bite — renderedversus returned — and the two are in different files, so they do not collide.
createDrainedComposeRule()from #57 is used throughout, nevercreateComposeRule().onNodeWithTag(FILE_CARD).onChildren()resolves to exactly theTextnodes: theCarddoes notmerge its descendants and the
Columnflattens away. Useful if #59 or #60 wants the same"and nothing else" shape.
Gate
:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue— green. detekt stays at 0 findings;lintDebugreports 0 errors, 0 warnings and the one pre-existing
UsableSpacehint inOutputPublisher.kt.