Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .agents/memory/project/porting-buildsrc-from-consumer-repos.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ filter) and repo-local convention plugins/helpers living in X's `buildSrc`
`PatchGeneratedTemplateString.kt`). Cross-check direction on each remaining
file — if `config` HEAD is newer (version bumps), the consumer copy is stale,
not improved.

Two port-time adaptations recur (both hit the validation#317 port):

- `config` runs detekt over `buildSrc`; consumer repos do not. A port that
was green in the consumer repo can fail here (e.g. `TooManyFunctions`,
top-level threshold 11 per file). Fix by refactoring — merge copy-paste
helpers, reuse an existing `config` helper the consumer-side author did
not know about (e.g. `consumesCoverageBinaryReports()` from
`SiblingCoverage.kt`) — never by suppressing.
- Generalize consumer-specific KDoc (module names such as `java`/`context`,
plugin names such as `JavaValidationPlugin`): `config` distributes these
files to every repo, where such references mean nothing.
72 changes: 72 additions & 0 deletions .agents/tasks/port-compiler-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
slug: port-compiler-coverage
branch: port-compiler-coverage
owner: claude
status: in-review
started: 2026-07-06
related-memories:
- porting-buildsrc-from-consumer-repos
- config-build-verification
---

## Goal

Upstream the Spine Compiler coverage helpers introduced in
[validation#317][pr-317] into `config`'s `buildSrc`, so every consumer
repo receives them via `./config/pull` and can credit forked-compiler
codegen execution to its root Kover report.

## Context

The `launch*SpineCompiler` tasks fork a JVM that Kover/JaCoCo do not
instrument, so codegen plugins report ~0% coverage in consumer repos.
[validation#317][pr-317] fixed this in `validation` (root coverage
12% → 86%) and its description names upstreaming the `buildSrc` helpers
to `config` as the follow-up. Only `buildSrc` files port; validation's
root `build.gradle.kts` wiring
(`subprojects { enableSpineCompilerCoverage() }`) stays consumer-owned.

## Plan

- [x] Fetch the four changed `buildSrc` files from the PR branch
`credit-codegen-coverage` and diff against `config` HEAD
(confirmed purely additive; no reverse drift).
- [x] `io/spine/dependency/test/Jacoco.kt` — add the `agent` coordinate
constant.
- [x] `io/spine/gradle/testing/SpineCompilerCoverage.kt` — new file:
`enableSpineCompilerCoverage()`, cacheable per-task exec output.
- [x] `io/spine/gradle/testing/TestKitCoverage.kt` — replace the inline
agent coordinate with `Jacoco.agent`.
- [x] `io/spine/gradle/report/coverage/KoverConfig.kt` — feed compiler
exec files into the root report's `additionalBinaryReports`.
- [x] Verify: `JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew
:buildSrc:build detekt`.
- [x] Review diff with `spine-code-review`, `kotlin-engineer`,
`dependency-audit`, and `review-docs`.
- [x] Commit on branch `port-compiler-coverage`, run the pre-PR gate,
open the PR.

## Log

- 2026-07-06 — drafted and started; user prompt authorizes the port
(edits only, no commits).
- 2026-07-06 — KDoc generalized while porting: validation-specific
references (`java`/`context` modules, `JavaValidationPlugin`) removed,
since `config` distributes these files to all consumer repos.
- 2026-07-06 — detekt (which covers `buildSrc` only here in `config`)
flagged `TooManyFunctions` in `KoverConfig.kt` after the port; merged
the duplicate `testKitExecFiles`/`compilerExecFiles` helpers into one
`execFiles(project, dirName)`. Build + detekt green afterwards.
- 2026-07-06 — all four reviewers APPROVE. Applied their improvements
on top of the verbatim port: reuse `consumesCoverageBinaryReports()`
from `SiblingCoverage.kt` (promoted to `internal`) instead of a
duplicate predicate; early-return guard makes
`enableSpineCompilerCoverage()` truly idempotent (a second call would
have double-loaded the agent); `dependsOn` comment no longer reads as
ordering-only; KDoc polish (grammar, `testFixtures` spelling,
`[Jacoco.agent]` links in `TestKitCoverage.kt`).
- 2026-07-06 — work complete; committed on branch
`port-compiler-coverage` (two commits), pre-PR gate re-run green (all
four reviewers APPROVE), PR opened for human review.

[pr-317]: https://github.com/SpineEventEngine/validation/pull/317
8 changes: 8 additions & 0 deletions buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,12 @@ package io.spine.dependency.test
@Suppress("ConstPropertyName")
object Jacoco {
const val version = "0.8.15"

/**
* The Maven coordinates of the standalone JaCoCo agent JAR (the `runtime`
* classifier), attached via `-javaagent:` to forked JVMs — Gradle TestKit
* workers and the Spine Compiler process — so their execution is credited
* to coverage.
*/
const val agent = "org.jacoco:org.jacoco.agent:$version:runtime"
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ package io.spine.gradle.report.coverage
import io.spine.dependency.test.Jacoco
import io.spine.dependency.test.Kover
import io.spine.gradle.testing.TESTKIT_COVERAGE_DIR
import io.spine.gradle.testing.isSpineCompilerLaunchTask
import java.io.File
import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
import org.gradle.api.Project
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.SourceSet
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
Expand Down Expand Up @@ -104,6 +106,13 @@ private const val KOTLIN_FILE_CLASS_SUFFIX: String = "Kt"
* the probe level against each module's actual bytecode: a line hit both
* in-process and out-of-process is counted once. Summing pre-aggregated XML
* reports instead would double-count such lines.
* - Feeds the JaCoCo execution data produced by the forked Spine Compiler JVMs
* (see [io.spine.gradle.testing.enableSpineCompilerCoverage]) into the **root**
* `total` report as `additionalBinaryReports`. This credits code-generation
* plugins — renderers and generators that run out-of-process in the compiler
* fork and are otherwise reported at ~0%. Fed only at the root: Codecov ingests
* the root report, whose aggregation owns those classes, so a per-module feed
* would be redundant.
*
* This is the Kover-based successor to the deprecated JaCoCo-based
* coverage aggregation pipeline. The behaviour mirrors what
Expand Down Expand Up @@ -198,6 +207,7 @@ class KoverConfig private constructor(
onCheck.set(true)
}
additionalBinaryReports.addAll(rootTestKitExecFilesProvider())
additionalBinaryReports.addAll(rootCompilerExecFilesProvider())
}
filters {
excludes {
Expand Down Expand Up @@ -239,7 +249,43 @@ class KoverConfig private constructor(
rootProject.provider {
rootProject.subprojects.asSequence()
.filter { it.pluginManager.hasPlugin(Kover.id) }
.flatMap { testKitExecFiles(it).asSequence() }
.flatMap { execFiles(it, TESTKIT_COVERAGE_DIR).asSequence() }
.toList()
}

/**
* Lazy `Provider` of the JaCoCo execution-data files produced by the forked
* Spine Compiler JVMs across every subproject.
*
* These files credit the out-of-process execution of compiler plugins —
* renderers and generators that run inside the compiler fork — to the root
* coverage rollup. Unlike the TestKit files, they are collected from **all**
* subprojects regardless of whether the producing module applies Kover: the
* exec may be produced by a module that does not itself apply Kover
* (typically a test-only module), while the classes it credits belong to the
* aggregated production modules. Resolved at task-graph time, after the
* launch tasks have written them.
*
* The files are the `.exec` outputs that each subproject's **currently
* configured** `launch*SpineCompiler` tasks still declare — read from the task
* outputs, not by scanning the coverage directory. A stale exec left in an
* un-`clean`ed workspace is therefore never credited, whether its launch task
* was removed or the module simply stopped enabling the helper (in which case
* the task no longer declares the exec as an output). Filtering by the `exec`
* extension drops any non-coverage outputs the compiler task may also declare.
*
* @see io.spine.gradle.testing.enableSpineCompilerCoverage
*/
private fun rootCompilerExecFilesProvider(): Provider<Iterable<File>> =
rootProject.provider {
rootProject.subprojects.asSequence()
.flatMap { sub ->
sub.tasks.withType(JavaExec::class.java)
.matching { it.isSpineCompilerLaunchTask() }
.asSequence()
.flatMap { it.outputs.files.asSequence() }
}
.filter { it.isFile && it.extension == "exec" }
.toList()
}

Expand All @@ -248,7 +294,7 @@ class KoverConfig private constructor(
* TestKit workers of [sub]. See [rootTestKitExecFilesProvider] for timing notes.
*/
private fun testKitExecFilesProvider(sub: Project): Provider<Iterable<File>> =
sub.provider { testKitExecFiles(sub) }
sub.provider { execFiles(sub, TESTKIT_COVERAGE_DIR) }

/**
* Lazy `Provider` of the generated-class FQN exclusion patterns
Expand Down Expand Up @@ -284,15 +330,20 @@ class KoverConfig private constructor(
}

/**
* Returns the JaCoCo execution-data (`.exec`) files written by the Gradle TestKit
* workers of the [project], or an empty list if the module produced none.
* Returns the JaCoCo execution-data (`.exec`) files found in the [dirName]
* directory under the [project]'s `build` directory, or an empty list if the
* module produced none.
*
* The files reside under `build/`[TESTKIT_COVERAGE_DIR] and are created by the
* `plugin-testlib` test harness when a module opts in via
* [io.spine.gradle.testing.enableTestKitCoverage].
* Used for the Gradle TestKit workers, which write into [TESTKIT_COVERAGE_DIR]
* when a module opts in via [io.spine.gradle.testing.enableTestKitCoverage]. A
* directory scan is safe there because the exec directory is wiped once per build
* (see `enableTestKitCoverage`); the Spine Compiler collector instead gathers its
* files from the current launch tasks (see
* [io.spine.gradle.testing.enableSpineCompilerCoverage]), because its cacheable
* tasks cannot wipe the directory.
*/
private fun testKitExecFiles(project: Project): List<File> {
val dir = project.layout.buildDirectory.dir(TESTKIT_COVERAGE_DIR).get().asFile
private fun execFiles(project: Project, dirName: String): List<File> {
val dir = project.layout.buildDirectory.dir(dirName).get().asFile
if (!dir.isDirectory) {
return emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,16 @@ private fun Project.execFilesOf(testTasks: TaskCollection<Test>): Provider<Itera
private const val BIN_REPORTS_DIR: String = "kover/bin-reports"

/**
* Tells whether this is a Kover task that reads the binary reports and therefore
* must run only after the [contributor's][creditTestCoverageFrom] test data exists.
* Tells whether this is a Kover task that reads binary execution-data reports
* and therefore must run only after the tasks that produce them — the
* [contributor's][creditTestCoverageFrom] test tasks, or the forked Spine
* Compiler launch tasks (see [io.spine.gradle.testing.enableSpineCompilerCoverage]).
*
* This matches both the report tasks (`koverXmlReport`, `koverHtmlReport`,
* `koverBinaryReport`) and the verification tasks (`koverVerify` and its
* cacheable companion `koverCachedVerify`) — the suffix test covers the
* `Cached*` variants Kover registers, which are the ones that actually consume
* the binary reports.
*/
private fun Task.consumesCoverageBinaryReports(): Boolean =
internal fun Task.consumesCoverageBinaryReports(): Boolean =
name.startsWith("kover") && (name.endsWith("Report") || name.endsWith("Verify"))
Loading
Loading