build: wire up JaCoCo code-coverage reporting #241

Merged
JMR-dev merged 4 commits from build-192-jacoco into main 2026-07-03 17:55:16 +00:00
JMR-dev commented 2026-07-03 17:24:14 +00:00 (Migrated from github.com)

Summary

Resumes crash-interrupted work on #192. A prior session's WIP (recovered onto build-192-jacoco) had already added a jacocoTestReport task and a hardcoded JaCoCo toolVersion to app/build.gradle.kts; this PR finishes wiring it up correctly and verifies it end to end.

What was recovered (prior session, unverified):

  • jacoco plugin applied (Gradle built-in — no org.jetbrains.kotlin.android, no KAPT, per this repo's AGP-9 build constraints).
  • A jacocoTestReport task scaffold: XML + HTML reports, classDirectories scoped to the debug variant's compiled Kotlin, sourceDirectories scoped to src/main/kotlin, executionData pointed at the unit-test exec file, and a generated-code exclusion list.
  • toolVersion hardcoded to "0.8.13" directly in app/build.gradle.kts.

What this PR finishes/fixes, verified against the actual compiled output:

  • Moves the JaCoCo tool version into gradle/libs.versions.toml (jacoco = "0.8.13"), matching how every other plugin version in this repo is sourced, instead of a hardcoded string.
  • Fixes the generated-code exclusion list. I inspected the real compileDebugKotlin output tree to check what actually needed excluding rather than guessing:
    • Room's KSP-generated _Impl DAOs/database and the Compose compiler's per-file ComposableSingletons holders are the only generated code that actually lands in classDirectories — Hilt/Dagger's generated Java and AGP's BuildConfig/R/Manifest are compiled by a separate javac task (hiltJavaCompileDebug / compileDebugJavaWithJavac) this report never reads, so those patterns are kept as belt-and-suspenders only.
    • Dropped the blanket **/*$$* exclude the WIP had. It was silently discarding ~200 real classes' worth of coverage — Kotlin's own $$inlined$ synthetic classes (e.g. the Flow.map { ... } transforms in the repository layer) are hand-written business logic, not generated boilerplate, and excluding them was quietly shrinking the measured surface.
    • Added Hilt_*/Dagger* prefix patterns so the (currently inert) Hilt exclusions are actually correct if classDirectories' scope ever changes.
    • DataBinding isn't enabled in this module (no buildFeatures.dataBinding/viewBinding), so there's nothing to exclude for it — documented instead of guessing at a pattern.
  • Adds a minimal CI step to the existing unit-tests job: runs :app:jacocoTestReport and uploads the XML+HTML report as a build artifact. No coverage-threshold gate yet — a jacocoTestCoverageVerification rule is a natural follow-up once there's a baseline to set a sensible floor against. Left the E2E matrix / managed-device lockstep untouched (out of scope).
  • Documents the new ./gradlew :app:jacocoTestReport command in CLAUDE.md.

Verification: built and ran on JDK 21 (C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot, since JAVA_HOME here defaults to 25). Confirmed both a from-scratch (clean) run and a warm/cached run produce the report cleanly. The generated report shows real signal — 30% instruction / 38% line coverage overall, with spot checks confirming hand-written repository/viewmodel/sync logic shows nonzero coverage and no Hilt/Dagger/BuildConfig/R/Room-_Impl classes leak into the numbers.

Test plan

  • :app:assembleDebug — pass
  • :app:testDebugUnitTest — pass
  • :app:compileDebugAndroidTestKotlin — pass
  • :app:lintDebug — pass
  • :app:ktlintCheck — pass (including the Kotlin-script ruleset over app/build.gradle.kts)
  • :app:detekt — pass
  • :app:jacocoTestReport — pass, from both a clean and a warm build; XML + HTML reports generated under app/build/reports/jacoco/jacocoTestReport/
  • CI green on this PR (relies on the new coverage step in .github/workflows/ci.yml's unit-tests job)

Closes #192

🤖 Generated with Claude Code

## Summary Resumes crash-interrupted work on #192. A prior session's WIP (recovered onto `build-192-jacoco`) had already added a `jacocoTestReport` task and a hardcoded JaCoCo `toolVersion` to `app/build.gradle.kts`; this PR finishes wiring it up correctly and verifies it end to end. **What was recovered (prior session, unverified):** - `jacoco` plugin applied (Gradle built-in — no `org.jetbrains.kotlin.android`, no KAPT, per this repo's AGP-9 build constraints). - A `jacocoTestReport` task scaffold: XML + HTML reports, `classDirectories` scoped to the debug variant's compiled Kotlin, `sourceDirectories` scoped to `src/main/kotlin`, `executionData` pointed at the unit-test exec file, and a generated-code exclusion list. - `toolVersion` hardcoded to `"0.8.13"` directly in `app/build.gradle.kts`. **What this PR finishes/fixes, verified against the actual compiled output:** - Moves the JaCoCo tool version into `gradle/libs.versions.toml` (`jacoco = "0.8.13"`), matching how every other plugin version in this repo is sourced, instead of a hardcoded string. - Fixes the generated-code exclusion list. I inspected the real `compileDebugKotlin` output tree to check what actually needed excluding rather than guessing: - Room's KSP-generated `_Impl` DAOs/database and the Compose compiler's per-file `ComposableSingletons` holders are the only generated code that actually lands in `classDirectories` — Hilt/Dagger's generated Java and AGP's BuildConfig/R/Manifest are compiled by a separate javac task (`hiltJavaCompileDebug` / `compileDebugJavaWithJavac`) this report never reads, so those patterns are kept as belt-and-suspenders only. - **Dropped the blanket `**/*$$*` exclude** the WIP had. It was silently discarding ~200 real classes' worth of coverage — Kotlin's own `$$inlined$` synthetic classes (e.g. the `Flow.map { ... }` transforms in the repository layer) are hand-written business logic, not generated boilerplate, and excluding them was quietly shrinking the measured surface. - Added `Hilt_*`/`Dagger*` prefix patterns so the (currently inert) Hilt exclusions are actually correct if `classDirectories`' scope ever changes. - DataBinding isn't enabled in this module (no `buildFeatures.dataBinding`/`viewBinding`), so there's nothing to exclude for it — documented instead of guessing at a pattern. - Adds a minimal CI step to the existing `unit-tests` job: runs `:app:jacocoTestReport` and uploads the XML+HTML report as a build artifact. No coverage-threshold gate yet — a `jacocoTestCoverageVerification` rule is a natural follow-up once there's a baseline to set a sensible floor against. Left the E2E matrix / managed-device lockstep untouched (out of scope). - Documents the new `./gradlew :app:jacocoTestReport` command in `CLAUDE.md`. **Verification:** built and ran on JDK 21 (`C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot`, since `JAVA_HOME` here defaults to 25). Confirmed both a from-scratch (`clean`) run and a warm/cached run produce the report cleanly. The generated report shows real signal — 30% instruction / 38% line coverage overall, with spot checks confirming hand-written repository/viewmodel/sync logic shows nonzero coverage and no Hilt/Dagger/BuildConfig/R/Room-`_Impl` classes leak into the numbers. ## Test plan - [x] `:app:assembleDebug` — pass - [x] `:app:testDebugUnitTest` — pass - [x] `:app:compileDebugAndroidTestKotlin` — pass - [x] `:app:lintDebug` — pass - [x] `:app:ktlintCheck` — pass (including the Kotlin-script ruleset over `app/build.gradle.kts`) - [x] `:app:detekt` — pass - [x] `:app:jacocoTestReport` — pass, from both a clean and a warm build; XML + HTML reports generated under `app/build/reports/jacoco/jacocoTestReport/` - [ ] CI green on this PR (relies on the new coverage step in `.github/workflows/ci.yml`'s `unit-tests` job) Closes #192 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.