diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d05abc5..c28522f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 18a3db8..8f8ed87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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" ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 124b928..b4d6630 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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("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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20e92fc..d4e236c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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