How does jacoco-report-aggregation actually collect the .exec data, classes and sources from each subproject — what's the resolution mechanism?
answer
- consumable vs resolvable vs bucket
- attributes Category=verification, VerificationType
- jacoco-results / main-source-element variants
- ArtifactView -> FileCollection
- transitive pull, graceful skip
basics
~10 sIt uses Gradle's variant-aware dependency resolution: contributing projects publish coverage data, classes and sources as outgoing variants, and the aggregation plugin declares resolvable configurations that select each variant by attributes.
solid answer
~40 sThe aggregation plugin doesn't hard-code file paths. Each contributing project (via `java` / `jvm-test-suite`) exposes **consumable** outgoing variants: one carrying the JaCoCo `.exec` execution data, one carrying compiled `classes` (class directories), and one carrying `sources`. The aggregation project declares three **resolvable** configurations that extend the `jacocoAggregation` dependency set and request each of those variants using attributes — notably `Category` (`verification` for coverage data, plus `DocsType`/`VerificationType` attributes like `jacoco-results`). Gradle resolves the graph, picks the matching variant from every project, and exposes the resulting file collections (artifact views) to the generated `JacocoReport` task as its `executionData`, `classDirectories`, and `sourceDirectories`. Because it's attribute-driven, it transitively includes projects you didn't list directly if they're on the graph and skips ones that don't expose coverage variants.
code
kotlin · 11 lines// A contributing project exposes coverage as a consumable variant (done
// automatically by jvm-test-suite). Conceptually:
configurations.consumable("coverageDataElements") {
attributes {
attribute(Category.CATEGORY_ATTRIBUTE,
objects.named(Category.VERIFICATION))
attribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE,
objects.named(VerificationType.JACOCO_RESULTS))
}
outgoing.artifact(layout.buildDirectory.file("jacoco/test.exec"))
}go deeper
Just know it gathers coverage data, classes, and sources from each module automatically.
Recognize that projects publish coverage as variants and the plugin resolves them, no manual paths.
Explain consumable vs resolvable configurations and attribute matching (Category=verification, VerificationType=jacoco-results) feeding the JacocoReport task.
Position it as one case of Gradle's general verification-results aggregation pattern and reason about transitivity, graceful skipping, and refactor-safety org-wide.
## First principles: consumable vs. resolvable Every Gradle `Configuration` is one of three roles: - **Consumable** (`canBeConsumed = true`, `canBeResolved = false`) — an *outgoing* variant a project exposes to others. - **Resolvable** (`canBeResolved = true`, `canBeConsumed = false`) — an *incoming* request a project resolves. - **Dependency bucket** (`canBeConsumed = false`, `canBeResolved = false`) — where you `add` dependencies, e.g. `jacocoAggregation`. Gradle matches a resolvable configuration to a consumable variant by comparing **attributes** (typed key/value metadata), not by name. ## What contributing projects publish When a project applies `java`/`jvm-test-suite`, it exposes outgoing variants tagged with attributes such as: - `Category = verification` + `VerificationType = jacoco-results` -> the `.exec` execution data. - `Category = verification` + `VerificationType = main-source-element` -> the source directories. - A `classes`-type variant -> the compiled class output directories (not jars, so JaCoCo can map bytecode to lines). ## What the aggregation plugin requests The plugin creates internal **resolvable** configurations that `extendsFrom(jacocoAggregation)` and set matching attributes. Resolving each one yields a `FileCollection` (via an `ArtifactView`) of, respectively, every reachable project's exec data, classes, and sources. ## Wiring into the report task Those file collections are bound to the registered `JacocoReport` (`testCodeCoverageReport`): ```kotlin tasks.named<JacocoReport>("testCodeCoverageReport") { // executionData, classDirectories, sourceDirectories // are all populated from the resolved aggregation variants } ``` ## Consequences of being attribute-driven - **Transitivity**: if `:app` depends on `:lib` and you only list `:app` in `jacocoAggregation`, `:lib` is still pulled in because it's on the dependency graph and exposes the variants. - **Graceful skipping**: a project that doesn't apply jacoco/jvm-test-suite simply has no matching variant, so it contributes nothing rather than failing the build. - **No path fragility**: you never reference `build/jacoco/test.exec` by hand; refactors that move outputs don't break aggregation. This is the same machinery behind `java-test-fixtures` and the `jvm-test-suite` aggregation of test results — coverage aggregation is one instance of Gradle's general 'verification results aggregation' pattern.
- Why does it need the class *directories* rather than the jars?JaCoCo instruments and maps coverage against the exact `.class` files; resolving the `classes` variant (directory output) avoids jar repackaging/relocation that would break line/branch mapping.
- If a module isn't listed in jacocoAggregation but is a transitive dependency, is it included?Yes. Resolution walks the dependency graph, so any reachable project exposing the coverage variants contributes, even if not named directly.
- What happens to a project that doesn't apply the jacoco/jvm-test-suite plugins?It exposes no matching variant, so it's silently skipped — the build doesn't fail, that module just isn't in the report.
saying these in an interview costs you the question
- Saying it greps the filesystem for *.exec files — it's variant/attribute resolution, not path scanning.
- Claiming you must list every project explicitly — transitive projects are pulled automatically.