Comprehensive code coverage tool for Scala providing statement and branch coverage through compiler plugin instrumentation and report generation
—
The CoverageAggregator provides functionality to combine coverage data from multiple subprojects, modules, or test runs into unified coverage reports. This is essential for multi-module builds where each module generates its own coverage data that needs to be combined for overall project analysis.
object CoverageAggregator {
def aggregate(dataDirs: Seq[File], sourceRoot: File): Option[Coverage]
def aggregate(dataDirs: Array[File], sourceRoot: File): Option[Coverage]
def aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File): Coverage
}Methods:
aggregate(dataDirs: Seq[File], sourceRoot: File) - Aggregate coverage from multiple data directories, returns None if no data foundaggregate(dataDirs: Array[File], sourceRoot: File) - Array version for Gradle plugin compatibilityaggregatedCoverage(dataDirs: Seq[File], sourceRoot: File) - Direct aggregation, always returns Coverage objectParameters:
dataDirs - Sequence of directories containing scoverage data filessourceRoot - Common source root directory for path resolutionimport java.io.File
import scoverage.reporter.CoverageAggregator
// Define data directories for each module
val dataDirs = Seq(
new File("module1/target/scoverage-data"),
new File("module2/target/scoverage-data"),
new File("shared/target/scoverage-data")
)
// Common source root for all modules
val sourceRoot = new File(".")
// Aggregate coverage data
val aggregatedCoverage = CoverageAggregator.aggregate(dataDirs, sourceRoot)
aggregatedCoverage match {
case Some(coverage) =>
println(s"Aggregated coverage: ${coverage.statementCoverageFormatted}%")
// Generate reports using the aggregated coverage
generateReports(coverage)
case None =>
println("No coverage data found in specified directories")
}import java.io.File
import scoverage.reporter.CoverageAggregator
import scoverage.reporter.ScoverageHtmlWriter
// Collect data directories from all sub-projects
val projectDataDirs = Seq(
new File("core/target/scoverage-data"),
new File("api/target/scoverage-data"),
new File("web/target/scoverage-data"),
new File("persistence/target/scoverage-data")
)
val rootSourceDir = new File(".")
val outputDir = new File("target/aggregated-coverage-report")
// Aggregate coverage
val coverage = CoverageAggregator.aggregatedCoverage(projectDataDirs, rootSourceDir)
// Generate consolidated HTML report
val allSourceDirs = Seq(
new File("core/src/main/scala"),
new File("api/src/main/scala"),
new File("web/src/main/scala"),
new File("persistence/src/main/scala")
)
val htmlWriter = new ScoverageHtmlWriter(allSourceDirs, outputDir, Some("UTF-8"))
htmlWriter.write(coverage)
println(s"Aggregated report generated with ${coverage.statementCoverageFormatted}% coverage")import java.io.File
import scoverage.reporter.CoverageAggregator
// Using Array for Gradle compatibility
val dataDirArray: Array[File] = Array(
new File("subproject1/build/scoverage"),
new File("subproject2/build/scoverage"),
new File("shared/build/scoverage")
)
val sourceRoot = new File(".")
val aggregatedCoverage = CoverageAggregator.aggregate(dataDirArray, sourceRoot)
aggregatedCoverage.foreach { coverage =>
println(s"Total statements: ${coverage.statementCount}")
println(s"Covered statements: ${coverage.invokedStatementCount}")
println(s"Statement coverage: ${coverage.statementCoverageFormatted}%")
println(s"Branch coverage: ${coverage.branchCoverageFormatted}%")
}import java.io.File
import scoverage.reporter.CoverageAggregator
val possibleDataDirs = Seq(
new File("module1/target/scoverage-data"),
new File("module2/target/scoverage-data"),
new File("optional-module/target/scoverage-data")
)
// Filter to only existing directories with coverage data
val validDataDirs = possibleDataDirs.filter { dir =>
dir.exists() && dir.isDirectory && {
val coverageFile = new File(dir, "scoverage.coverage")
coverageFile.exists()
}
}
if (validDataDirs.nonEmpty) {
val sourceRoot = new File(".")
val coverage = CoverageAggregator.aggregatedCoverage(validDataDirs, sourceRoot)
println(s"Successfully aggregated coverage from ${validDataDirs.size} modules")
println(s"Overall coverage: ${coverage.statementCoverageFormatted}%")
} else {
println("No valid coverage data directories found")
}The aggregation process works as follows:
scoverage.coverage files are deserializedscoverage.measurements.*) are loadedDuring aggregation, statement IDs are reassigned to ensure uniqueness:
// Pseudo-code showing ID reassignment process
var globalId = 0
val mergedCoverage = Coverage()
dataDirs.foreach { dataDir =>
val moduleCoverage = loadCoverageFromDir(dataDir)
val measurements = loadMeasurementsFromDir(dataDir)
// Apply measurements to module coverage
moduleCoverage.apply(measurements)
// Add statements with new unique IDs
moduleCoverage.statements.foreach { stmt =>
globalId += 1
mergedCoverage.add(stmt.copy(id = globalId))
}
}All source paths are resolved relative to the provided sourceRoot parameter to ensure consistent path handling across modules.
project-root/
├── module1/
│ ├── src/main/scala/
│ └── target/scoverage-data/
├── module2/
│ ├── src/main/scala/
│ └── target/scoverage-data/
└── shared/
├── src/main/scala/
└── target/scoverage-data/project-root/
├── subproject1/
│ ├── src/main/scala/
│ └── build/scoverage/
├── subproject2/
│ ├── src/main/scala/
│ └── build/scoverage/
└── shared/
├── src/main/scala/
└── build/scoverage/// Aggregate coverage from different platforms (JVM, JS, Native)
val platformDataDirs = Seq(
new File("target/scala-2.13/scoverage-data"), // JVM
new File("target/scala-2.13/scoverage-data-js"), // Scala.js
new File("target/scala-2.13/scoverage-data-native") // Scala Native
)
val coverage = CoverageAggregator.aggregatedCoverage(platformDataDirs, new File("."))// Start with base coverage
var aggregatedCoverage = Coverage()
// Add modules incrementally
moduleDataDirs.foreach { dataDir =>
val moduleCoverage = CoverageAggregator.aggregatedCoverage(Seq(dataDir), sourceRoot)
// Merge with existing aggregated coverage
// Note: This requires manual statement ID management
moduleCoverage.statements.foreach { stmt =>
aggregatedCoverage.add(stmt.copy(id = generateUniqueId()))
}
}Missing Coverage Files:
// Handle missing scoverage.coverage files
val validDirs = dataDirs.filter { dir =>
val coverageFile = new File(dir, "scoverage.coverage")
if (!coverageFile.exists()) {
println(s"Warning: No coverage file found in ${dir.getPath}")
false
} else {
true
}
}Inconsistent Source Roots:
// Validate source root accessibility
val sourceRoot = new File(".")
if (!sourceRoot.exists() || !sourceRoot.isDirectory) {
throw new IllegalArgumentException(s"Source root does not exist: ${sourceRoot.getPath}")
}Empty Data Directories:
val coverage = CoverageAggregator.aggregate(dataDirs, sourceRoot)
coverage match {
case Some(cov) if cov.statementCount == 0 =>
println("Warning: Aggregated coverage contains no statements")
case Some(cov) =>
println(s"Successfully aggregated ${cov.statementCount} statements")
case None =>
println("No coverage data found to aggregate")
}Option-returning aggregate method to handle cases with no dataInstall with Tessl CLI
npx tessl i tessl/maven-org-scoverage--scalac-scoverage-plugin