A team wants per-PR coverage enforcement that doesn't block on legacy code but holds the bar on new work. How do you structure jacocoTestCoverageVerification rules, and how do you wire and stage the gate in CI?
answer
- task gate = whole-codebase, not diff-aware
- ratchet BUNDLE floor at current %
- includes scope strict rule to new modules
- module-per-gate aligns with ownership
- diff coverage needs extra tooling
basics
~10 sUse multiple rules: a lenient BUNDLE floor everyone passes today, plus stricter per-CLASS/per-PACKAGE rules with includes/excludes targeting new or critical areas. Wire it into check, and ratchet thresholds up over time.
solid answer
~40 sJaCoCo's task-level gate is whole-codebase, not diff-aware, so you engineer around that. Put several `rule {}` blocks in one `violationRules`: (1) a `BUNDLE` headline set just below current coverage so it never regresses; (2) per-`PACKAGE` or per-`CLASS` rules scoped with `includes`/`excludes` to the modules where you want a higher bar; exclude legacy packages from the strict rules. Wire `check.dependsOn(jacocoTestCoverageVerification)` so every build and PR enforces it. To stage rollout, ratchet: start the headline at today's number, raise it in small increments per sprint. For true new-code-only enforcement you layer a diff-coverage tool (separate from this task) or a multi-module split where new code lives in modules with strict rules. Keep the legacy debt visible via the report but un-gated.
code
kotlin · 15 linestasks.jacocoTestCoverageVerification {
violationRules {
rule { // never regress overall
element = "BUNDLE"
limit { value = "COVEREDRATIO"; minimum = "0.62".toBigDecimal() }
}
rule { // strict on greenfield code only
element = "CLASS"
includes = listOf("com.acme.newmodule.*")
limit { minimum = "0.85".toBigDecimal() }
}
}
}
tasks.named("check") { dependsOn(tasks.named("jacocoTestCoverageVerification")) }go deeper
Out of depth; at most: multiple rules exist and you wire the task into check.
Describe multiple rules with includes/excludes and the dependsOn wiring.
Design a ratcheting + scoped strategy and articulate the task's diff-blindness honestly.
Set org-wide coverage governance: per-module gates aligned to ownership, ratchet cadence, and where to invest in diff-coverage tooling vs JaCoCo rules.
## The core constraint `jacocoTestCoverageVerification` evaluates the **whole** execution data against rules; it has no concept of 'lines changed in this PR'. So 'enforce on new work, forgive legacy' must be built from the primitives the task gives you: multiple rules, element scoping, and includes/excludes. ## Strategy 1 — ratcheting headline Set a `BUNDLE` rule to *current* coverage (say 0.62 if you're at 63%). It can never go down without failing, which prevents regression. Each sprint, nudge the minimum up. This is the simplest, most robust approach and needs no extra tooling. ```kotlin violationRules { rule { // anti-regression floor, raised over time element = "BUNDLE" limit { value = "COVEREDRATIO"; minimum = "0.62".toBigDecimal() } } } ``` ## Strategy 2 — scoped strict rules Apply a high bar only where new code lives, using `includes`: ```kotlin rule { element = "CLASS" includes = listOf("com.acme.newmodule.*") limit { minimum = "0.85".toBigDecimal() } } ``` Legacy packages simply aren't in `includes`, so they don't fail. As you migrate, expand the include list. ## Strategy 3 — module boundaries In a multi-module build, give greenfield modules strict `jacocoTestCoverageVerification` rules and leave legacy modules lenient. Each module owns its own gate; `check` per-module enforces it. This aligns the gate with code ownership. ## Wiring & CI Regardless of strategy, the gate only bites if it runs: ```kotlin tasks.named("check") { dependsOn(tasks.named("jacocoTestCoverageVerification")) } ``` Now `./gradlew check` in CI enforces it on every PR. Keep the `jacocoTestReport` running too so reviewers see numbers even when the gate passes. Cache-friendly: the verification task is up-to-date-checked on its inputs (.exec + classes + config), so it's cheap on unchanged builds. ## Where JaCoCo's task stops Genuine *diff/patch coverage* (only lines touched in the PR) is outside this task — that needs a diff-coverage plugin or a service like a coverage SaaS. Be honest in interviews: the Gradle task gates absolute thresholds; new-code-only gating is approximated via scoping/ratcheting or delegated to a diff tool.
- Can jacocoTestCoverageVerification gate only the lines changed in a PR?No. The task evaluates aggregate coverage of the whole codebase against rules. New-code/diff coverage requires a separate diff-coverage plugin or coverage service; with the bare task you approximate it via scoped includes, ratcheting, or per-module gates.
- What's the risk of setting a single high BUNDLE minimum on a legacy codebase?It fails immediately and blocks all PRs, including unrelated bug fixes, creating pressure to disable the gate entirely. Ratcheting from the current number avoids the all-or-nothing wall.
- How do you keep the gate cheap in CI?It participates in up-to-date checking on its inputs (.exec, class dirs, the rule config). On unchanged builds with a populated build cache it's skipped/up-to-date, so adding it to check costs little beyond the test run it already depends on.
saying these in an interview costs you the question
- Claiming jacocoTestCoverageVerification supports per-diff/changed-lines enforcement natively.
- Recommending a single 90% BUNDLE bar dropped onto a legacy repo overnight.
- Forgetting to wire the task into check, so the 'gate' never actually runs in CI.