CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scoverage--scalac-scoverage-plugin

Comprehensive code coverage tool for Scala providing statement and branch coverage through compiler plugin instrumentation and report generation

Pending
Overview
Eval results
Files

aggregation.mddocs/

Coverage Aggregation

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.

Core API

CoverageAggregator Object

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 found
  • aggregate(dataDirs: Array[File], sourceRoot: File) - Array version for Gradle plugin compatibility
  • aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File) - Direct aggregation, always returns Coverage object

Parameters:

  • dataDirs - Sequence of directories containing scoverage data files
  • sourceRoot - Common source root directory for path resolution

Usage Examples

Basic Multi-Module Aggregation

import 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")
}

SBT Multi-Project Build Integration

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")

Gradle Multi-Project Build

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}%")
}

Conditional Aggregation with Error Handling

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")
}

Aggregation Process

Data Collection Phase

The aggregation process works as follows:

  1. Directory Scanning: Each data directory is scanned for coverage files
  2. Coverage File Loading: scoverage.coverage files are deserialized
  3. Measurement Loading: Measurement files (scoverage.measurements.*) are loaded
  4. Data Application: Measurement data is applied to coverage statements
  5. Statement Merging: Statements from all modules are combined with unique IDs

ID Management

During 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))
  }
}

Source Path Resolution

All source paths are resolved relative to the provided sourceRoot parameter to ensure consistent path handling across modules.

Multi-Module Patterns

Standard Maven/SBT Layout

project-root/
├── module1/
│   ├── src/main/scala/
│   └── target/scoverage-data/
├── module2/
│   ├── src/main/scala/
│   └── target/scoverage-data/
└── shared/
    ├── src/main/scala/
    └── target/scoverage-data/

Gradle Layout

project-root/
├── subproject1/
│   ├── src/main/scala/
│   └── build/scoverage/
├── subproject2/
│   ├── src/main/scala/
│   └── build/scoverage/
└── shared/
    ├── src/main/scala/
    └── build/scoverage/

Advanced Aggregation Scenarios

Cross-Platform Builds

// 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("."))

Incremental Coverage Aggregation

// 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()))
  }
}

Performance Considerations

Memory Usage

  • Large multi-module projects can consume significant memory during aggregation
  • Consider processing modules in batches for very large projects
  • Monitor memory usage when aggregating 100+ modules

File I/O Optimization

  • Ensure data directories are on fast storage (SSD preferred)
  • Consider parallel processing for independent modules
  • Cache loaded coverage data when generating multiple report formats

Error Handling

Common Issues

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")
}

Best Practices

  1. Validate Input: Always check that data directories exist and contain valid coverage files
  2. Handle Empty Results: Use the Option-returning aggregate method to handle cases with no data
  3. Path Consistency: Use consistent path resolution across all modules
  4. Memory Management: Monitor memory usage for large aggregations
  5. Error Reporting: Provide clear feedback when modules fail to load or contribute no data

Install with Tessl CLI

npx tessl i tessl/maven-org-scoverage--scalac-scoverage-plugin

docs

aggregation.md

cobertura-reports.md

coverage-model.md

html-reports.md

index.md

io-utils.md

plugin.md

runtime.md

serialization.md

xml-reports.md

tile.json