Compare commits

...
Author SHA1 Message Date
JMR-dev 3d55004286 Count the Robolectric tests, which JaCoCo has never counted
The three #52 test PRs landed 56 new tests and the coverage figure moved 29.8% -> 29.7%.
That looked like the tests being worthless. It was the measurement.

Robolectric loads every class it touches through its own sandbox classloader, and those
classes arrive with no source location. JaCoCo skips no-location classes unless told
otherwise, and nothing here told it. So not one Robolectric test has ever contributed
coverage in this repo -- and Robolectric is what exercises the framework edge: both
workers, the publisher, both ViewModels, every Compose screen.

Same commit, same 335 tests, same 0 failures, only the block below added:

  LINE    652/2194  29.7%  ->  1519/2194  69.2%
  BRANCH  425/1424  29.8%  ->   758/1424  53.2%

  OutputPublisher       0.0% -> 97.5%      MainActivityKt     6.8% -> 86.4%
  ConversionViewModel   0.0% -> 85.4%      ConverterScreenKt  6.6% -> 62.8%

The discriminator, so this is not cargo cult: inside ConverterScreenKt, `describe` is the
one non-Composable and is exercised by a plain JVM test. It reported 8/8 covered while
every @Composable in the same class reported 0 -- including ones whose mutations
demonstrably failed the build when reverted. Across files the split is exactly
Robolectric-vs-not: StagingSweep, tested purely, 100%; OutputPublisher, ConversionViewModel
and FailureOutcome, tested under Robolectric, 0%.

`excludes = listOf("jdk.internal.*")` is not decoration. Without it JaCoCo walks
JDK-internal classes Robolectric has no location for either and the test JVM dies rather
than reporting a number.

CLAUDE.md's coverage bullet is rewritten, because it was wrong twice over. The figure was
an artifact, and the explanation attached to it -- that coverage fell as the suite grew
from 11 test files to 43 because the denominator outran the numerator on framework-edge
code "the JVM cannot reach" -- described a cause that does not exist. The JVM reaches that
code fine. The new tests were disproportionately Robolectric, so each one added denominator
and no numerator: the measurement was punishing precisely the tests that were hardest to
write, and the conclusion drawn from it was that writing them had not helped.

Mutation, run both ways on this branch: remove the block and jacocoTestReport collapses
back to 29.7% / 29.8%; restore it and it returns to 69.2% / 53.2%.

Two things that were true stay true. There is still no coverage gate, and a floor still
needs a settled baseline -- this one just moved 39 points in one build change. And
"re-measure before quoting it" was already written down; following it is the only reason
this was found.
2026-08-24 18:45:30 -05:00
Jason Ross e06b0826a0 Merge pull request #73 from JMR-dev/docs/instrumented-tests-correction
Say where instrumented tests run, instead of where they used to not run
2026-08-24 17:17:18 -05:00
2 changed files with 44 additions and 6 deletions
+20 -6
View File
@@ -108,12 +108,26 @@ install for code that can never run — and on API 37 the full APK does not fit
- The `model` package is excluded from `ReturnCount` and `CyclomaticComplexMethod` only. It is the
decision layer, where one branch is one documented user-visible outcome and the metric counts
answers rather than complexity. Every other rule still applies there.
- **Coverage is reported, not gated** — **29.8% of lines (629/2113), 28.7% of branches**, measured
on `main` 2026-08-23 with `./gradlew :app:jacocoTestReport`. A floor needs a baseline that has
settled first, and this one has not: the figure **fell** from the ~31% recorded earlier even
though the JVM suite went from 11 test files to 43. Main source grew 4,114 -> 5,715 lines over
the same period, so the denominator outran the numerator. Re-measure before quoting it; do not
assume more tests means a higher percentage here.
- **Coverage is reported, not gated** — **69.2% of lines (1519/2194), 53.2% of branches**,
measured 2026-08-24 with `./gradlew :app:jacocoTestReport`.
**Every figure this file carried before that date was an artifact, roughly half the real one.**
Robolectric loads classes through its own sandbox classloader with no source location, JaCoCo
skips no-location classes by default, and nothing told it otherwise — so **not one Robolectric
test counted**, and Robolectric is what exercises the framework edge here. The
`isIncludeNoLocationClasses` block in `app/build.gradle.kts` is what fixes it; **do not delete
it as stray config**, and re-run the numbers if you ever touch it. Same commit, same 335 tests:
29.7% -> 69.2% with that block alone.
The old entry also explained the wrong thing. It said coverage **fell** as the suite grew from 11
test files to 43 because "the denominator outran the numerator" on framework-edge code "the JVM
cannot reach". The JVM reaches that code fine. What actually happened is that the new tests were
disproportionately Robolectric, so each one added denominator and no numerator — the measurement
was punishing exactly the tests that were hardest to write.
Two things still hold. A floor needs a baseline that has settled, and this one has now moved by
39 points in a single build change, so it has not. And **re-measure before quoting** — that
instruction is the only reason this was caught.
- **Testable code is not done until it is tested.** If a piece is unit testable, it gets unit
tests before it counts as done. If it is e2e testable, it gets e2e tests. Both clauses apply —
a change that is both needs both.
+24
View File
@@ -188,6 +188,30 @@ detekt {
}
// Pin the coverage agent rather than inheriting whatever Gradle bundles.
// Robolectric loads every class it touches through its own sandbox classloader, and those
// classes arrive with no source location. JaCoCo skips no-location classes by default, so
// without this block **not one Robolectric test counts** -- and Robolectric is what exercises
// the framework edge here: the workers, the publisher, both ViewModels, every Compose screen.
//
// Measured on e06b082, same 335 tests, same 0 failures, only this block added:
//
// LINE 29.7% -> 69.2% OutputPublisher 0.0% -> 97.5%
// BRANCH 29.8% -> 53.2% ConversionViewModel 0.0% -> 85.4%
//
// The discriminator, if this ever looks like superstition: inside ConverterScreenKt, `describe`
// is the one non-Composable and is exercised by a plain JVM test -- it reported 8/8 covered while
// every @Composable in the same class reported 0, including ones whose mutations demonstrably
// failed the build when reverted.
//
// `excludes` is not optional. Without it JaCoCo walks JDK-internal classes that Robolectric has
// no location for either, and the test JVM dies rather than reporting a number.
tasks.withType<Test>().configureEach {
extensions.configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf("jdk.internal.*")
}
}
jacoco {
toolVersion = libs.versions.jacoco.get()
}