Search code examples
gradlegroovyclosurescode-coveragejacoco

How do you exclude implicit Groovy closure classes from JaCoCo test resport with Gradle


If you use JaCoCo on a Groovy project, you get a lot of implicitly created closure classes in your coverage report. The source linked to those, are the enclosing classes. The coverage report of the enclosing classes also contains the closure coverage, so it does not add any value to the coverage report. Even worse, it falsifies the coverage report, as the statements to cover in closures are counted twice in the overall statistic.

So how do you exclude those closures from the generated JaCoCo coverage report?


Solution

  • You can exclude the implicit closure classes from JaCoCo recording with the following snippet, this does not prevent the statements to be recorded in the enclosing class

    test {
        jacoco.excludes = ['**/*$*_closure*']
    }
    

    With that snippet the coverage is not recorded anymore, but now all closures are shown with 0% coverage in the report and thus also reduce the overall coverage statistic.

    Adding the following snippet will remove the closure classes from the JaCoCo report and thus will also give a more meaningful overall statistic. The above snippet is not really necessary, it just brings a little performance improvement as the coverage does not need to be recorded and transformed for the report, so I suggest using both snippets.

    jacocoTestReport {
        doFirst {
            classDirectories = classDirectories.collect { fileTree(it) { exclude '**/*$*_closure*' } }.sum()
        }
    }
    

    If you have multiple Test and JacocoReport tasks that you want to modify this way, you can of course also do it generically like

    tasks.withType(Test) {
        jacoco.excludes = ['**/*$*_closure*']
    }
    
    tasks.withType(JacocoReport) {
        doFirst {
            classDirectories = classDirectories.collect { fileTree(it) { exclude '**/*$*_closure*' } }.sum()
        }
    }
    

    Update:

    Here a reworked version of the last example working with Gradle 8.0.2 for Kotlin DSL:

    tasks.withType<Test>().configureEach {
        the<JacocoTaskExtension>().excludes = listOf("**/*$*_closure*")
    }
    
    afterEvaluate {
        tasks.withType<JacocoReport>().configureEach {
            classDirectories.setFrom(
                classDirectories
                    .asFileTree
                    .matching { exclude("**/*$*_closure*") }
                    .toList()
            )
        }
    }
    

    The afterEvaluate is of course evil, but in this case necessary. Changing the configuration in doFirst and thus during execution phase was even more evil, but in this case more reliable, but does not work anymore as the property is read-only during the execution phase.