Skip to content

code-coverage

Type Skill
Plugin awl-android · v0.0.19
Invoke /awl-android:code-coverage
Tools Read, Write, Edit, Bash, Glob, Grep, WebFetch
Source plugins/awl-android/skills/code-coverage/SKILL.md

Sets up Kotlin code coverage with Kover for an Android or KMP project the way the AWL templates do it (per-module custom variant, koverPrintCoverageCustom, exclusion filters, optional threshold, enable_coverage on the android CI component). Use when asked to “add code coverage”, “set up Kover”, “measure test coverage”, “generate a coverage report”, “add a coverage gate or threshold”, “show coverage on the MR”, or when koverPrintCoverageCustom is missing in CI. NOT for JaCoCo, for iOS coverage (xcov via the ios-fastlane component), or for writing the tests themselves.

Trigger phrases: add code coverage · set up Kover · measure test coverage · generate a coverage report · add a coverage gate or threshold · show coverage on the MR

Each module (androidApp, shared) gets its own Kover config bound to a coverage variant named custom, with exclusion filters. Kover does not aggregate across modules at the root, so each module reports independently and one *Custom task fans out to all of them. No JaCoCo.

Step Do Where
1 Find the variant each module tests against this file
2 Add the plugin to the catalog and root build this file
3 Configure androidApp templates/androidApp-kover.kts
4 Configure shared templates/shared-kover.kts
5 Propose project-specific exclusions this file
6 Generate reports and verify this file
7 Wire into CI templates/gitlab-coverage.yml
  1. Modules to cover. Default: androidApp (Compose UI plus ViewModels) and shared (KMP logic).
  2. The variant each module’s unit tests run against, because it binds the coverage variant:
    • androidApp: the Android build variant under test. The AWL templates use devDebug. Find it with ./gradlew :androidApp:tasks --all | grep -i 'test.*UnitTest'.
    • shared: the Android target of the KMP block, android in the templates.
  3. A version catalog at gradle/libs.versions.toml. Inline versions if the project has none.
  4. Kover version: the templates pin 0.9.9. Check https://github.com/Kotlin/kotlinx-kover/releases for newer.

gradle/libs.versions.toml:

[versions]
kover = "0.9.9"
[plugins]
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }

Root build.gradle.kts, so modules can opt in:

alias(libs.plugins.kover) apply false

Apply alias(libs.plugins.kover) in plugins {} and paste the kover {} block from templates/androidApp-kover.kts. The variant name custom is the core of the setup: declaring the same name in every module lets one task (koverHtmlReportCustom) cover the whole app. Bind it with addWithDependencies("devDebug"), or whatever step 1 found.

The template excludes *di.*, *ui.theme.*, *ComposableSingletons*, and everything annotated with androidx.compose.ui.tooling.preview.Preview. A Compose Multiplatform app also excludes org.jetbrains.compose.ui.tooling.preview.Preview.

Same pattern, from templates/shared-kover.kts, bound to the Android target with addWithDependencies("android"). A shared module that only holds business logic excludes *di.*. When shared also holds Compose UI (Compose Multiplatform), reuse the full androidApp exclusion list including both Preview annotations.

Start from the generic exclusions in the templates. Then look at the project and propose more only where they clearly apply:

If the project has Suggest excluding Pattern
Generated protobuf, wire, or contracts modules the generated package *.contracts.*
Generated navigation (KSP, compose-destinations) nav glue and generated args *.ui.NavGraph, *NavArgsGettersKt
Debug-only UI (debug drawer, dev screens) debug UI *.ui.debug.*
Custom preview annotations annotate-by that annotation annotatedBy("com.example.DevicesPreview")
Sample or fake data baked into the build those classes *.SampleData, *Fake* (check it does not hide real code)

Confirm the list with the user before adding it, then check each pattern actually disappears from the HTML report.

The suffix is the capitalised variant name:

Terminal window
./gradlew koverPrintCoverageCustom # console summary, all modules
./gradlew koverHtmlReportCustom # HTML
./gradlew koverXmlReportCustom # XML for CI

Kover runs the bound variant’s unit tests itself, so no separate test<Variant>UnitTest call is needed. Open the HTML reports and confirm real percentages and absent excluded packages:

Terminal window
open androidApp/build/reports/kover/reportCustom/index.html
open shared/build/reports/kover/reportCustom/index.html

Report the headline percentage per module to the user. Outputs: <module>/build/reports/kover/reportCustom/index.html and <module>/build/reports/kover/reportCustom.xml. The console line reads application line coverage: NN%, once per module.

The template’s fastlane lane android test runs the same pair (koverXmlReportCustom koverPrintCoverageCustom).

On GitLab with the AWL android component, set enable_coverage: true on the include. The testUnit job then appends the two Kover tasks to the same Gradle run as test_task, parses the line percentage for the MR, and keeps every module’s reportCustom.xml as an artifact. Because Kover prints one line per module and GitLab takes the last match, the MR number shows the last module printed; the XML artifacts hold all of them. Keep the custom binding aligned with test_task (devDebug with testDevDebugUnitTest).

Projects that cannot use the component get a standalone job from templates/gitlab-coverage.yml. GitHub repos use templates/ci-publish-tests.yml, which posts per-module coverage to the PR.

A threshold is optional; the templates ship none. Enforce one either via the GitLab project setting “coverage must not decrease” or with Kover’s own rule (kover { reports { verify { rule { minBound(80) } } } }) plus koverVerifyCustom in the job.

  • The variant name has to match across modules; mismatched names mean separate tasks.
  • Exclusion globs are fully qualified class patterns: *di.*, *ComposableSingletons*, ch.example.contracts.*.
  • KMP shared binds to the Android target only. Other KMP targets are not covered by this variant.
  • No root-level merge: report each module independently.
  • iOS coverage in a KMP repo comes from xcov in the ios-fastlane component, not from Kover.