k3b
k3b

Reputation: 14755

Gradle jacoco coverage report with more than one submodule(s)?

Does anybody know how to configure a gradle file for java jacoco report that contain codecoverage of more than one gradle submodule?

my current approach only shows codecoverage of the current submodule but not codecoverage of a sibling-submodul.

I have this project structure

- build.gradle (1)
- corelib/
    - build.gradle (2)
    - src/main/java/package/Core.java
- extlib/
    - build.gradle (3)
    - src/main/java/package/Ext.java
    - src/test/java/package/Integrationtest.java

when i execute gradlew :extlib:check :extlib:jacocoTestReport the junit-test "Integrationtest.java" is executed and a codecoverage report is generated that does not contain codecoverage for corelib classes like Core.java

The result should include the codecoverage of Ext.java and Core.java

I already read

but found no clues

here is content of the gradle files

// root build.gradle (1)
// Top-level build file where you can add configuration options 
// common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
allprojects {
    repositories {
        jcenter()
    }
}

// build.gradle (2) subproject build file for corelib.
apply plugin: 'java'
apply plugin: 'jacoco'

dependencies {
}

jacocoTestReport {
    reports {
        xml.enabled true
        html.enabled true
    }
}

// build.gradle (3) subproject build file for extlib.
apply plugin: 'java'
apply plugin: 'jacoco'

dependencies {
    compile project(':corelib')
    testCompile 'junit:junit:4.11'

    // this does not compile
    // jacocoTestReport project(':pixymeta-core-lib')
}

jacocoTestReport {
    reports {
        xml.enabled true
        html.enabled true
    }
}

// workaround because android-studio does not make the test resources available
// see https://code.google.com/p/android/issues/detail?id=64887#c13
task copyTestResources(type: Copy) {
    from sourceSets.test.resources
    into sourceSets.test.output.classesDir
}
processTestResources.dependsOn copyTestResources

[Update 2016-08-01]

thanks to @Benjamin Muschko i also tried in the root gradle file

// https://discuss.gradle.org/t/merge-jacoco-coverage-reports-for-multiproject-setups/12100/6
// https://docs.gradle.org/current/dsl/org.gradle.testing.jacoco.tasks.JacocoMerge.html
task jacocoMerge(type: JacocoMerge) {
   subprojects.each { subproject ->
      executionData subproject.tasks.withType(Test)
   } 

}

but got error message (with gradle-2.14)

* What went wrong:
Some problems were found with the configuration of task ':jacocoMerge'.
> No value has been specified for property 'jacocoClasspath'.
> No value has been specified for property 'executionData'.
> No value has been specified for property 'destinationFile'.

there is also the gradle plugin https://github.com/paveldudka/JacocoEverywhere where i have asked for mulit-submodule support https://github.com/paveldudka/JacocoEverywhere/issues/16

[update 2016-08-01] i found a working solution based on https://github.com/palantir/gradle-jacoco-coverage

See my own answer below

Upvotes: 15

Views: 27034

Answers (6)

Mr-IDE
Mr-IDE

Reputation: 7661

To create a combined code coverage report in Jacoco, which works across all the unit tests in the sub-modules of a Gradle project, it is not an easy task. But it's possible to do it without using any third-party plugins (which are mentioned in the other answers), and instead just modify your Gradle scripts. In fact, I was not able to get any plugins to work on an Android multi-module Gradle project.

There are 2 parts of this process that are important to know:

  1. The project must be configured to run all the unit tests as 1 combined group. And you must use a single gradle command to run this combined group of tests. (This configuration can be made to be temporary or on-demand. See Part 4)

  2. The Jacoco test runner must create 1 *.exec file for the combined group of tests. This file will then be used in a custom jacocoTestReport() task later on.

Part 1 - Create the combined group of unit tests, using sourceSets

Assume your project is setup and structured like this, with separate groups of unit tests:

build.gradle
├─ app/
    ├─ build.gradle
    ├─ src/main/java/package/App.java
    ├─ src/test/java/package/UnitTest1.java
├─ core-lib/
    ├─ build.gradle
    ├─ src/main/java/package/Core.java
    ├─ src/test/java/package/UnitTest2.java
├─ ext-lib/
    ├─ build.gradle
    ├─ src/main/kotlin/package/Ext.kt
    ├─ src/test/kotlin/package/IntegrationTest.kt

Add this to your app/build.gradle

android {
 /**
  * We want to create a combined (aggregated) code coverage report across all the
  * sub-modules of unit tests. So combine the tests into 1 group first. Guides:
  * https://stackoverflow.com/questions/60187527/run-all-ui-unit-tests-from-a-multi-module-project-using-a-single-command
  * https://stackoverflow.com/questions/39623730/configuring-gradle-for-multiple-android-instrumentation-test-folders
  * https://stackoverflow.com/questions/52717879/add-multiple-source-test-directories-for-tests
  */
  sourceSets {
    test {
      // java.srcDirs += "$rootDir/app/src/test/java/" // <-- Not needed. Already added by default.
      java.srcDirs += "$rootDir/core-lib/src/test/java/"
      java.srcDirs += "$rootDir/ext-lib/src/test/kotlin/"
    }
  }
  ...
}

Part 2 - Write a custom jacocoTestReport() Gradle task to create the combined report

Add this to your app/build.gradle

/**
 * Automatically create a code coverage report, whenever
 * "./gradlew app:testDebugUnitTest" is run. Sources:
 * https://proandroiddev.com/unified-code-coverage-for-android-revisited-44789c9b722f
 * https://stackoverflow.com/questions/18683022/how-to-get-code-coverage-using-android-studio#61124821
 * https://android.jlelse.eu/unified-code-coverage-reports-for-both-unit-and-ui-tests-e7c954a4e8ac
 *
 * To create a combined (aggregated) code coverage report across multiple Gradle modules, see the
 * sourceSets section above, and add the additional directories for "sourceDirectories" and "classDirectories".
 * https://github.com/tramalho/unified-code-coverage-android/blob/mixed-languages-multi-module/jacoco.gradle
 * https://www.veskoiliev.com/how-to-setup-jacoco-for-android-project-with-java-kotlin-and-multiple-flavours/
 */
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
  reports {
    xml.required = true
    html.required = true
  }

  def fileFilter = [
    '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'
  ]

  def appSources = "$project.projectDir/src/main/java"
  def coreLibSources = "$rootDir/core-lib/src/main/java/"
  def extLibSources = "$rootDir/ext-lib/src/main/kotlin/"
  // If your project has auto-generated source files, add their path as well. Example: Moshi for Kotlin.
  def extLibAutoGeneratedSources = "$rootDir/ext-lib/build/generated/ksp/main/kotlin/"

  def appJavaClasses = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter)
  def appKotlinClasses = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
  def coreLibJavaClasses = fileTree(dir: "$rootDir/core-lib/build/classes/java/main", excludes: fileFilter)
  def extLibKotlinClasses = fileTree(dir: "$rootDir/ext-lib/build/classes/kotlin/main", excludes: fileFilter)

  sourceDirectories.setFrom(files([appSources, coreLibSources, extLibSources, extLibAutoGeneratedSources]))
  classDirectories.setFrom(files([appJavaClasses, appKotlinClasses, coreLibJavaClasses, extLibKotlinClasses]))

  executionData.setFrom(fileTree(dir: "$rootDir", includes: [
    // Important: To create the individual or the combined/aggregated unit test report,
    // only specify one exec file. Otherwise you will get errors.
    "app/build/jacoco/testDebugUnitTest.exec",
  ]))
}

tasks.withType(Test) {
  finalizedBy jacocoTestReport
  jacoco.includeNoLocationClasses = true // Needed if you have Robolectric unit tests
}

Part 3 - Run the Gradle commands to create the combined coverage report

Compile and build the unit tests for each module first. This is needed because Gradle doesn't know which ones are built or not, as they are independent modules.

./gradlew assembleDebugUnitTest     # Build the Android app unit tests
./gradlew core-lib:compileTestJava  # Build the unit tests for Java library
./gradlew ext-lib:compileTestKotlin # Build the unit tests for Kotlin library

Then run the unit tests for the app module. This will now create a combined (aggregated) report across all 3 modules.

./gradlew app:testDebugUnitTest

View the code coverage report

  • Check that the exec file is large, and it's generated at the expected path: app/build/jacoco/testDebugUnitTest.exec
  • Open the index.html file in a web browser: app/build/reports/jacoco/jacocoTestReport/html/index.html

Part 4 - Use an on-demand Gradle configuration for better flexibility

It can be inconvenient to have all the unit tests grouped into 1 sourceSets block in Gradle. For example, Android Studio will display all the unit test files as one coupled group, instead of as separate independent groups of tests in each Gradle module. Another problem: Sometimes you may want to run an individual module of tests, rather than the whole combined group.

This is why an on-demand configuration is useful. The idea is to keep the unit tests as separate independent groups, by default. But when you want to create a combined code coverage report, Gradle will use a simple flag to combine the groups into one, while it runs the tests. After the tests have finished, the configuration returns back to the independent groups.

Use a simple boolean flag to achieve this. Modify the first version of app/build.gradle like this:

/*
 * A boolean flag to enable or disable the combined code coverage.
 * This should be sent as a flag to the gradlew command.
 */
def useCombinedCodeCoverage = (
  hasProperty('useCombinedCodeCoverage') &&
  property('useCombinedCodeCoverage') == "true"
)

/*
 * This is mainly to control the code coverage for UI tests on an Android device.
 * But we can switch it on for unit tests as well, for reassurance. Note that
 * it causes the Jacoco *.exec file path to be different.
 */
def codeCoverageUiTests = (
  hasProperty('codeCoverageUiTests') &&
  property('codeCoverageUiTests') == "true"
)

android {
  sourceSets {
    if (useCombinedCodeCoverage) {
      test {
        // java.srcDirs += "$rootDir/app/src/test/java/" // Already added by default.
        java.srcDirs += "$rootDir/core-lib/src/test/java/"
        java.srcDirs += "$rootDir/ext-lib/src/test/kotlin/"
      }
    }
  }

  buildTypes {
    debug {
      testCoverageEnabled = codeCoverageUiTests
    }
  }
  ...
}
/**
 * Automatically create a code coverage report when "./gradlew app:testDebugUnitTest" is run.
 */
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
  reports {
    xml.required = true
    html.required = true
  }

  def fileFilter = [
    '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'
  ]

  if (useCombinedCodeCoverage) {
    def appSources = "$project.projectDir/src/main/java"
    def coreLibSources = "$rootDir/core-lib/src/main/java/"
    def extLibSources = "$rootDir/ext-lib/src/main/kotlin/"
    // If your project has auto-generated source files, add their path as well. Example: Moshi for Kotlin.
    def extLibAutoGeneratedSources = "$rootDir/ext-lib/build/generated/ksp/main/kotlin/"

    def appJavaClasses = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter)
    def appKotlinClasses = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
    def coreLibJavaClasses = fileTree(dir: "$rootDir/core-lib/build/classes/java/main", excludes: fileFilter)
    def extLibKotlinClasses = fileTree(dir: "$rootDir/ext-lib/build/classes/kotlin/main", excludes: fileFilter)

    sourceDirectories.setFrom(files([appSources, coreLibSources, extLibSources, extLibAutoGeneratedSources]))
    classDirectories.setFrom(files([appJavaClasses, appKotlinClasses, coreLibJavaClasses, extLibKotlinClasses]))

  } else {
    def appSources = "$project.projectDir/src/main/java"
    def appJavaClasses = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter)
    def appKotlinClasses = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)

    sourceDirectories.setFrom(files([appSources]))
    classDirectories.setFrom(files([appJavaClasses, appKotlinClasses]))
  }

  executionData.setFrom(fileTree(dir: "$rootDir", includes: [
    // Important: To create the individual or the combined/aggregated unit test report,
    // only specify one exec file. Otherwise you will get errors.
    // Notice that the path has changed, compared to the first version.
    "app/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec",
  ]))
}

tasks.withType(Test) {
  finalizedBy jacocoTestReport
  jacoco.includeNoLocationClasses = true // Needed if you have Robolectric unit tests
}

To create a combined code coverage report

./gradlew assembleDebugUnitTest
./gradlew core-lib:compileTestJava
./gradlew ext-lib:compileTestKotlin
./gradlew app:testDebugUnitTest -PcodeCoverageUiTests=true -PuseCombinedCodeCoverage=true

To create a code coverage report for the app module only

./gradlew assembleDebugUnitTest
./gradlew app:testDebugUnitTest -PcodeCoverageUiTests=true

View the code coverage report

The same path is used for either the combined report or the individual report.

  • Check that the exec file is generated at the expected path: app/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec
  • Open the index.html file in a web browser: app/build/reports/jacoco/jacocoTestReport/html/index.html

Sources, references, and further tutorials

Part 1

Part 2


Similar or duplicate questions on StackOverflow

Upvotes: 0

k3b
k3b

Reputation: 14755

Finally I found this plugin: https://github.com/palantir/gradle-jacoco-coverage that did the job for me:

root gradle.build

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        // see https://jcenter.bintray.com/com/android/tools/build/gradle/
        classpath 'com.android.tools.build:gradle:2.1.0'
        // classpath 'com.android.tools.build:gradle:2.2.0-alpha1'

        // https://github.com/palantir/gradle-jacoco-coverage
        classpath 'com.palantir:jacoco-coverage:0.4.0'      
    }
}

// https://github.com/palantir/gradle-jacoco-coverage
apply plugin: 'com.palantir.jacoco-full-report'

all subprojects that has

apply plugin: 'jacoco'

are included in the report.

[Update 2023-07-20]

Now there is a new gradle plugin that does the job. Therefore i changed the accepted answer

Upvotes: 9

Wei Yuan
Wei Yuan

Reputation: 321

This works for me

plugins {
    id 'org.kordamp.gradle.jacoco' version '0.43.0'
}

config {
    coverage {
        jacoco {
            enabled
            aggregateExecFile
            aggregateReportHtmlFile
            aggregateReportXmlFile
            additionalSourceDirs
            additionalClassDirs
        }
    }
}

https://kordamp.org/kordamp-gradle-plugins/#_org_kordamp_gradle_jacoco

Upvotes: 7

Le Duc Duy
Le Duc Duy

Reputation: 1881

You can create a merged report without merged exec file. Create a new task to the root of build.gradle with following content.

task jacocoReport(type: JacocoReport) {
    for (p in allprojects) {
        def testTask = p.tasks.findByName("test")
        if (testTask != null)
            dependsOn(testTask)

        executionData.setFrom(file("${p.buildDir}/jacoco/test.exec"))
        classDirectories.from(file("${p.buildDir}/classes/java/main"))
    }
}

Upvotes: 0

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.

Upvotes: 1

Benjamin Muschko
Benjamin Muschko

Reputation: 33436

You will need to create a new task of type JacocoMerge that aggregates the JaCoCo reports from all subprojects. See a similar discussion in this post.

Upvotes: 0

Related Questions