Merging jacoco reports for coveralls

plugins

(Thomas Andolf) #1

Hi, first of all im a long time maven user trying to learn groovy and im having some troubles.

Tried the following:

https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/

https://discuss.gradle.org/t/merge-jacoco-coverage-reports-for-multiproject-setups/12100/6

Im building a multi module project using spring boot and neo4j. Building it with travis-ci and wanted to use coveralls but i cant merge the reports.

When printing the “executionData” it seems like it can only find “test.exec” and not my “integrationTest.exec”

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()
    })
}

subprojects {
    sourceSets {
    integrationTest {
        java {
            compileClasspath += main.output + test.output
            runtimeClasspath += main.output + test.output
            srcDir file('src/integrationTest/java')
        }
        resources.srcDir file('src/integrationTest/resources')
    }
}

configurations {
    integrationTestCompile.extendsFrom testCompile
    integrationTestRuntime.extendsFrom testRuntime
}

task integrationTest(type:Test){

    testClassesDir = sourceSets.integrationTest.output.classesDir
    classpath = sourceSets.integrationTest.runtimeClasspath

    testLogging {
        events "passed", "skipped", "failed"
    }
}

coveralls {
    sourceDirs = subprojects.sourceSets.main.allSource.srcDirs.flatten()
    jacocoReportPath = "${buildDir}/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
}

anything i have missed?


(Ben Manes) #2

You may have ordering issue in your script, as you configure the task prior to configuring the source sets to include the integrationTest type. Gradle splits the build into n init (bootstrap), configure, and execution phase. The script is configured in order, so by moving the jacocoRootReport task near the bottom it might just “work”. Its one of the few quirks of using a scripting language than a pure meta model.


(Thomas Andolf) #3

thanks for the answer.
i placed the task at the bottom and i still get the same result.

build/jacoco/test.exec
build/jacoco/integrationTest.exec

but when printing i only get test.exec


(Thomas Andolf) #4

i’ll post the complete build file. Any suggestions?

https://github.com/Tandolf/lift/blob/master/build.gradle


(Ben Manes) #5

It might be due to jacocoRootReport having a dependency on subprojects.test and not also subprojects.integrationTest. Perhaps integrationTest output is not being captured or getting overwritten. The first step is to see if the individual project’s report comes out properly. If so then you can debug the aggregation.


(Thomas Andolf) #6

the execution report is coming out good i think cause in the folder i have an integrationTest.exec and a test.exec.

when running the JacocoRootReport task and changing its dependency to subproject.integrationTest i see no difference. When running the task with the debug flag gives me in the log:

22:53:34.361 [INFO] [org.gradle.api.internal.project.ant.AntLoggingAdapter] [ant:jacocoReport] Loading execution data file /Users/Thomas/Documents/code/Java/lift/lift-controller/build/jacoco/test.exec

but in that same folder there is a “integrationTest.exec” that it doesnt seem to find. Its like it is hardcoded to just find test.exec. I dont get anything of this, its so poorly documented all of this.


(Hámorszky, Balázs) #7

One possible solution (with some sonar specific parts):

def getJacocoMergeTask(Project proj){
    def jmClosure =  {
        doFirst {
            logger.info "${path} started"
            executionData.each { ed ->
                logger.info "${path} data: ${ed}"
            }
        }
        onlyIf {
            executionData != null && !executionData.isEmpty()
        }
    }

    def jacocoMerge = null
    if(!proj.tasks.findByName('jacocoMerge')){

        jacocoMerge = proj.tasks.create('jacocoMerge', JacocoMerge.class)
        jacocoMerge.configure jmClosure

        // sonar specific part
        proj.rootProject.tasks["sonarqube"].mustRunAfter jacocoMerge

        proj.sonarqube {
            properties {
                property "sonar.jacoco.reportPaths", jacocoMerge.destinationFile.absolutePath
            }
        }
        // end of sonar specific part

        logger.info "${jacocoMerge.path} created"
    } else {
        jacocoMerge = proj.tasks["jacocoMerge"]
    }
    jacocoMerge
}


afterEvaluate { project ->
    def jacocoMerge = getJacocoMergeTask(project)

    project.tasks.withType(Test) { task ->
        logger.info "${jacocoMerge.path} cfg: ${task.path}"

        task.finalizedBy jacocoMerge
        jacocoMerge.dependsOn task

        task.doLast {
            logger.info "${jacocoMerge.path} executionData ${task.path}"
            jacocoMerge.executionData task
        }

        def cfg = configurations.getByName("${task.name}Runtime")
        logger.info "${project.path} process config: ${cfg.name}"

        cfg.getAllDependencies().withType(ProjectDependency.class).each { pd ->
            def depProj = pd.dependencyProject
            logger.info "${task.path} dependsOn ${depProj.path}"
            def jm = getJacocoMergeTask(depProj)

            task.finalizedBy jm
            jm.dependsOn task

            task.doLast {
                logger.info "${jm.path} executionData ${task.path}"
                jm.executionData task
            }
        }
    }
}

This will merge all the executionData from all the projects, that used a certain project during testing as a dependency.


(Cristian) #8

If you are using Sonarqube you must first merge the Jacoco execution data and then tell it to use that instead of the individual exec files generated by each submodule.

Here’s how it would look like:

def allTestCoverageFile = "$buildDir/jacoco/allTestCoverage.exec"

sonarqube {
    properties {
        property "sonar.projectKey", "your.org:YourProject"
        property "sonar.projectName", "YourProject"
        property "sonar.jacoco.reportPaths", allTestCoverageFile
    }
}

task jacocoMergeTest(type: JacocoMerge) {
    destinationFile = file(allTestCoverageFile)
    executionData = project.fileTree(dir: '.', include:'**/build/jacoco/test.exec')
}

task jacocoMerge(dependsOn: ['jacocoMergeTest']) {
    // used to run the other merge tasks
}

subprojects {
    sonarqube {
        properties {
            property "sonar.jacoco.reportPath", allTestCoverageFile
        }
    }
}

In a nutshell:

  • First, we define a global coverage file output for our test reports (allTestCoverageFile).

  • Then we need to tell Sonarqube to use that file (using sonar.jacoco.reportPaths). But notice we also have to do it in the subprojects closure. This is extremely important. Don’t miss it.

  • Finally, we create a custom task that extends from JacocoMerge (an incubating class from the Jacoco plugin), that merges all the test coverage reports from all projects (executionData) into our allTestCoverageFile.