From dc3ef8b80e6100cb9d3612578702a8703b5e4719 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sun, 25 Oct 2026 12:55:10 -0700 Subject: [PATCH] ci: publish test results and track test coverage over time Publish JUnit results from both test stages and record JaCoCo coverage with the Jenkins Coverage plugin, in post always blocks so failing tests still land in the trends. Coverage is informational: a reporting problem marks the build unstable instead of failing it and blocking the release. Gradle gets the built-in jacoco plugin with an XML report. The IntelliJ test framework loads classes through its own class loaders, so the test task sets includeNoLocationClasses (and excludes jdk.internal), and the generated lexer/parser/PSI code is left out of the report. Locally this measures about 94% line and 77% branch coverage. Co-Authored-By: Claude Opus 5.5 (1M context) --- build.gradle.kts | 21 +++++++++++++++++++++ ci/release.Jenkinsfile | 23 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9b21b01b..60ca4e8d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ plugins { id("org.jetbrains.intellij.platform") version "2.19.0" id("org.jetbrains.grammarkit") version "2023.3.0.4" id("checkstyle") + id("jacoco") id("com.avast.gradle.docker-compose") version "0.17.21" id("org.jetbrains.kotlin.jvm") version "2.4.21" id("idea") @@ -147,6 +148,26 @@ tasks { // Forward the experimental grammar-engine flag so CI can run the whole suite twice: once on the // original engine and once with -Dsystemd.unit.grammarParseEngine=true (see GrammarOptionValue). systemProperty("systemd.unit.grammarParseEngine", System.getProperty("systemd.unit.grammarParseEngine", "false")) + // Coverage for CI trends. The IntelliJ test framework loads classes through its own class loaders, which + // JaCoCo only records with includeNoLocationClasses; jdk.internal is excluded because instrumenting it + // breaks the JVM. + configure { + isIncludeNoLocationClasses = true + excludes = listOf("jdk.internal.*") + } + } + + jacocoTestReport { + // Only reports on existing execution data (run `test` first); CI publishes the XML with the Coverage plugin. + reports { + xml.required = true + html.required = true + csv.required = false + } + // Generated lexer/parser/PSI code would only dilute the numbers. + classDirectories.setFrom(files(classDirectories.files.map { + fileTree(it) { exclude("**/generated/**", "**/psi/impl/**") } + })) } } diff --git a/ci/release.Jenkinsfile b/ci/release.Jenkinsfile index 0e168df8..c98ae7ca 100644 --- a/ci/release.Jenkinsfile +++ b/ci/release.Jenkinsfile @@ -414,7 +414,7 @@ pipeline { sh(""" mkdir -p ./build ./generate-changelog > build/CHANGELOG - ./gradlew --no-daemon -I ./build-cache-init.gradle.kts -I ./repo-cache-init.gradle.kts --build-cache build buildPlugin -PbuildScan + ./gradlew --no-daemon -I ./build-cache-init.gradle.kts -I ./repo-cache-init.gradle.kts --build-cache build buildPlugin jacocoTestReport -PbuildScan """) script { if (env.BRANCH_NAME ==~ /^([0-9][0-9][0-9]\.x)$/) { @@ -452,6 +452,22 @@ pipeline { archiveArtifacts artifacts: 'build/reports/**' } } + } + post { + // Runs even when tests fail, so failing tests and coverage still land in the trends. + always { + junit testResults: 'build/test-results/test/*.xml', allowEmptyResults: true + // Coverage is informational: a reporting problem marks the build unstable rather than failing it + // (and blocking the release). + catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') { + script { + if (fileExists('build/reports/jacoco/test/jacocoTestReport.xml')) { + recordCoverage(tools: [[parser: 'JACOCO', pattern: 'build/reports/jacoco/test/jacocoTestReport.xml']], + sourceDirectories: [[path: 'src/main/kotlin'], [path: 'src/main/java']]) + } + } + } + } } } stage("Unit Tests (new grammar engine)") { @@ -472,6 +488,11 @@ pipeline { ./gradlew --no-daemon -I ./build-cache-init.gradle.kts -I ./repo-cache-init.gradle.kts --build-cache test -Dsystemd.unit.grammarParseEngine=true """) } + post { + always { + junit testResults: 'build/test-results/test/*.xml', allowEmptyResults: true + } + } } } }