10

I am building a SonarQube 6.2 server which is already analyzing my Java 8/Gradle 3.3 projects. When adding JaCoCo to a multimodule gradle project, I realized that SonarQube is measuring code coverage on a "per-module" basis:

If a class is located in module A and a test for this class is located in module B, SonarQube figures the class is not covered.

I want to measure code coverage across all modules, not on a per module basis. How do I achieve this?

There are lots of similar questions but no helpful answers, although the situation seems quite common to me. Jenkins for example does that per default.

I decided to build a blueprint on github to clarify the issue.

The main build.gradle consists of

plugins { id "org.sonarqube" version "2.2.1" }

subprojects {
    apply plugin: 'java'
    apply plugin: 'jacoco'

    repositories { mavenCentral() }
    dependencies { testCompile "junit:junit:4.12" }
}

modA/build.gradleis empty.

It contains 3 classes: TestedInModA, TestedInModATest and TestedViaModB.

modB/build.gradlejust declares a dependency to modA:

dependencies { compile project(':modA') }

It contains just one class: TestedViaModBTest, testing the class TestedViaModB located in modA.

My (private) Jenkins instance shows 100% coverage for the two classes included while SonarQube says only the class TestedInModA (which is tested in its own module) is covered.

How can I modify my build process to see "cross-module coverage" in SonarQube?

I would love to update my project so future visitors to this question can find a working example.

My working solution (thanks @Godin)

  1. add the following to the subprojects closure

    tasks.withType(Test) {
        // redirect all coverage data to one file
        // ... needs cleaning the data prior to the build to avoid accumulating coverage data of different runs.
        // see `task cleanJacoco`
        jacoco {
            destinationFile = file("$rootProject.buildDir/jacoco/test.exec")
        }
    }
    
  2. add

    task cleanJacoco(dependsOn: 'clean') {  delete "$buildDir/jacoco" }
    

outside the subprojects closure.

sk_dev
  • 295
  • 2
  • 10
  • unfortunately the blueprint link is dead. I am still searching for a working solution. – Bobbelinio Aug 19 '20 at 10:18
  • sorry @Bobbelinio, it seems I've removed the blueprint from my github account and I did not find another copy. However, the aforementioned solution does not work any more (according to a comment I've found it stopped working when updating to Gradle 5.2.1). Since we've stopped using sonarqube, we did not bother fixing the problem. – sk_dev Aug 31 '20 at 16:49

2 Answers2

7

When you perform build JaCoCo Gradle Plugin will produce modA/build/jacoco/test.exec and modB/build/jacoco/test.exe that contain information about execution of tests in modA and modB respectively. SonarQube performs analysis of modules separately, so during analysis of modA for the file TestedViaModB it sees only modA/build/jacoco/test.exec.

Most common trick to cross boundaries - is to collect all coverage information into single location. This can be done with JaCoCo Gralde Plugin

  • either by changing location - see destinationFile and destPath ( since information is appended to exec file, don't forget to remove this single location prior to build, otherwise it will be accumulating information from different builds and not just from different modules ),

  • either by merging all files into single one - see JacocoMerge task. And then specify this single location to SonarQube as sonar.jacoco.reportPath.

Another trick: SonarQube 6.2 with Java Plugin 4.4 supports property sonar.jacoco.reportPaths allowing to specify multiple locations.

Godin
  • 9,801
  • 2
  • 39
  • 76
  • Thanks @Godin; the `destinationFile` seems to work, but how do I consistently delete the old exec data prior to a new run? Using `jacocoMerge` task seems to have the same result (... besides the original `*.exec` files are kept), but the file is automatically regenerated by the next build. However I was unable to integrate this at the right place (see the commented code in the main `build.gradle` within the github project). – sk_dev Jan 19 '17 at 19:16
  • The `sonar.jacoco.reportPaths` looks promising but seems to be ignored (coverage still 50%, logfile says `Property 'sonar.jacoco.reportPath' is deprecated. Please use 'sonar.jacoco.reportPaths' instead.`) I updated the main `build.gradle` with my attempt. – sk_dev Jan 19 '17 at 19:17
  • @sk_dev about first question from first comment - why not put it into the build directory, so that it will be cleaned on `./gradlew clean build` – Godin Jan 20 '17 at 00:54
  • I used `jacoco { destinationFile = file("$rootProject.buildDir/jacoco/test.exec") }` within the `subprojects` closure but it is not cleaned during `./gradlew clean`. Only the subprojects are cleaned... do I need to `apply plugin _____` to the main project? (`apply plugin 'java'` apparently works, but I need to check for side effects... – sk_dev Jan 20 '17 at 07:43
0

If you are interested in the solution with sonar.jacoco.reportPaths (see answer of Godin), have looke at this gradle code:

tasks.getByName('sonarqube') {
    doFirst {
        // lazy initialize the property to collect all coverage files
        def jacocoFilesFromSubprojects = subprojects.findAll {it.plugins.hasPlugin('jacoco')}
                .collect {it.tasks.withType(Test)}.flatten().collect {it.jacoco.destinationFile}

        sonarqube.properties {
            property "sonar.jacoco.reportPaths", jacocoFilesFromSubprojects.join(',')
        }
    }
}

This will collect all coverage binary files and set them as comma-separated list to the sonar property. Considers all test tasks of sub projects where jacoco is applied and their jacoco destination file configured.

Steffen Harbich
  • 2,639
  • 2
  • 37
  • 71