gradlegroovyspockjacoco

JaCoCo coverage with SpockFramework + groovy is incomplete


Given a case statement with a default a unit test for this

Copilot believes that extra byte code added by the groovy compiler can cause this. Not sure I believe that given the examples I've seen searching but I've yet to find a solution for my case.

How could I get the coverage tools to agree for groovy + SpockFramework tests?

Jacoco coverage

enter image description here

IDEA coverage

enter image description here

build.gradle

plugins {
    id 'com.jfrog.artifactory' version '5.0.3'
    id 'com.github.spotbugs' version '5.1.1'
    id 'org.sonarqube' version '4.4.1.3373'
    id 'org.ajoberstar.grgit' version '4.0.1'
    id 'jacoco'
    id 'groovy'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

sourceSets {
    gdsl {
        groovy {
            srcDirs 'gdsl'
            compileClasspath = compileGroovy.classpath
        }
    }
    main {
        groovy {
            srcDirs = ['src', 'vars']
        }
    }
    test {
        groovy {
            srcDirs 'test/groovy'
        }
        resources {
            srcDirs = [ 'test/resources']
        }
    }
}

repositories {
...
}

tasks.withType(Test) {
    testLogging {
        showStandardStreams = true
        events "passed", "failed"
        displayGranularity = 1
    }
}

dependencies {
    implementation 'org.codehaus.groovy:groovy-all:2.4.21'
    implementation 'com.cloudbees:groovy-cps:1.32'
    implementation "org.jenkins-ci.main:jenkins-core:${jenkinsVersion}"
    implementation 'org.yaml:snakeyaml:2.2'

    // Spock Unit Test Support
    testImplementation platform("org.spockframework:spock-bom:1.3-groovy-2.4")
    testImplementation 'org.spockframework:spock-core'
    

    // pipeline unit support
    testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.19'

    // junit support - mostly for junit @Rules
    testImplementation 'junit:junit:4.13.1'
}

tasks.withType(GroovyCompile) {
    options.debug = true
    options.compilerArgs = ['-g:lines,vars,source']
}

spotbugs {
    ignoreFailures = true
    showStackTraces = true
    showProgress = true
    effort = 'max'
    reportLevel = 'default'
    excludeFilter.set(file("${projectDir}/configuration/spotbugsExcludeFilter.xml"))
}

tasks.withType(com.github.spotbugs.snom.SpotBugsTask) {
    reports {
        xml {
            enabled = true
        }
        html {
            enabled = true
        }
    }
}

def gitBranch() {
    def branchName = "";
    def proc = "git rev-parse --abbrev-ref HEAD".execute();
    proc.in.eachLine { line -> branchName = line }
    proc.err.eachLine { line -> println line }
    proc.waitFor();
    return branchName;
}

jacoco {
    toolVersion = "0.8.11"
}

jacocoTestReport {
    reports {
        xml.required = true
        csv.required = false
    }

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it,
                    include: ['vars/**', '**/mycorp/**']) // Exclude other directories if needed
        }))
    }

    dependsOn test // tests are required to run before generating the report
}

test {
    // use available processor resources
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1

    // set location for unit tests to have their "workspaces"
    systemProperty "testing.root.dir", "${project.buildDir.absolutePath}/working-test-dirs"
    systemProperty "project.root.dir", "${project.rootDir.absolutePath}"
    useJUnit()
    finalizedBy jacocoTestReport // report is always generated after tests run
}

task consumerGroovyDoc(type: Groovydoc) {
    source project.files('vars')
    docTitle = "Library Consumer Documentation"
    destinationDir = file("${project.buildDir}/docs/groovydoc/consumers")
    classpath = project.groovydoc.classpath
}

task internalsGroovyDoc(type: Groovydoc) {
    source project.files('src')
    docTitle = "Library Internals Documentation"
    destinationDir = file("${project.buildDir}/docs/groovydoc/internals")
    classpath = project.groovydoc.classpath
}

task generateGroovyDoc(dependsOn: [consumerGroovyDoc, internalsGroovyDoc]) {
}
tasks.build.finalizedBy tasks.generateGroovyDoc

Solution

  • From the JaCoCo FAQ

    Source code lines with exceptions show no coverage. Why?

    JaCoCo determines code execution with so called probes. Probes are inserted into the control flow at certain positions. Code is considered as executed when a subsequent probe has been executed. In case of exceptions such a sequence of instructions is aborted somewhere in the middle and the corresponding lines of source code are not marked as covered.