or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

aggregation.mdcobertura-reports.mdcoverage-model.mdhtml-reports.mdindex.mdio-utils.mdplugin.mdruntime.mdserialization.mdxml-reports.md

aggregation.mddocs/

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