The JaCoCo results from all subprojects shall be combined.
- Don't make any assumptions about where files are located in the build folders.
- Refer to the sources of truth for getting at needed information.
- Don't make any assumptions about which source sets are being tested. It might be
main
, but it might not. - Handle subprojects that don't JaCoCo.
- Handle Test tasks that don't have any tests, or don't produce a .exec file for some other reason.
- Correctly declare inputs and outputs for up-to-date checking.
- Groovy DSL
- The code below was written for Gradle 6.1.1.
def getProjectList() {
// These projects are considered. Replace with a different list as needed.
subprojects + project
}
task jacocoMerge(type: JacocoMerge) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = 'Merge the JaCoCo data files from all subprojects into one'
project.afterEvaluate { // do it at the end of the config phase to be sure all information is present
FileCollection execFiles = project.objects.fileCollection() // an empty FileCollection
getProjectList().each { Project subproject ->
if (subproject.pluginManager.hasPlugin('jacoco')) {
def testTasks = subproject.tasks.withType(Test)
dependsOn(testTasks) // ensure that .exec files are actually present
testTasks.each { Test task ->
// The JacocoTaskExtension is the source of truth for the location of the .exec file.
JacocoTaskExtension extension = task.getExtensions().findByType(JacocoTaskExtension.class);
if (extension != null) {
execFiles.from extension.getDestinationFile()
}
}
}
}
executionData = execFiles
}
doFirst {
// .exec files might be missing if a project has no tests. Filter in execution phase.
executionData = executionData.filter { it.canRead() }
}
}
def getReportTasks(JacocoReport pRootTask) {
getProjectList().collect {
it.tasks.withType(JacocoReport).findAll { it != pRootTask }
}.flatten()
}
task jacocoRootReport(type: JacocoReport, dependsOn: tasks.jacocoMerge) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = 'Generates an aggregate report from all subprojects'
logger.lifecycle 'Using aggregated file: ' + tasks.jacocoMerge.destinationFile
executionData.from tasks.jacocoMerge.destinationFile
project.afterEvaluate {
// The JacocoReport tasks are the source of truth for class files and sources.
def reportTasks = getReportTasks(tasks.jacocoRootReport)
classDirectories.from project.files({
reportTasks.collect {it.classDirectories}.findAll {it != null}
})
sourceDirectories.from project.files({
reportTasks.collect {it.sourceDirectories}.findAll {it != null}
})
}
}
Hope this helps someone! Let me know in the comments how the code could be better.
The above solution combines many great comments and suggestions from these pages:
Thanks @NikolayMetchev for posting the Gradle example! In fact, I used it to improve my plugin code so that it no longer needs to rely on evaluationDependsOnChildren().
The Gradle example shows how to configure the aggregator task in piecemeal fashion - so whenever a new report task gets added anywhere in the build, the callback is invoked and the aggregator task config is modified.
However, the Gradle example is not complete, as it's missing handling of missing tests, or handling of exclusions, and some other real-life corner cases. Adding support for those made things more complex.