Reputation: 14755
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
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:
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)
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.
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/"
}
}
...
}
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
}
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
app/build/jacoco/testDebugUnitTest.exec
index.html
file in a web browser: app/build/reports/jacoco/jacocoTestReport/html/index.html
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.
app/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec
index.html
file in a web browser: app/build/reports/jacoco/jacocoTestReport/html/index.html
Part 1
Part 2
Upvotes: 0
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
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
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
Reputation: 21
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
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