build(coverage): finish wiring JaCoCo unit-test coverage reporting
Completes the crash-interrupted #192 WIP (app/build.gradle.kts already had a jacocoTestReport task and toolVersion pin recovered onto build-192-jacoco): - Move the JaCoCo tool version into gradle/libs.versions.toml instead of a hardcoded string in app/build.gradle.kts, matching how every other plugin version in this repo is sourced. - Fix the generated-code exclusion list against the real compileDebugKotlin output (verified by inspecting the compiled class tree): 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, since Hilt/Dagger's generated Java and AGP's BuildConfig/R/Manifest are compiled by a separate javac task this report never reads. Drop the blanket `**/*$$*` exclude the WIP had — it was silently discarding ~200 real classes' worth of coverage on Kotlin's own `$$inlined$` synthetic classes (e.g. Flow.map { ... } transforms in the repositories), which is hand-written logic, not generated boilerplate. - Add Hilt_*/Dagger* prefix patterns so the (currently inert, belt-and-suspenders) Hilt exclusions are actually correct if the classDirectories scope ever changes. - Add a minimal CI step to the existing unit-tests job that runs 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). - Document the new :app:jacocoTestReport task in CLAUDE.md. Verified on JDK 21: fast gate (assembleDebug, testDebugUnitTest, compileDebugAndroidTestKotlin, lintDebug, ktlintCheck, detekt) plus jacocoTestReport all pass, from both a warm and a `clean` build. The report shows real signal (30% instruction / 38% line coverage) with no generated classes leaking in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,21 @@ jobs:
|
||||
path: app/build/reports/tests/testDebugUnitTest/
|
||||
if-no-files-found: warn
|
||||
|
||||
# JaCoCo XML + HTML coverage for the JVM unit tests (issue #192). Reporting only — no
|
||||
# threshold gate yet, so a coverage regression does not fail CI (a jacocoTestCoverageVerification
|
||||
# gate is a natural follow-up once there's a baseline to enforce).
|
||||
- name: Generate JaCoCo coverage report
|
||||
if: ${{ !cancelled() }}
|
||||
run: ./gradlew :app:jacocoTestReport --stacktrace
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-coverage-report
|
||||
path: app/build/reports/jacoco/jacocoTestReport/
|
||||
if-no-files-found: warn
|
||||
|
||||
static-analysis:
|
||||
name: Static analysis
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -16,6 +16,7 @@ Use a **JDK 17–21** for the Gradle daemon. AGP 9.2 does **not** support JDK 25
|
||||
./gradlew :app:testDebugUnitTest # JVM unit tests
|
||||
./gradlew :app:lintDebug # Android lint
|
||||
./gradlew :app:ktlintCheck :app:detekt # static analysis (CI's "Static analysis" gate)
|
||||
./gradlew :app:jacocoTestReport # JVM unit-test coverage (XML+HTML under app/build/reports/jacoco/)
|
||||
# single unit test:
|
||||
./gradlew :app:testDebugUnitTest --tests "org.libremail.data.SomeClassTest"
|
||||
```
|
||||
|
||||
+22
-7
@@ -178,9 +178,10 @@ detekt {
|
||||
config.setFrom(rootProject.file("config/detekt/detekt.yml"))
|
||||
}
|
||||
|
||||
// Pin a modern JaCoCo so the coverage agent understands Kotlin 2.4.0 bytecode on JDK 21.
|
||||
// Pin a modern JaCoCo (version catalog) so the coverage agent understands Kotlin 2.4.0 bytecode
|
||||
// on JDK 21.
|
||||
jacoco {
|
||||
toolVersion = "0.8.13"
|
||||
toolVersion = libs.versions.jacoco.get()
|
||||
}
|
||||
|
||||
// Unit-test coverage report (issue #192). Reads the exec data the base `jacoco` plugin records for
|
||||
@@ -198,24 +199,38 @@ tasks.register<JacocoReport>("jacocoTestReport") {
|
||||
html.required.set(true)
|
||||
}
|
||||
|
||||
// Strip generated code from the denominator so the % reflects hand-written Kotlin: Room (*_Impl*),
|
||||
// Hilt/Dagger, Compose singletons/synthetics, and Android BuildConfig/R/Manifest scaffolding.
|
||||
// Strip generated code from the denominator so the % reflects hand-written Kotlin. Verified
|
||||
// against an actual compileDebugKotlin output tree: 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 below (Room's KSP output is added as an extra Kotlin
|
||||
// source root on the *same* compile task, so it comes out the same door as hand-written code).
|
||||
// Hilt/Dagger's generated Java (Hilt_*, Dagger*_HiltComponents*, *_GeneratedInjector, *_Factory,
|
||||
// *_MembersInjector, hilt_aggregated_deps) and AGP's BuildConfig/R/Manifest are compiled by a
|
||||
// separate javac task (hiltJavaCompileDebug / compileDebugJavaWithJavac) into a directory this
|
||||
// report never reads, so those patterns are conventional belt-and-suspenders in case that ever
|
||||
// changes. DataBinding isn't enabled in this module (no buildFeatures.dataBinding/viewBinding),
|
||||
// so there's nothing generated for it to exclude; if it's turned on later, add "**/BR.class",
|
||||
// "**/DataBinderMapperImpl*.class" and "**/*Binding.class".
|
||||
//
|
||||
// Deliberately NOT excluded: Kotlin's own `$$inlined$` synthetic classes (e.g. for
|
||||
// `Flow.map { ... }` in the repositories) — those hold real hand-written transform logic, not
|
||||
// generated boilerplate, so stripping them would silently shrink the measured surface.
|
||||
val generated = listOf(
|
||||
"**/R.class",
|
||||
"**/R\$*.class",
|
||||
"**/BuildConfig.*",
|
||||
"**/Manifest*.*",
|
||||
"**/Hilt_*.class",
|
||||
"**/Dagger*.class",
|
||||
"**/*_Hilt*",
|
||||
"**/*_GeneratedInjector.class",
|
||||
"**/hilt_aggregated_deps/**",
|
||||
"**/dagger/**",
|
||||
"**/*_Factory*",
|
||||
"**/*_MembersInjector*",
|
||||
"**/*_Provide*",
|
||||
"**/*_Impl_*",
|
||||
"**/*_Impl*",
|
||||
"**/ComposableSingletons*",
|
||||
"**/*\$\$*",
|
||||
"**/*\$*Lambda*",
|
||||
)
|
||||
|
||||
// Classes = the debug variant's compiled Kotlin (AGP 9 built-in Kotlin output). All hand-written
|
||||
|
||||
@@ -35,6 +35,9 @@ errorprone = "2.50.0"
|
||||
# detekt 2.0 is the only line with Gradle 9 support (stable 1.23.x tops out at Gradle 8.12).
|
||||
ktlint = "14.2.0"
|
||||
detekt = "2.0.0-alpha.5"
|
||||
# JaCoCo coverage agent/report tool (issue #192). Pinned above Gradle's bundled default so the
|
||||
# coverage agent understands Kotlin 2.4.0 bytecode on JDK 21.
|
||||
jacoco = "0.8.13"
|
||||
|
||||
[libraries]
|
||||
# AndroidX core / lifecycle / activity / navigation
|
||||
|
||||
Reference in New Issue
Block a user