0
# Coverage Aggregation
1
2
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.
3
4
## Core API
5
6
### CoverageAggregator Object
7
8
```scala { .api }
9
object CoverageAggregator {
10
def aggregate(dataDirs: Seq[File], sourceRoot: File): Option[Coverage]
11
def aggregate(dataDirs: Array[File], sourceRoot: File): Option[Coverage]
12
def aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File): Coverage
13
}
14
```
15
16
**Methods:**
17
- `aggregate(dataDirs: Seq[File], sourceRoot: File)` - Aggregate coverage from multiple data directories, returns None if no data found
18
- `aggregate(dataDirs: Array[File], sourceRoot: File)` - Array version for Gradle plugin compatibility
19
- `aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File)` - Direct aggregation, always returns Coverage object
20
21
**Parameters:**
22
- `dataDirs` - Sequence of directories containing scoverage data files
23
- `sourceRoot` - Common source root directory for path resolution
24
25
## Usage Examples
26
27
### Basic Multi-Module Aggregation
28
29
```scala
30
import java.io.File
31
import scoverage.reporter.CoverageAggregator
32
33
// Define data directories for each module
34
val dataDirs = Seq(
35
new File("module1/target/scoverage-data"),
36
new File("module2/target/scoverage-data"),
37
new File("shared/target/scoverage-data")
38
)
39
40
// Common source root for all modules
41
val sourceRoot = new File(".")
42
43
// Aggregate coverage data
44
val aggregatedCoverage = CoverageAggregator.aggregate(dataDirs, sourceRoot)
45
46
aggregatedCoverage match {
47
case Some(coverage) =>
48
println(s"Aggregated coverage: ${coverage.statementCoverageFormatted}%")
49
// Generate reports using the aggregated coverage
50
generateReports(coverage)
51
52
case None =>
53
println("No coverage data found in specified directories")
54
}
55
```
56
57
### SBT Multi-Project Build Integration
58
59
```scala
60
import java.io.File
61
import scoverage.reporter.CoverageAggregator
62
import scoverage.reporter.ScoverageHtmlWriter
63
64
// Collect data directories from all sub-projects
65
val projectDataDirs = Seq(
66
new File("core/target/scoverage-data"),
67
new File("api/target/scoverage-data"),
68
new File("web/target/scoverage-data"),
69
new File("persistence/target/scoverage-data")
70
)
71
72
val rootSourceDir = new File(".")
73
val outputDir = new File("target/aggregated-coverage-report")
74
75
// Aggregate coverage
76
val coverage = CoverageAggregator.aggregatedCoverage(projectDataDirs, rootSourceDir)
77
78
// Generate consolidated HTML report
79
val allSourceDirs = Seq(
80
new File("core/src/main/scala"),
81
new File("api/src/main/scala"),
82
new File("web/src/main/scala"),
83
new File("persistence/src/main/scala")
84
)
85
86
val htmlWriter = new ScoverageHtmlWriter(allSourceDirs, outputDir, Some("UTF-8"))
87
htmlWriter.write(coverage)
88
89
println(s"Aggregated report generated with ${coverage.statementCoverageFormatted}% coverage")
90
```
91
92
### Gradle Multi-Project Build
93
94
```scala
95
import java.io.File
96
import scoverage.reporter.CoverageAggregator
97
98
// Using Array for Gradle compatibility
99
val dataDirArray: Array[File] = Array(
100
new File("subproject1/build/scoverage"),
101
new File("subproject2/build/scoverage"),
102
new File("shared/build/scoverage")
103
)
104
105
val sourceRoot = new File(".")
106
107
val aggregatedCoverage = CoverageAggregator.aggregate(dataDirArray, sourceRoot)
108
109
aggregatedCoverage.foreach { coverage =>
110
println(s"Total statements: ${coverage.statementCount}")
111
println(s"Covered statements: ${coverage.invokedStatementCount}")
112
println(s"Statement coverage: ${coverage.statementCoverageFormatted}%")
113
println(s"Branch coverage: ${coverage.branchCoverageFormatted}%")
114
}
115
```
116
117
### Conditional Aggregation with Error Handling
118
119
```scala
120
import java.io.File
121
import scoverage.reporter.CoverageAggregator
122
123
val possibleDataDirs = Seq(
124
new File("module1/target/scoverage-data"),
125
new File("module2/target/scoverage-data"),
126
new File("optional-module/target/scoverage-data")
127
)
128
129
// Filter to only existing directories with coverage data
130
val validDataDirs = possibleDataDirs.filter { dir =>
131
dir.exists() && dir.isDirectory && {
132
val coverageFile = new File(dir, "scoverage.coverage")
133
coverageFile.exists()
134
}
135
}
136
137
if (validDataDirs.nonEmpty) {
138
val sourceRoot = new File(".")
139
val coverage = CoverageAggregator.aggregatedCoverage(validDataDirs, sourceRoot)
140
141
println(s"Successfully aggregated coverage from ${validDataDirs.size} modules")
142
println(s"Overall coverage: ${coverage.statementCoverageFormatted}%")
143
} else {
144
println("No valid coverage data directories found")
145
}
146
```
147
148
## Aggregation Process
149
150
### Data Collection Phase
151
152
The aggregation process works as follows:
153
154
1. **Directory Scanning**: Each data directory is scanned for coverage files
155
2. **Coverage File Loading**: `scoverage.coverage` files are deserialized
156
3. **Measurement Loading**: Measurement files (`scoverage.measurements.*`) are loaded
157
4. **Data Application**: Measurement data is applied to coverage statements
158
5. **Statement Merging**: Statements from all modules are combined with unique IDs
159
160
### ID Management
161
162
During aggregation, statement IDs are reassigned to ensure uniqueness:
163
164
```scala
165
// Pseudo-code showing ID reassignment process
166
var globalId = 0
167
val mergedCoverage = Coverage()
168
169
dataDirs.foreach { dataDir =>
170
val moduleCoverage = loadCoverageFromDir(dataDir)
171
val measurements = loadMeasurementsFromDir(dataDir)
172
173
// Apply measurements to module coverage
174
moduleCoverage.apply(measurements)
175
176
// Add statements with new unique IDs
177
moduleCoverage.statements.foreach { stmt =>
178
globalId += 1
179
mergedCoverage.add(stmt.copy(id = globalId))
180
}
181
}
182
```
183
184
### Source Path Resolution
185
186
All source paths are resolved relative to the provided `sourceRoot` parameter to ensure consistent path handling across modules.
187
188
## Multi-Module Patterns
189
190
### Standard Maven/SBT Layout
191
192
```
193
project-root/
194
├── module1/
195
│ ├── src/main/scala/
196
│ └── target/scoverage-data/
197
├── module2/
198
│ ├── src/main/scala/
199
│ └── target/scoverage-data/
200
└── shared/
201
├── src/main/scala/
202
└── target/scoverage-data/
203
```
204
205
### Gradle Layout
206
207
```
208
project-root/
209
├── subproject1/
210
│ ├── src/main/scala/
211
│ └── build/scoverage/
212
├── subproject2/
213
│ ├── src/main/scala/
214
│ └── build/scoverage/
215
└── shared/
216
├── src/main/scala/
217
└── build/scoverage/
218
```
219
220
## Advanced Aggregation Scenarios
221
222
### Cross-Platform Builds
223
224
```scala
225
// Aggregate coverage from different platforms (JVM, JS, Native)
226
val platformDataDirs = Seq(
227
new File("target/scala-2.13/scoverage-data"), // JVM
228
new File("target/scala-2.13/scoverage-data-js"), // Scala.js
229
new File("target/scala-2.13/scoverage-data-native") // Scala Native
230
)
231
232
val coverage = CoverageAggregator.aggregatedCoverage(platformDataDirs, new File("."))
233
```
234
235
### Incremental Coverage Aggregation
236
237
```scala
238
// Start with base coverage
239
var aggregatedCoverage = Coverage()
240
241
// Add modules incrementally
242
moduleDataDirs.foreach { dataDir =>
243
val moduleCoverage = CoverageAggregator.aggregatedCoverage(Seq(dataDir), sourceRoot)
244
245
// Merge with existing aggregated coverage
246
// Note: This requires manual statement ID management
247
moduleCoverage.statements.foreach { stmt =>
248
aggregatedCoverage.add(stmt.copy(id = generateUniqueId()))
249
}
250
}
251
```
252
253
## Performance Considerations
254
255
### Memory Usage
256
- Large multi-module projects can consume significant memory during aggregation
257
- Consider processing modules in batches for very large projects
258
- Monitor memory usage when aggregating 100+ modules
259
260
### File I/O Optimization
261
- Ensure data directories are on fast storage (SSD preferred)
262
- Consider parallel processing for independent modules
263
- Cache loaded coverage data when generating multiple report formats
264
265
## Error Handling
266
267
### Common Issues
268
269
**Missing Coverage Files:**
270
```scala
271
// Handle missing scoverage.coverage files
272
val validDirs = dataDirs.filter { dir =>
273
val coverageFile = new File(dir, "scoverage.coverage")
274
if (!coverageFile.exists()) {
275
println(s"Warning: No coverage file found in ${dir.getPath}")
276
false
277
} else {
278
true
279
}
280
}
281
```
282
283
**Inconsistent Source Roots:**
284
```scala
285
// Validate source root accessibility
286
val sourceRoot = new File(".")
287
if (!sourceRoot.exists() || !sourceRoot.isDirectory) {
288
throw new IllegalArgumentException(s"Source root does not exist: ${sourceRoot.getPath}")
289
}
290
```
291
292
**Empty Data Directories:**
293
```scala
294
val coverage = CoverageAggregator.aggregate(dataDirs, sourceRoot)
295
coverage match {
296
case Some(cov) if cov.statementCount == 0 =>
297
println("Warning: Aggregated coverage contains no statements")
298
case Some(cov) =>
299
println(s"Successfully aggregated ${cov.statementCount} statements")
300
case None =>
301
println("No coverage data found to aggregate")
302
}
303
```
304
305
### Best Practices
306
307
1. **Validate Input**: Always check that data directories exist and contain valid coverage files
308
2. **Handle Empty Results**: Use the `Option`-returning `aggregate` method to handle cases with no data
309
3. **Path Consistency**: Use consistent path resolution across all modules
310
4. **Memory Management**: Monitor memory usage for large aggregations
311
5. **Error Reporting**: Provide clear feedback when modules fail to load or contribute no data