Sonarqube is unable to pickup code coverage report generated by Jacoco in my gitlab CI pipeline

my build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.excludes = [
            '**/databinding/*Binding.*',
            '**/R.class',
            '**/R$*.class',
            '**/BuildConfig.*',
            '**/Manifest*.*',
            '**/*Test*.*',
            'android/**/*.*',
            '**/*$ViewInjector*.*',
            '**/*$ViewBinder*.*',
            '**/Lambda$*.class',
            '**/Lambda.class',
            '**/*Lambda.class',
            '**/*Lambda*.class',
            '**/*_MembersInjector.class',
            '**/Dagger*Component*.*',
            '**/*Module_*Factory.class',
            '**/di/module/*',
            '**/*_Factory*.*',
            '**/*Module*.*',
            '**/*Dagger*.*',
            '**/*Hilt*.*',
            // kotlin
            '**/*MapperImpl*.*',
            '**/*$ViewInjector*.*',
            '**/*$ViewBinder*.*',
            '**/BuildConfig.*',
            '**/*Component*.*',
            '**/*BR*.*',
            '**/Manifest*.*',
            '**/*$Lambda$*.*',
            '**/*Companion*.*',
            '**/*Module*.*',
            '**/*Dagger*.*',
            '**/*Hilt*.*',
            '**/*MembersInjector*.*',
            '**/*_MembersInjector.class',
            '**/*_Factory*.*',
            '**/*_Provide*Factory*.*',
            '**/*Extensions*.*'
    ]

    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.2.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20"
        classpath 'com.google.gms:google-services:4.4.0'
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
        classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929"
        classpath "org.jacoco:org.jacoco.core:0.8.8"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

plugins {
    id 'com.google.dagger.hilt.android' version '2.44' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
    id "org.sonarqube" version "4.0.0.2929"
    id 'jacoco'
}

sonar {
    properties {
        property "sonar.projectKey", "***************"
        property "sonar.projectName", "*************"
        property "sonar.qualitygate.wait", true
        property "sonar.sources", "src/main/java"
        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.tests", ["src/test/java"]
        property "sonar.test.inclusions", "**/*Test*/**"
        property "sonar.java.coveragePlugin", 'jacoco'
        property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/jacoco.xml"
        property "sonar.exclusions", excludes
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

my jacoco.gradle

apply plugin: 'jacoco'
jacoco {
    toolVersion = "0.8.8"
}
project.afterEvaluate {
    if (android.hasProperty("applicationVariants")) {
        android.applicationVariants.all { variant ->
            createVariantCoverage(variant)
        }
    } else if (android.hasProperty("libraryVariants")) {
        android.libraryVariants.all { variant ->
            createVariantCoverage(variant)
        }
    }
}

def createVariantCoverage(variant) {
    def variantName = variant.name
    def testTaskName = "test${variantName.capitalize()}UnitTest"

    // Add unit test coverage tasks
    tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: "$testTaskName") {
        group = "Reporting"
        description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build."

        reports {
            xml.required = true
            csv.required = false
            xml.destination file("${buildDir}/reports/jacoco/jacoco.xml")
        }

        def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: excludes)
        def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes)
        getClassDirectories().setFrom(files([javaClasses, kotlinClasses]))

        getSourceDirectories().setFrom(files([
                "$project.projectDir/src/main/java",
                "$project.projectDir/src/${variantName}/java",
                "$project.projectDir/src/main/kotlin",
                "$project.projectDir/src/${variantName}/kotlin"
        ]))

        getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))

        doLast {
            def m = new File("${project.buildDir}/reports/jacoco/${testTaskName}Coverage/html/index.html")
                    .text =~ /Total[^%]*>(\d?\d?\d?%)/
            if (m) {
                println "Test coverage: ${m[0][1]}"
            }
        }
    }

    // Add unit test coverage verification tasks
    tasks.create(name: "${testTaskName}CoverageVerification", type: JacocoCoverageVerification, dependsOn: "${testTaskName}Coverage") {
        group = "Reporting"
        description = "Verifies Jacoco coverage for the ${variantName.capitalize()} build."
        violationRules {
            rule {
                limit {
                    minimum = 0
                }
            }
            rule {
                element = 'BUNDLE'
                limit {
                    counter = 'LINE'
                    value = 'COVEREDRATIO'
                    minimum = 0.00
                }
            }
        }
        def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: excludes)
        def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes)
        getClassDirectories().setFrom(files([javaClasses, kotlinClasses]))
        getSourceDirectories().setFrom(files([
                "$project.projectDir/src/main/java",
                "$project.projectDir/src/${variantName}/java",
                "$project.projectDir/src/main/kotlin",
                "$project.projectDir/src/${variantName}/kotlin"
        ]))
        getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))
    }
}

my gitlab.-ci.yml

# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Android.gitlab-ci.yml

# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template.

image: eclipse-temurin:17-jdk-jammy

variables:

  # ANDROID_COMPILE_SDK is the version of Android you're compiling with.
  # It should match compileSdkVersion.
  ANDROID_COMPILE_SDK: "33"

  # ANDROID_BUILD_TOOLS is the version of the Android build tools you are using.
  # It should match buildToolsVersion.
  ANDROID_BUILD_TOOLS: "33.0.2"

  # It's what version of the command line tools we're going to download from the official site.
  # Official Site-> https://developer.android.com/studio/index.html
  # There, look down below at the cli tools only, sdk tools package is of format:
  #        commandlinetools-os_type-ANDROID_SDK_TOOLS_latest.zip
  # when the script was last modified for latest compileSdkVersion, it was which is written down below
  ANDROID_SDK_TOOLS: "9477386"

# Packages installation before running script
before_script:
  - apt-get --quiet update --yes
  - apt-get --quiet install --yes wget unzip

  # Setup path as android_home for moving/exporting the downloaded sdk into it
  - export ANDROID_HOME="${PWD}/android-sdk-root"
  # Create a new directory at specified location
  - install -d $ANDROID_HOME
  # Here we are installing androidSDK tools from official source,
  # (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well)
  # after that unzipping those tools and
  # then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build
  - wget --no-verbose --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
  - unzip -q -d "$ANDROID_HOME/cmdline-tools" "$ANDROID_HOME/cmdline-tools.zip"
  - mv -T "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
  - export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin

  # Nothing fancy here, just checking sdkManager version
  - sdkmanager --version

  # use yes to accept all licenses
  - yes | sdkmanager --licenses > /dev/null || true
  - sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}"
  - sdkmanager "platform-tools"
  - sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}"

  # Not necessary, but just for surity
  - chmod +x ./gradlew

stages:      # List of stages for jobs, and their order of execution
  - build
  - test
  - deploy
  - code-quality

# Basic android and gradle stuff
# Check linting
#lintDebug:
#  interruptible: true
#  stage: build
#  allow_failure: true
#  script:
#    - ./gradlew -Pci --console=plain :app:lintDevDebug -PbuildDir=lint
#  artifacts:
#    paths:
#      - app/lint/reports/lint-results-debug.html
#    expose_as: "lint-report"
#    when: always
#  only:
#    - master

# Make Project
assembleQA:
  interruptible: true
  stage: build
  script:
    - ./gradlew assembleQaRelease
  artifacts:
    paths:
      - app/build/outputs/
#  only:
#    - master

assembleStaging:
  stage: deploy
  environment:
    name: staging
  needs:
    - assembleQA
  when: manual
  script:
    - ./gradlew assembleStagingRelease
  artifacts:
    paths:
      - app/build/outputs/
  only:
    - master

assembleSandbox:
  stage: deploy
  environment:
    name: sandbox
  needs:
    - assembleQA
  when: manual
  script:
    - ./gradlew assembleSandboxRelease
  artifacts:
    paths:
      - app/build/outputs/
  only:
    - master

assembleProduction:
  stage: deploy
  environment:
    name: production
  needs:
    - assembleQA
  when: manual
  script:
    - ./gradlew assembleProductionRelease
  artifacts:
    paths:
      - app/build/outputs/
  only:
    - master
  


# Run all tests, if any fails, interrupt the pipeline(fail it)
debugTests:
  needs: [assembleQA]
  interruptible: true
  stage: test
  script:
    - ./gradlew testDevDebugUnitTestCoverageVerification
#  only:
#    - master


sonar-qube:
  stage: code-quality
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"  # Defines the location of the analysis task cache
    GIT_DEPTH: "0"  # Tells git to fetch all the branches of the project,required by the analysis task
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  allow_failure: true
  script: ./gradlew sonar
#  only:
#    - master

When i run command .\gradlew testDevDebugUnitTestCoverageVerification in my AndroidStudio terminal I can see the xml report file is generated under app/build/reports/jacoco path but in my CI when the job code-quality runs sonarqube reports error saying

No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths=‘/builds//builds/radiusxrllc/radius-xr-android/build/reports/jacoco/jacoco.xml’. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml

I need help

Hi,

This looks to me like a question of variable interpolation. At a guess, the variable’s value changes somewhere in the process. I suggest some good old-fashioned print debugging.

 
HTH,
Ann

Thanks for pointing me out in the right direction. I did a bit changes
in my jacoco.gradle

reports {
            xml.required = true
            csv.required = false
            xml.destination file("${rootProject.buildDir}/reports/jacoco/jacoco.xml")
        }

        println "destination: ${rootProject.buildDir}/reports/jacoco/jacoco.xml"

and in build.gradle (sonar.properties)

   property "sonar.coverage.jacoco.xmlReportPaths", "${rootProject.buildDir}/reports/jacoco/jacoco.xml"

in my jacoco.gradle code "destination: ${rootProject.buildDir}/reports/jacoco/jacoco.xml"
prints
/builds/radiusxrllc/radius-xr-android/build/reports/jacoco/jacoco.xml

but the sonarqube stills fails to find the report
No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths='/builds/radiusxrllc/radius-xr-android/build/reports/jacoco/jacoco.xml'. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml

Please note the both paths are now same

Now i’m clueless :frowning:

Hi,

Is there a builds directory in the root of your server?

 
Ann

I’m using gitlab for my CI, which in turns uses a default runner. I think it does have a build directory otherwise the command xml.destination file("${rootProject.buildDir}/reports/jacoco/jacoco.xml") must have created one

Hi,

Can you share your new analysis log?

Can you also verify that ${rootProject.buildDir}/reports/jacoco/jacoco.xml exists?

 
Thx,
Ann

Can you also verify that ${rootProject.buildDir}/reports/jacoco/jacoco.xml exists?
how to do that in my gitlab CI?

Also I have tried to export the report file by following

debugTests:
  needs: [assembleQA]
  interruptible: true
  stage: test
  script:
    - ./gradlew testDevDebugUnitTestCoverageVerification
  artifacts:
    paths:
      - app/build/reports/jacoco/

This saved the file reports file in gitlab storage

then I did

        property "sonar.coverage.jacoco.xmlReportPaths", "app/build/reports/jacoco/testDevDebugUnitTestCoverage/testDevDebugUnitTestCoverage.xml"

But sonarqube still unable to find the file

No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths='app/build/reports/jacoco/testDevDebugUnitTestCoverage/testDevDebugUnitTestCoverage.xml'. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml

Hi,

Is this an optional property? Is there any way to let this output to the default path, which is where analysis ends up looking?

 
Ann

This is optional and part of CI server, gitlab exports the artifacts (reports xml file in my case) to this storage path hosted at gitlab.

Hi,

Can you try letting the coverage report output to the default path?

 
Ann

What do you exactly mean by that? Should I remove the artifacts parameter in my CI or you want me to give the path that matches the default storage path for xml reports

Hi,

I’m suggesting you generate the report to that^ location.

 
Ann

As per your suggestion I tried

debugTests:
  needs:
    - assembleQA
  interruptible: true
  stage: test
  script:
    - ./gradlew testDevDebugUnitTestCoverageVerification
  artifacts:
    paths:
      - target/site/jacoco/*.xml
#  only:
#    - master

sonar-qube:
  stage: code-quality
  needs:
    - job: debugTests
      artifacts: true
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"  # Defines the location of the analysis task cache
    GIT_DEPTH: "0"  # Tells git to fetch all the branches of the project,required by the analysis task
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  allow_failure: true
  script: ./gradlew sonar -Dsonar.coverage.jacoco.xmlReportPaths="target/site/jacoco/*.xml"
#  only:
#    - master

but my test job fails with error

WARNING: target/site/jacoco/*.xml: no matching files. Ensure that the artifact path is relative to the working directory (/builds/radiusxrllc/radius-xr-android)

Hi,

Could you maybe turn off archiving?

 
Ann

My initial solution was without archiving and it didn’t work.

I think this is some kind of bug in sonarqube that it fails to pickup coverage report.