Symptom
When kensa-core lands on a test classpath that runs zero Kensa tests, the JUnit Platform listener (META-INF/services-registered) still:
- Recreates
<outputDir> (deletes any prior contents — IoUtil.recreate(...) from ResultWriter's init block).
- Writes
index.html, kensa.js, logo.svg, configuration.json, indices.json (with empty indices).
- Prints
Kensa Output : <outputDir>/index.html to stdout.
This surfaces in the gradle plugin as kensa-dev/build-plugins#3 — non-Kensa test tasks emit the banner even when nothing in them used Kensa.
Why it's not solvable purely build-plugin-side
In the gradle plugin's pattern 2 (@ExpandableSentence support code in main only — compiler plugin applied to main, tests don't use the macros), the gradle plugin adds kensa-core as implementation to main. That dependency flows transitively to testRuntimeClasspath via Gradle's standard configuration inheritance. JUnit Platform auto-discovers and instantiates KensaTestExecutionListener from the META-INF/services/... entry before any build-side code can intervene — so the listener will fire on the test task even if that task is not configured as a Kensa output source set. The listener itself has to be defensive.
Fix
In core/src/main/kotlin/dev/kensa/context/KensaLifecycleManager.kt:88-91, short-circuit writeAllResults() on empty containers before resultWriter.value is accessed:
fun writeAllResults() {
if (!isOutputEnabled) return
if (kensaContext.testContainers.isEmpty()) return // <-- new
resultWriter.value.write(kensaContext.testContainers)
}
Ordering matters. ResultWriter's init block calls IoUtil.recreate(configuration.outputDir), so the guard must precede .value triggering construction — otherwise an empty plan still nukes the output directory.
One change covers JUnit 5, JUnit 6, TestNG, and Kotest — all four framework listeners funnel through writeAllResults():
frameworks/junit/junit5/src/main/kotlin/dev/kensa/junit/KensaTestExecutionListener.kt:25
frameworks/junit/junit6/src/main/kotlin/dev/kensa/junit/KensaTestExecutionListener.kt
frameworks/testng/src/main/kotlin/dev/kensa/testng/KensaTestNgListener.kt
frameworks/kotest/src/main/kotlin/dev/kensa/kotest/KensaKotestListener.kt
Verification
Add a regression test (likely in frameworks/junit/junit5/src/.../...Test.kt — pick the existing test harness file that hosts an end-to-end JUnit Platform invocation):
- Run an empty JUnit 5 test plan (or a plan with only non-Kensa tests) with kensa-core on the classpath.
- Assert
outputDir was not created/touched.
- Assert captured stdout does not contain
Kensa Output.
Related
Symptom
When
kensa-corelands on a test classpath that runs zero Kensa tests, the JUnit Platform listener (META-INF/services-registered) still:<outputDir>(deletes any prior contents —IoUtil.recreate(...)fromResultWriter'sinitblock).index.html,kensa.js,logo.svg,configuration.json,indices.json(with emptyindices).Kensa Output : <outputDir>/index.htmlto stdout.This surfaces in the gradle plugin as kensa-dev/build-plugins#3 — non-Kensa test tasks emit the banner even when nothing in them used Kensa.
Why it's not solvable purely build-plugin-side
In the gradle plugin's pattern 2 (
@ExpandableSentencesupport code inmainonly — compiler plugin applied tomain, tests don't use the macros), the gradle plugin addskensa-coreasimplementationtomain. That dependency flows transitively totestRuntimeClasspathvia Gradle's standard configuration inheritance. JUnit Platform auto-discovers and instantiatesKensaTestExecutionListenerfrom theMETA-INF/services/...entry before any build-side code can intervene — so the listener will fire on thetesttask even if that task is not configured as a Kensa output source set. The listener itself has to be defensive.Fix
In
core/src/main/kotlin/dev/kensa/context/KensaLifecycleManager.kt:88-91, short-circuitwriteAllResults()on empty containers beforeresultWriter.valueis accessed:Ordering matters.
ResultWriter'sinitblock callsIoUtil.recreate(configuration.outputDir), so the guard must precede.valuetriggering construction — otherwise an empty plan still nukes the output directory.One change covers JUnit 5, JUnit 6, TestNG, and Kotest — all four framework listeners funnel through
writeAllResults():frameworks/junit/junit5/src/main/kotlin/dev/kensa/junit/KensaTestExecutionListener.kt:25frameworks/junit/junit6/src/main/kotlin/dev/kensa/junit/KensaTestExecutionListener.ktframeworks/testng/src/main/kotlin/dev/kensa/testng/KensaTestNgListener.ktframeworks/kotest/src/main/kotlin/dev/kensa/kotest/KensaKotestListener.ktVerification
Add a regression test (likely in
frameworks/junit/junit5/src/.../...Test.kt— pick the existing test harness file that hosts an end-to-end JUnit Platform invocation):outputDirwas not created/touched.Kensa Output.Related
sourceSetsfromoutputSourceSetsto give users explicit control, but the listener-side fix is what closes the transitive-classpath leak path).ResultWriter.writesite:core/src/main/kotlin/dev/kensa/output/ResultWriter.kt:37-43