Skip to content

Instantly share code, notes, and snippets.

@tsjensen
Last active March 29, 2024 14:32
Show Gist options
  • Save tsjensen/d8b9ab9e6314ae2f63f4955c44399dad to your computer and use it in GitHub Desktop.
Save tsjensen/d8b9ab9e6314ae2f63f4955c44399dad to your computer and use it in GitHub Desktop.
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:

@rle125
Copy link

rle125 commented Aug 4, 2023

The above example works great on Gradle 6.9.4 + Java 8. However, after upgrading to Gradle 7.3.3. + OpenJDK 17, the report is empty. The test.exec and jacocoMerge.exec are created, but still an empty report. I added some logging and both environments seem identical.

Has anyone been able to migrate this example to work with Gradle 7+ and Java 17?

@NikolayMetchev
Copy link

I've switched over to kotlinx Kover. Works great.

@rle125
Copy link

rle125 commented Aug 4, 2023

Upgrading the jacoco to version 0.8.8 resolved the issue:

Release 0.8.8 (2022/04/05)
New Features

JaCoCo now officially supports Java 17 and 18 (GitHub [#1282](https://github.com/jacoco/jacoco/issues/1282), [#1198](https://github.com/jacoco/jacoco/issues/1198)).

@nbauma109
Copy link

This didn't work for me until I did a few changes.
My subprojects have these tasks:

apply plugin: 'jacoco'

jacoco {
    toolVersion = '0.8.11'
}

task javaCodeCoverageReport(type: JacocoReport, dependsOn: test) {
    sourceDirectories = files(['src/main/java'])
    classDirectories = files(['build/classes/main'])
    executionData = files('build/jacoco/test.exec')
    reports {
        xml.enabled = true
        html.enabled = true
    }
}

And the root project has these tasks :

apply plugin: 'jacoco'

dependencies {
	jacocoAnt 'org.jacoco:org.jacoco.ant:0.8.11'
}

task jacocoMerge(type: JacocoMerge) {
    group = LifecycleBasePlugin.VERIFICATION_GROUP
    description = 'Merge the JaCoCo data files from all subprojects into one'
    destinationFile = file("$buildDir/jacoco/jacocoMerge.exec")
    afterEvaluate {  // do it at the end of the config phase to be sure all information is present
        FileCollection execFiles = files()   // an empty FileCollection
        subprojects.each { subproject ->
            if (subproject.pluginManager.hasPlugin('jacoco')) {
                def testTasks = subproject.tasks.withType(Test)
                // ensure that .exec files are actually present
                testTasks.each { 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() {
    subprojects.collect { subproject ->
        subproject.tasks.withType(JacocoReport).findAll { it.name == 'javaCodeCoverageReport' }
    }.flatten()
}


task jacocoRootReport(type: JacocoReport, dependsOn: tasks.jacocoMerge) {
    group = LifecycleBasePlugin.VERIFICATION_GROUP
    description = 'Generates an aggregate report from all subprojects'

    executionData = files("$buildDir/jacoco/jacocoMerge.exec")

    afterEvaluate {

        // The JacocoReport tasks are the source of truth for class files and sources.
        def reportTasks = getReportTasks()
        classDirectories = files({
            reportTasks.collect {it.classDirectories}.findAll {it != null}
        })
        sourceDirectories = files({
            reportTasks.collect {it.sourceDirectories}.findAll {it != null}
        })
    }
    reports {
        xml.enabled = true
        html.enabled = true
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment