How To Conditionally Enable/Disable Jacoco In Gradle Build

Background

My team recently added Jacoco to our Android Gradle build, in order to generate test coverage reports. We customized the jacocoTestReport Gradle task provided by the Jacoco Gradle plugin, which is used to generate reports. Our continuous integration is configured to run this task for every build, which allows us to see how our pull-requests affect test coverage. While test coverage is not the end-all-be-all of quality, it's good to see how code changes affect coverage.

Unfortunately, we soon noticed that Jacoco was adding time to every build -- not just the ones where we want to generate coverage reports. Jacoco needs to instrument code in order to determine test coverage, and it does this regardless of whether test coverage is actually going to be measured. We couldn't find any built-in Jacoco configuration that would allow us to eliminate this useless instrumentation overhead based on whether the jacocoTestReport task had been invoked; so long as you set the built-in testCoverageEnabled flag to true, Jacoco instruments your code. Skipping Jacoco instrumentation and the loading of the Jacoco Gradle plugin was going to take a little extra work.

Research led me to this Stack Overflow answer, in which it was demonstrated that you can conditionally apply the Jacoco plugin. However, it didn't explain the mechanism by which one sets the jacocoEnabled flag to true or false, and since my Gradle fu was rusty I'm guessing that's true for many other developers, so I wanted to post a complete solution. I also added the crucial step of conditionally disabling the testCoverageEnabled flag. When I first wrote this post, I didn't understand that Gradle had built-in support for offline Jacoco instrumentation, and that the Jacoco plugin is only responsible for using the instrumentation. With this change in place, the jacocoDebug task (or jacoco[flavor]Debug task) should be completely absent from your Gradle debug build, unless you explicitly set the jacocoEnabled system property to "true".

How To Conditionally Enable/Disable Jacoco Via A System Property

Step 1: Apply jacoco plugin in a separate jacoco.gradle file

We want to incorporate the jacoco plugin, tasks, and related Gradle goo only if our jacocoEnabled flag is true. So all of your Jacoco Gradle config should go in its own file, in your project's root directory. Ours ended up looking something like this:

apply plugin: 'jacoco'

jacoco {
toolVersion = '0.8.2'
}

task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {

group "Reporting"
description "Generate Jacoco coverage reports."

reports {
xml.enabled = true
html.enabled = true
html.destination file("${rootProject.buildDir}/coverage-report")
}

def fileFilter = ['**/R.class',
'**/R2.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*Test*.*',
'**/AutoValue_*.*',
'**/*JavascriptBridge.class',
'**/*JsonObjectMapper*.*',
'**/*MembersInjector*.*',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*View.class',
'**/*Activity*.*',
'**/*Fragment*.*',
'**/views/**',
'**/viewholder/**',
'**/viewholders/**',
'**/wrapper/**',
'**/orientation/**',
'**/ktx/**',
'**/decorator/**',
'**/*$ViewInjector*.class',
'**/*$ViewHolder*.class',
'**/*$Viewholder*.class',
'**/*ViewBinding*.*',
'**/Dao*.class',
'**/*Component*.*',
'**/*Module*.*',
'**/*Injector*.*',
'android/**/*.*',
'**/io/**',
'**/apache/**',
'**/sharing/**']

def javaClasses = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter)
def kotlinClasses = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
def javaSrc = "$project.projectDir/src/main/java"
def kotlinSrc = "$project.projectDir/src/main/java"
def execution = fileTree(dir: project.buildDir,
includes: ['jacoco/testDebugUnitTest.exec'])

sourceDirectories = files([javaSrc, kotlinSrc])
classDirectories = files([javaClasses, kotlinClasses])

print execution

executionData = files(execution)

doLast() {
print "file://${reports.html.destination}/index.html"
}
}

Step 2: Set the jacocoEnabled variable in your root build.gradle file

In the ext section of your root build.gradle file, assign the system-property value to the jacocoEnabled variable, with a default of false in case the system property is not defined:

jacocoEnabled = System.properties.getProperty("jacocoEnabled") ?: "false"

Step 3: Apply your jacoco.gradle file conditionally

This is the first half of the puzzle. Unless the jacocoEnabled property has been set to true, skip applying the jacoco.gradle file. This way, the plugin isn't loaded. That doesn't buy you a whole lot, but why load bits you don't need?

if (jacocoEnabled.toBoolean()) {
project.logger.lifecycle('Applying jacoco build file: jacoco.gradle')
apply from: rootProject.file("jacoco.gradle")
}

Step 4: Enable/disable Jacoco instrumentation conditionally

This is the second half of the puzzle, and by far the more valuable piece, in terms of reducing your build time. Turning this off prevents offline Jacoco instrumentation from being performed. (Jacoco's Java-agent approach to run-time instrumentation is not compatible with Android, so offline instrumentation is the only mode of Jacoco instrumentation supported on Android.) This eliminates the jacocoDebug task from your Gradle build.

debug {
...
testCoverageEnabled jacocoEnabled.toBoolean()
}

Step 5: Set the jacocoEnabled flag via system property

Since we defaulted the jacocoEnabled variable to false in Step 2, you'll have to explicitly set it to true via the jacocoEnabled system property when you want to run Jacoco and generate a test-coverage report.

Note: If you use the Gradle daemon, it will retain the jacoco plugin between runs, even if you don't set jacocoEnabled=true. If you run Jacoco on your dev box to test this out, be sure to kill your Gradle daemon when you're done, via the gradlew --stop command. Otherwise, based on my observations, the plugin will remain in place until the Gradle daemon dies. (However, this is not necessary if you just want to make sure that your class files are not instrumented as part of the build.)

Here's how to set a system property via the command-line when running Gradle:

./gradlew -PjacocoEnabled=true jacocoTestReport


If you found this helpful, you'd like to thank me, and you enjoy reading -- or know someone who does -- buy one of my books!


comments powered by Disqus