Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Aggregated Jacoco reports in a multi-project Gradle build
allprojects {
apply plugin: 'java'
apply plugin: 'jacoco'
repositories {
jcenter()
}
jacoco {
toolVersion = '0.7.1.201405082137'
}
}
subprojects {
dependencies {
testCompile 'junit:junit:4.11'
}
jacocoTestReport {
additionalSourceDirs = files(sourceSets.main.allSource.srcDirs)
sourceDirectories = files(sourceSets.main.allSource.srcDirs)
classDirectories = files(sourceSets.main.output)
reports {
html.enabled = true
xml.enabled = true
csv.enabled = false
}
}
}
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
dependsOn = subprojects.test
additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
classDirectories = files(subprojects.sourceSets.main.output)
executionData = files(subprojects.jacocoTestReport.executionData)
reports {
html.enabled = true
xml.enabled = true
csv.enabled = false
}
}
@huxi

This comment has been minimized.

Copy link

commented Jul 23, 2014

jacocoRootReport doesn't work if some subprojects don't have any tests at all because this causes the onlyIf of JacocoReport to be false.

My workaround looks like this:

task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
    dependsOn = subprojects.test
    additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
    sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
    classDirectories = files(subprojects.sourceSets.main.output)
    executionData = files(subprojects.jacocoTestReport.executionData)
    reports {
        html.enabled = true
        xml.enabled = true
        csv.enabled = false
    }
    onlyIf = {
        true
    }
    doFirst {
        executionData = files(executionData.findAll {
            it.exists()
        })
    }
}

I think this is actually a bug in JacocoReport. The onlyIf should be removed and the task should only actually use existing files.

@sleroy

This comment has been minimized.

Copy link

commented Dec 2, 2014

Thank you for initial solution and the feedback, I was stucked with the same problem.

@Skamandros

This comment has been minimized.

Copy link

commented Jun 22, 2015

Thanks, this gist helped me a lot. If it's possible to determine the set of Java projects with tests, one can also use a custom project set instead of subprojects.

@mjdetullio

This comment has been minimized.

Copy link

commented Aug 26, 2015

A problem I ran into was these examples didn't work with custom source sets, since those source set closures are not yet evaluated when configuring JaCoCo. In addition, some subprojects were not Java projects. Here's my solution:

In root build.gradle:

allprojects {
    repositories {
        jcenter()
    }
}

apply plugin: 'jacoco'

task jacocoTestReport(type: JacocoReport) {
    sourceDirectories = files()
    classDirectories = files()
    executionData = files()

    reports {
        html.enabled = true
        xml.enabled = true
        csv.enabled = false
    }

    // Work-around to allow us to build list of executionData files in doFirst
    onlyIf = {
        true
    }

    /*
     * Builds list of source dirs, class dirs, and executionData files
     * when task is run, not at script evaluation time
     */
    doFirst {
        subprojects.findAll { subproject ->
            subproject.pluginManager.hasPlugin('java')
        }.each { subproject ->
            additionalSourceDirs files((Set<File>) subproject.sourceSets.main.allJava.srcDirs)
            additionalClassDirs ((FileCollection) subproject.sourceSets.main.output)
            if (subproject.pluginManager.hasPlugin('jacoco')) {
                executionData subproject.tasks.jacocoTestReport.executionData
            }
        }

        executionData = files(executionData.findAll {
            it.exists()
        })
    }
}

task build {
    dependsOn tasks.jacocoTestReport
}

Root java-common.gradle:

// Common settings for projects that already apply the 'java' plugin

tasks.withType(JavaCompile) {
    options.debug = true
}

apply plugin: 'jacoco'

// Must be after sourceSets closure
jacocoTestReport {
    dependsOn tasks.test

    //noinspection GroovyAssignabilityCheck
    sourceSets sourceSets.main
    reports {
        html.enabled = true
        xml.enabled = true
        csv.enabled = false
    }
}

tasks.check.dependsOn tasks.jacocoTestReport
rootProject.tasks.jacocoTestReport.dependsOn tasks.test

In each Java module's build.gradle:

apply plugin: 'java'

sourceSets {
    // customizations
}

apply from: rootProject.file('java-common.gradle')

And since the JacocoReport task type doesn't support "groups" in the report structure, I also wrote a completely custom task that uses the Ant report task directly. On the report index this will break up each module instead of clumping all the packages together. You can use this in place of the other example.

In root build.gradle, along with same per-project settings mentioned above:

allprojects {
    repositories {
        jcenter()
    }
}

apply plugin: 'jacoco'

task jacocoTestReport << {
    FileCollection executionData = files()

    subprojects.findAll { subproject ->
        subproject.pluginManager.hasPlugin('java') && subproject.pluginManager.hasPlugin('jacoco')
    }.each { subproject ->
        executionData += subproject.tasks.jacocoTestReport.executionData
    }

    executionData = files(executionData.findAll {
        it.exists()
    })

    ant.taskdef(name: 'jacocoReport', classname: 'org.jacoco.ant.ReportTask',
            classpath: configurations.jacocoAnt.asPath)

    ant.jacocoReport {
        executiondata {
            executionData.addToAntBuilder(ant, 'resources')
        }
        structure(name: project.name) {
            subprojects.findAll { subproject ->
                subproject.pluginManager.hasPlugin('java')
            }.each { subproject ->
                group(name: subproject.name) {
                    classfiles {
                        ((FileCollection) subproject.sourceSets.main.output).filter {
                            it.exists()
                        }.addToAntBuilder(ant, 'resources')
                    }
                    sourcefiles {
                        files((Set<File>) subproject.sourceSets.main.allJava.srcDirs).filter {
                            it.exists()
                        }.addToAntBuilder(ant, 'resources')
                    }
                }
            }
        }
        html(destdir: "${buildDir}/reports/jacoco/${name}/html")
        xml(destfile: "${buildDir}/reports/jacoco/${name}/${name}.xml")
        // Uncomment for CSV
        //csv(destfile: "${buildDir}/reports/jacoco/${name}/${name}.csv")
    }
}

task build {
    dependsOn tasks.jacocoTestReport
}

If you want the group names to go by path instead of just name, you can use subproject.projectDir.path.substring(projectDir.path.length() + 1) in place of subproject.name.

@nitin88

This comment has been minimized.

Copy link

commented Oct 20, 2015

Hi,

I am looking a way to exclude classes from aggregated report. Any help is welcome!

task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
    dependsOn = subprojects.test
    additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
    sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
    classDirectories = files(subprojects.sourceSets.main.output)
    executionData = files(subprojects.jacocoTestReport.executionData)  //how to exclude some package/classes com.test.**
    reports {
        html.enabled = true
        xml.enabled = true
        csv.enabled = false
    }
    onlyIf = {
        true
    }
    doFirst {
        executionData = files(executionData.findAll {
            it.exists()
        })
    }
}
@nz1845

This comment has been minimized.

Copy link

commented Dec 1, 2015

I was able to exclude classes for example by adding this to the task

afterEvaluate {
        classDirectories = files(classDirectories.files.collect {
            fileTree(dir: it,
                    exclude: ['*com/package/a*',
                              '*com/package/b*',
                              '*com/something*'])
        })
    }
@vjkm

This comment has been minimized.

Copy link

commented Dec 19, 2015

Hi All

I have a problem using this
As I am using AndroidTest folder to place our tests and we are using the task connectedCheck

Nothing as sujjested above is working!!!

Can someone help me out!

Thanks
vkm

@philbert

This comment has been minimized.

Copy link

commented Aug 23, 2016

@nitin88 I discovered this code here which works for me:

// aggregates jacoco results from all subprojects and core project and generate a report
task jacocoRootTestReport(type: JacocoReport) {

    sourceSets sourceSets.main

    def jacocoTestFiles = ["$buildDir/jacoco/test.exec"]
    subprojects.each { p ->
        def coverageFileLocation = "$p.buildDir/jacoco/test.exec"
        if (new File(coverageFileLocation).exists()) {
            jacocoTestFiles << coverageFileLocation
        }
    }

    logger.info('Aggregating next JaCoCo Coverage Files: {}', jacocoTestFiles)
    executionData files(jacocoTestFiles)

    reports {
        xml.enabled false
        csv.enabled false
        html.enabled true
    }
}
@dnltsk

This comment has been minimized.

Copy link

commented Jan 6, 2018

thanks to @philbert and @nitin88 for posting their solutions. All my attempts do end up in a path may not be null or empty string. path='null' exception message from gradle. The command I am using is gradle clean test jacocoRootReport

@dnltsk

This comment has been minimized.

Copy link

commented Jan 7, 2018

@philbert and @nitin88 I figured out that the jacocoRootTestReport and jacocoRootReport task doesn't work with code written in Kotlin. https://github.com/dnltsk/blog-2016-10-gradle-multi-module-coverage

@JLLeitschuh

This comment has been minimized.

Copy link

commented Feb 14, 2018

This is the solution that I got working for Junit 5 with the Gradle Kotlin DSL:

val jacocoTestResultTaskName = "jacocoJunit5TestReport"
subprojects {
    val junitPlatformTest: JavaExec by tasks // You don't need this if using Junt 4

    jacoco {
        toolVersion = "0.8.0"
        applyTo(junitPlatformTest) // You don't need this if using Junt 4
    }

    val sourceSets = java.sourceSets

    task<JacocoReport>(jacocoTestResultTaskName) { // For Junit4 I think this becomes `tasks.getting(JacocoReport::class) { ...`

        executionData(junitPlatformTest) // You don't need this if using Junt 4
        dependsOn(junitPlatformTest)

        sourceSets(sourceSets["main"])
        sourceDirectories = files(sourceSets["main"].allSource.srcDirs)
        classDirectories = files(sourceSets["main"].output)
        reports {
            html.isEnabled = true
            xml.isEnabled = true
            csv.isEnabled = false
        }
    }
}

task<JacocoReport>("jacocoRootReport") {
    val jacocoReportTasks =
        subprojects
            .map { it.tasks[jacocoTestResultTaskName] as JacocoReport } // For Junit 4 use `.flatMap { it.tasks.withType<JacocoReport>() }`
    dependsOn(jacocoReportTasks) // For Junit 4 this becomes `dependsOn(tasks["test"])`

    val executionData = jacocoReportTasks.map { it.executionData }
    executionData(*executionData.toTypedArray())

    // Pre-initialize these to empty collections to prevent NPE on += call below.
    additionalSourceDirs = files()
    sourceDirectories = files()
    classDirectories = files()

    subprojects.forEach { testedProject ->
        val sourceSets = testedProject.java.sourceSets
        this@task.additionalSourceDirs += files(sourceSets["main"].allSource.srcDirs)
        this@task.sourceDirectories += files(sourceSets["main"].allSource.srcDirs)
        this@task.classDirectories += files(sourceSets["main"].output)
    }

    reports {
        html.isEnabled = true
        xml.isEnabled = true
        csv.isEnabled = false
    }
}

/**
 * Retrieves the [java][org.gradle.api.plugins.JavaPluginConvention] project convention.
 */
val Project.`java`: org.gradle.api.plugins.JavaPluginConvention
    get() = convention.getPluginByName("java")
@CarrierDirectmgao

This comment has been minimized.

Copy link

commented Aug 8, 2018

Like to share the code that I got working for Kotlin junit: 4.12

allprojects {
apply plugin: 'jacoco'

repositories {
    jcenter()
}

jacoco {
    toolVersion = '0.8.1'
}

}
subprojects {
apply plugin: 'kotlin'
apply plugin: "kotlin-spring"
apply plugin: 'io.spring.dependency-management'

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/libs-snapshot' }
    maven { url 'https://repo.spring.io/libs-milestone' }
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.boot:spring-boot-dependencies:${spring_boot_version}"
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Finchley.M9'
    }
}

dependencies {
    compile "com.github.wnameless:json-flattener:0.5.0"
    compile "com.google.guava:guava:$guava_version"
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

    testCompile "junit:junit:4.12"
    testCompile "org.mockito:mockito-core:$mockito_version"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

jacocoTestReport {
    sourceDirectories = files(sourceSets.getByName("main").allSource.srcDirs)
    afterEvaluate {
        classDirectories = files(sourceSets.getByName("main").output.files.collect {
            fileTree(dir: it, exclude: ['**/*body*', '**/*inlined*'])
        })
    }
    reports {
        html.enabled = true
        xml.enabled = true
        csv.enabled = false
    }
}

}
task jacoco(type: JacocoReport) {
sourceDirectories = files()
classDirectories = files()
executionData = files()

getSubprojects().forEach{ subproject ->
    def coverageFileLocation = "${subproject.buildDir}/jacoco/test.exec"
    if (new File(coverageFileLocation).exists()) {
        executionData += subproject.tasks.findByName("jacocoTestReport").property("executionData")
        sourceDirectories += subproject.tasks.findByName("jacocoTestReport").property("sourceDirectories")
        classDirectories += files(subproject.sourceSets.getByName("main").output.files.collect {
            fileTree(dir: it, exclude: ['**/*body*', '**/*inlined*'])
        })
    }
}

reports {
    xml.enabled = true
    csv.enabled = false
    html.enabled = true

}

}

task wrapper(type: Wrapper) {
gradleVersion = '4.8.1' //version required
}

Note: Excluding the library classes created for library source file ServerRequestExtensions.kt, ServerResponseExtensions.kt, and Extensions.kt(which caused code climate failure with error message: could not find those files) by Kotlin compiler.

@twyatt

This comment has been minimized.

Copy link

commented Nov 1, 2018

For others that find there way to this helpful Gist, the gradle-android-junit-jacoco-plugin offers a simplified solution to many of the problems being addressed in this Gist's discussion thread.

@aalmiray

This comment has been minimized.

Copy link
Owner Author

commented Mar 8, 2019

The org.kordamp.gradle.jacoco plugin takes care of mane aspects shown here https://aalmiray.github.io/kordamp-gradle-plugins/#_org_kordamp_gradle_jacoco

@karippery

This comment has been minimized.

Copy link

commented Mar 11, 2019

in build.gradle.kts ".property("executionData") this part i can't translate ..

please help me to convert .kts file

    executionData += subproject.tasks.findByName("jacocoTestReport").property("executionData")
    sourceDirectories += subproject.tasks.findByName("jacocoTestReport").property("sourceDirectories")
@vlsi

This comment has been minimized.

Copy link

commented Mar 16, 2019

Here's my approach with Gradle 5.2.1 + Kotlin DSL.

Note: it allows Gradle to skip task creation unless required.

Note: doFirst should not be used for executionData configuration as it is not compatible with Gradle's incremental model.
In other words, the input parameters for the task should be configured at, well, configuration time otherwise Gradle would think the task is UP-TO-DATE.

fun reportsForHumans() = !(System.getenv()["CI"]?.toBoolean() ?: false)

val jacocoReport by tasks.registering(JacocoReport::class) {
    group = "Coverage reports"
    description = "Generates an aggregate report from all subprojects"
    executionData(jacocoMerge)
}

allprojects {
    plugins.withType<JacocoPlugin> { // this is executed for each project that has Jacoco plugin applied
        the<JacocoPluginExtension>().toolVersion = "0.8.3"

        val testTasks = tasks.withType<Test>()
        testTasks.configureEach {
            extensions.configure<JacocoTaskExtension> {
                // We don't want to collect coverage for third-party classes
                includes?.add("com.acme.*")
            }
        }

        jacocoReport {
            // Note: this creates a lazy collection
            // Some of the projects might fail to create a file (e.g. no tests or no coverage),
            // So we check for file existence. Otherwise JacocoReport would fail
            val execFiles = files(testTasks).filter { it.exists() && it.name.endsWith(".exec") }
            executionData(execFiles)
        }

        tasks.withType<JacocoReport>().configureEach {
            reports {
                html.isEnabled = reportsForHumans()
                xml.isEnabled = !reportsForHumans()
            }
        }
        // Add each project to combined report
        configure<SourceSetContainer> {
            val mainCode = main.get()
            jacocoReport {
                additionalSourceDirs.from(mainCode.allJava.srcDirs)
                sourceDirectories.from(mainCode.allSource.srcDirs)
                // IllegalStateException: Can't add different class with same name: module-info
                // https://github.com/jacoco/jacoco/issues/858
                classDirectories.from(mainCode.output.asFileTree.matching {
                    exclude("module-info.class")
                })
            }
        }
    }
}
@slavik57

This comment has been minimized.

Copy link

commented Apr 21, 2019

I tried the suggestion of @huxi and got the same error as @dnltsk got: path may not be null or empty string. path='null'
After a bit of fiddling around I managed to solve this error by adding the Jacoco plugin for all the projects instead of only subprojects:

allprojects {
    ...
    apply plugin: 'jacoco'
    jacoco {
        toolVersion = "0.8.3"
    }
}

Then I got an error Cannot resolve external dependency org.jacoco:org.jacoco.ant:0.8.3 because no repositories are defined.
Then I moved the repositories section also to the allprojects scope instead of only the subprojects

After doing all of that it worked like a charm.

The final root build.gradle file:

allprojects {
    ...
    apply plugin: 'jacoco'
    jacoco {
        toolVersion = "0.8.3"
    }

    repositories {
        ...
        jcenter()
        mavenLocal()
    }
}

subprojects {
    ...

    def jacocoExecutionDataFiles = fileTree(buildDir).include("/jacoco/*.exec")
    jacocoTestReport {
        executionData = jacocoExecutionDataFiles
    }
}
...
task jacocoRootReport(type: JacocoReport) {
    description = 'Generates an aggregate report from all subprojects'

    additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
    sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
    classDirectories = files(subprojects.sourceSets.main.output)
    executionData = files(subprojects.jacocoTestReport.executionData)
}
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.