@@ -17,7 +17,14 @@ import java.io.OutputStream
1717/* *
1818 * Base class for generating reports based on the binary JaCoCo exec dump files.
1919 *
20- * It takes care of ignoring non-identical classes with the same fully qualified name and classes without coverage.
20+ * JaCoCo's execution data only contains per-class boolean arrays indicating which probes fired. It does not contain
21+ * any structural information (method names, line numbers, branch locations). The class files in
22+ * [codeDirectoriesOrArchives] provide that structural information: JaCoCo re-analyzes them to reconstruct which probes
23+ * correspond to which lines and branches, then merges this with the execution data to produce a meaningful coverage
24+ * report.
25+ *
26+ * Since the same class can appear in multiple archives (for example, a dependency bundled in two WARs), this class
27+ * detects such duplicates via [EnhancedCoverageVisitor] and handles them according to [duplicateClassFileBehavior].
2128 *
2229 * @param codeDirectoriesOrArchives Directories and zip files that contain class files.
2330 * @param locationIncludeFilter Include filter to apply to all locations during class file traversal.
@@ -30,8 +37,16 @@ abstract class JaCoCoBasedReportGenerator<Visitor : ICoverageVisitor>(
3037 private val duplicateClassFileBehavior : EDuplicateClassFileBehavior ,
3138 private val ignoreUncoveredClasses : Boolean ,
3239 private val logger : ILogger ,
33- /* * The coverage visitor which will be called with all the data found in the exec files. */
34- protected val coverageVisitor : Visitor ,
40+ /* *
41+ * Supplier that creates a fresh coverage visitor for each dump. This is a supplier rather than
42+ * a plain instance because [EnhancedCoverageVisitor] and the coverage visitor must share the
43+ * same lifecycle: both are created per [convertSingleDumpToReport] call. If the coverage visitor
44+ * were reused across dumps, it would still carry class IDs from a previous dump, and a class
45+ * that reappears with a different CRC64 (for example, after an application server hot-reload)
46+ * would crash inside JaCoCo's CoverageBuilder instead of being handled by our duplicate
47+ * detection in [EnhancedCoverageVisitor].
48+ */
49+ private val coverageVisitorSupplier : () -> Visitor ,
3550) {
3651
3752 /* *
@@ -43,9 +58,10 @@ abstract class JaCoCoBasedReportGenerator<Visitor : ICoverageVisitor>(
4358 fun convertSingleDumpToReport (dump : Dump , outputFilePath : File ): CoverageFile {
4459 val coverageFile = CoverageFile (outputFilePath)
4560 val mergedStore = dump.store
46- analyzeStructureAndAnnotateCoverage(mergedStore)
61+ val coverageVisitor = coverageVisitorSupplier()
62+ analyzeStructureAndAnnotateCoverage(mergedStore, coverageVisitor)
4763 coverageFile.outputStream.use { outputStream ->
48- createReport(outputStream, dump.info, mergedStore)
64+ createReport(outputStream, dump.info, mergedStore, coverageVisitor )
4965 }
5066 return coverageFile
5167 }
@@ -62,27 +78,48 @@ abstract class JaCoCoBasedReportGenerator<Visitor : ICoverageVisitor>(
6278 convertSingleDumpToReport(Dump (sessionInfo, loader.executionDataStore), outputFilePath)
6379 }
6480
65- /* * Creates an XML report based on the given session and coverage data. */
81+ /* *
82+ * Creates an XML report based on the given session and coverage data.
83+ *
84+ * @param coverageVisitor The visitor that was populated during [analyzeStructureAndAnnotateCoverage] for this dump.
85+ * Passed as a parameter (rather than being a field) because a fresh instance is created per dump via
86+ * [coverageVisitorSupplier].
87+ */
6688 @Throws(IOException ::class )
6789 protected abstract fun createReport (
6890 output : OutputStream ,
6991 sessionInfo : SessionInfo ? ,
70- store : ExecutionDataStore
92+ store : ExecutionDataStore ,
93+ coverageVisitor : Visitor
7194 )
7295
7396 /* *
74- * Analyzes the structure of the class files in [. codeDirectoriesOrArchives] and builds an in-memory coverage
97+ * Analyzes the structure of the class files in [codeDirectoriesOrArchives] and builds an in-memory coverage
7598 * report with the coverage in the given store.
99+ *
100+ * We share a single [EnhancedCoverageVisitor] across all entries so that its duplicate-detection map tracks classes
101+ * globally. Without this, the same class appearing in two different archives (for example, old and new WAR after an
102+ * application server reload) would bypass our duplicate handling and hit `CoverageBuilder` directly, which always
103+ * throws `IllegalStateException` regardless of the configured [duplicateClassFileBehavior].
76104 */
77105 @Throws(IOException ::class )
78- private fun analyzeStructureAndAnnotateCoverage (store : ExecutionDataStore ) {
106+ private fun analyzeStructureAndAnnotateCoverage (store : ExecutionDataStore , coverageVisitor : Visitor ) {
107+ val visitor = EnhancedCoverageVisitor (coverageVisitor)
79108 codeDirectoriesOrArchives.forEach { file ->
80- FilteringAnalyzer (store, EnhancedCoverageVisitor () , locationIncludeFilter, logger)
109+ FilteringAnalyzer (store, visitor , locationIncludeFilter, logger)
81110 .analyzeAll(file)
82111 }
83112 }
84113
85- private inner class EnhancedCoverageVisitor : ICoverageVisitor {
114+ /* *
115+ * Wrapper around the actual coverage visitor (typically JaCoCo's [org.jacoco.core.analysis.CoverageBuilder] or
116+ * [com.teamscale.report.compact.TeamscaleCompactCoverageBuilder]) that intercepts duplicate, non-identical class
117+ * files before they reach the wrapped visitor. Without this, duplicates would hit `CoverageBuilder` directly,
118+ * which always throws `IllegalStateException` regardless of the configured [duplicateClassFileBehavior].
119+ */
120+ private inner class EnhancedCoverageVisitor (
121+ private val coverageVisitor : Visitor
122+ ) : ICoverageVisitor {
86123
87124 private val classIdByClassName: MutableMap <String , Long > = mutableMapOf ()
88125
@@ -95,6 +132,7 @@ abstract class JaCoCoBasedReportGenerator<Visitor : ICoverageVisitor>(
95132 warnAboutDuplicateClassFile(coverage)
96133 return
97134 }
135+
98136 coverageVisitor.visitCoverage(coverage)
99137 }
100138
0 commit comments