Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Gradle: Create a JaCoCo Report aggregating all subprojects

Create an Aggregated JaCoCo Report

The JaCoCo results from all subprojects shall be combined.

Requirements

  • 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.

Code

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.

Credits

The above solution combines many great comments and suggestions from these pages:

@VenomVendor

This comment has been minimized.

Copy link

@VenomVendor VenomVendor commented Mar 20, 2020

DSL version of the same. Re-verify before usage.

fun getProjectList() = subprojects + project

@Suppress("UnstableApiUsage")
task<JacocoMerge>("jacocoMerge") {
    group = LifecycleBasePlugin.VERIFICATION_GROUP
    description = "Merge the JaCoCo data files from all subprojects into one"
    afterEvaluate {
        // An empty FileCollection
        val execFiles = objects.fileCollection()
        getProjectList().forEach { subProject: Project ->
            if (subProject.pluginManager.hasPlugin("jacoco")) {
                val testTasks = subProject.tasks.withType<Test>()
                // ensure that .exec files are actually present
                dependsOn(testTasks)

                testTasks.forEach { task: Test ->
                    // The JacocoTaskExtension is the source of truth for the location of the .exec file.
                    val extension = task.extensions.findByType(JacocoTaskExtension::class.java)
                    extension?.let {
                        execFiles.from(it.destinationFile)
                    }
                }
            }
        }
        executionData = execFiles
    }
    doFirst {
        // .exec files might be missing if a project has no tests. Filter in execution phase.
        executionData = executionData.filter { it.canRead() }
    }
}

fun getReportTasks(jacocoReport: JacocoReport): List<JacocoReport> {
    return getProjectList().map {
        return it.tasks.withType<JacocoReport>()
            .filter { report -> report != jacocoReport }
    }
}

@Suppress("UnstableApiUsage")
task<JacocoReport>("jacocoRootReport") {
    dependsOn("jacocoMerge")
    group = LifecycleBasePlugin.VERIFICATION_GROUP
    description = "Generates an aggregate report from all subProjects"

    val jacocoMergeTask = tasks.named("jacocoMerge", JacocoMerge::class).orNull
    val destFile = jacocoMergeTask?.destinationFile
    logger.lifecycle("Using aggregated file: $destFile")
    executionData.from(destFile)
    val that = this
    project.afterEvaluate {
        // The JacocoReport tasks are the source of truth for class files and sources.
        val reportTasks = getReportTasks(that)

        classDirectories.from(project.files(reportTasks.mapNotNull { it.classDirectories }))
        sourceDirectories.from(project.files(reportTasks.mapNotNull { it.sourceDirectories }))
    }
}
@luispollo

This comment has been minimized.

Copy link

@luispollo luispollo commented Mar 25, 2020

@tsjensen @VenomVendor, thank you so much for posting these examples. I've spent several hours trying to get this to work on one of my multi-module projects and, after coming across many other posts and suggested solutions, yours was the one that seemed the most promising, but unfortunately it failed for me with this exception:

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':jacocoMerge'.
        at org.gradle.execution.plan.LocalTaskNode.resolveMutations(LocalTaskNode.java:244)
        at org.gradle.execution.plan.DefaultExecutionPlan.getResolvedMutationInfo(DefaultExecutionPlan.java:607)
        at org.gradle.execution.plan.DefaultExecutionPlan.selectNext(DefaultExecutionPlan.java:533)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$executeNextNode$1(DefaultPlanExecutor.java:166)
        at org.gradle.internal.resources.DefaultResourceLockCoordinationService.withStateLock(DefaultResourceLockCoordinationService.java:45)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:155)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:124)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
        at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
Caused by: org.gradle.api.internal.provider.MissingValueException: Cannot query the value of this property because it has no value available.
        at org.gradle.api.internal.provider.AbstractMinimalProvider.get(AbstractMinimalProvider.java:92)
        at org.gradle.testing.jacoco.tasks.JacocoMerge.getDestinationFile(JacocoMerge.java:65)
        at org.gradle.testing.jacoco.tasks.JacocoMerge_Decorated.getDestinationFile(Unknown Source)
        at org.gradle.api.internal.tasks.properties.bean.AbstractNestedRuntimeBeanNode$BeanPropertyValue$1$1.create(AbstractNestedRuntimeBeanNode.java:79)
        at org.gradle.internal.deprecation.DeprecationLogger.whileDisabled(DeprecationLogger.java:218)
        at org.gradle.api.internal.tasks.properties.bean.AbstractNestedRuntimeBeanNode$BeanPropertyValue$1.get(AbstractNestedRuntimeBeanNode.java:75)
        at com.google.common.base.Suppliers$NonSerializableMemoizingSupplier.get(Suppliers.java:167)
        at org.gradle.api.internal.tasks.properties.bean.AbstractNestedRuntimeBeanNode$BeanPropertyValue.call(AbstractNestedRuntimeBeanNode.java:145)
        at org.gradle.util.GUtil.uncheckedCall(GUtil.java:425)
        at org.gradle.util.DeferredUtil.unpackNestableDeferred(DeferredUtil.java:64)
        at org.gradle.util.DeferredUtil.unpack(DeferredUtil.java:38)
        at org.gradle.api.internal.tasks.properties.FileParameterUtils.resolveOutputFilePropertySpecs(FileParameterUtils.java:107)
        at org.gradle.execution.plan.LocalTaskNode$1.lambda$visitOutputFileProperty$1(LocalTaskNode.java:208)
        at org.gradle.execution.plan.LocalTaskNode.withDeadlockHandling(LocalTaskNode.java:285)
        at org.gradle.execution.plan.LocalTaskNode.access$000(LocalTaskNode.java:52)
        at org.gradle.execution.plan.LocalTaskNode$1.visitOutputFileProperty(LocalTaskNode.java:204)
        at org.gradle.api.internal.tasks.properties.annotations.AbstractOutputPropertyAnnotationHandler.visitPropertyValue(AbstractOutputPropertyAnnotationHandler.java:50)
        at org.gradle.api.internal.tasks.properties.bean.AbstractNestedRuntimeBeanNode.visitProperties(AbstractNestedRuntimeBeanNode.java:58)
        at org.gradle.api.internal.tasks.properties.bean.RootRuntimeBeanNode.visitNode(RootRuntimeBeanNode.java:32)
        at org.gradle.api.internal.tasks.properties.DefaultPropertyWalker.visitProperties(DefaultPropertyWalker.java:41)
        at org.gradle.api.internal.tasks.TaskPropertyUtils.visitProperties(TaskPropertyUtils.java:42)
        at org.gradle.api.internal.tasks.TaskPropertyUtils.visitProperties(TaskPropertyUtils.java:32)
        at org.gradle.execution.plan.LocalTaskNode.resolveMutations(LocalTaskNode.java:201)
        ... 9 more

I'm running with gradle 6.2.2 and using the Kotlin DSL.

Any ideas?

@barfuin

This comment has been minimized.

Copy link

@barfuin barfuin commented Mar 25, 2020

I wrote a Gradle plugin based on this solution (well, the plugin was already there, but I added report aggregation to it as a feature).
Maybe it'll help somebody, either directly, or as sample code! https://gitlab.com/barfuin/gradle-jacoco-log/-/blob/master/README.md

@luispollo

This comment has been minimized.

Copy link

@luispollo luispollo commented Mar 26, 2020

Responding to my own question above in case it helps others: the problem for me was that I was configuring repositories in the subprojects section, and they needed to be in the allprojects section so that the top-level project has access to them.

@NikolayMetchev

This comment has been minimized.

Copy link

@NikolayMetchev NikolayMetchev commented Apr 17, 2020

I tried this script and it appears as though for me getReportTasks is returning an empty list. It seems none of the subprojects have a JacocoReport task when that function is evaluated. however I can clearly run those tasks manually! This is with gradle 6.3

allprojects {
    apply plugin: 'jacoco'

    jacoco {
        toolVersion = "0.8.5"
    }
  //...
}

subprojects {
    jacocoTestReport {
        reports {
            xml.enabled true
            csv.enabled false
            html.enabled true
        }
    }
    test.finalizedBy jacocoTestReport
}
@tsjensen

This comment has been minimized.

Copy link
Owner Author

@tsjensen tsjensen commented Apr 17, 2020

@NikolayMetchev getReportTasks() must be run inside an afterEvaluate() closure, as shown in the original gist above. Else, your tasks have not been created when the script runs.

@NikolayMetchev

This comment has been minimized.

Copy link

@NikolayMetchev NikolayMetchev commented Apr 17, 2020

@NikolayMetchev getReportTasks() must be run inside an afterEvaluate() closure, as shown in the original gist above. Else, your tasks have not been created when the script runs.

Yes. I didn't modify the code. I debugged it and traced the problem to that...It is being called in afterEvaluate() because that is what is happening in the jacocoRootReport task.

@dhakehurst

This comment has been minimized.

Copy link

@dhakehurst dhakehurst commented Jul 27, 2020

DSL version of the same. Re-verify before usage.

bug fix:

fun getReportTasks(jacocoReport: JacocoReport): List<JacocoReport> {
    return getProjectList().flatMap {
        it.tasks.withType<JacocoReport>().filter { it.name == "jacocoTestReport" }
                .filter { report -> report != jacocoReport }
    }
}
@NikolayMetchev

This comment has been minimized.

Copy link

@NikolayMetchev NikolayMetchev commented Jul 27, 2020

@dhakehurst

This comment has been minimized.

Copy link

@dhakehurst dhakehurst commented Jul 28, 2020

Gradle docs has a working version of this:
https://docs.gradle.org/6.5.1/samples/sample_jvm_multi_project_with_code_coverage.html

that does not work if some of your modules do not have tests, unfortunately

@barfuin

This comment has been minimized.

Copy link

@barfuin barfuin commented Jul 30, 2020

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.

@soberich

This comment has been minimized.

Copy link

@soberich soberich commented Sep 8, 2020

(I copy my comment from here)


new jvm-ecosystem plugin to create aggregated report
and
Higher level APIs for the Java ecosystem.pdf proposes a way to write all boilerplate to create resolvable configurations

    //NOT like this
    ...
    isVisible = false
    isCanBeResolved = true
    isCanBeConsumed = false
    extendsFrom(configurations.implementation.get())
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("source-folders"))
    }
    ...

in idiomatic way, which Gradle guys already dogfood

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.