Search code examples
androidkotlinsonarqubejacoco

SonarQube Code Coverage Could not account for Kotlin files on an Android Project


Below is my sonarqube properties snippet:

sonarqube {
   properties{
      property "sonar.junit.reportPaths", "build/test-results/testDebugUnitTest/*.xml"
      property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacocoTestReport.xml"
}
}

Jacoco configuration and properties are working fine, how did I confirm this? I created a java class and wrote a unit test for it, sonarqube recognises this and recorded it as part of the Code Coverage, while it basically ignore all the Kotlin file test. I went ahead to change a kotlin file to java and also write a UnitTest for it and yes, it got recorgnised and add as part of the Code Coverage, again, the kotlin file tests were ignored.

Below is my Jacoco.gradle by the way:

apply plugin: 'jacoco'

ext {
    coverageExclusions = [
            '**/*Activity*.*',
            '**/*Fragment*.*',
            '**/R.class',
            '**/R$*.class',
            '**/BuildConfig.*',
    ]
}

jacoco {
    toolVersion = '0.8.6'
    reportsDir = file("$buildDir/reports")
}

tasks.withType(Test) {
    jacoco.includeNoLocationClasses = true
    jacoco.excludes = ['jdk.internal.*']
}


tasks.withType(Test) {
    finalizedBy jacocoTestReport // report is always generated after tests run
}

task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports for Debug build"

    reports {
        xml.enabled(true)
        html.enabled(true)
        xml.destination(file("build/reports/jacocoTestReport.xml"))
    }

    def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug/classes", excludes: coverageExclusions)
    def mainSrc = "/src/main/java"

    additionalSourceDirs.from = files(mainSrc)
    sourceDirectories.from = files([mainSrc])
    classDirectories.from = files([debugTree])

    executionData.from = fileTree(dir: project.buildDir, includes: [
            'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec'
    ])

Solution

  • After so much research and debugging, and also with info's from @LarryX answer above, I was able to play around the classes as he said, below is my working Jacoco.gradle file.

    apply plugin: 'jacoco'
    
    jacoco {
        toolVersion = "0.8.4"
    }
    
    tasks.withType(Test) {
        jacoco.includeNoLocationClasses = true
        jacoco.excludes = ['jdk.internal.*']
    }
    
    project.afterEvaluate {
        (android.hasProperty('applicationVariants')
                ? android.'applicationVariants'
                : android.'libraryVariants')
                .all { variant ->
                    def variantName = variant.name
                    def unitTestTask = "test${variantName.capitalize()}UnitTest"
                    def androidTestCoverageTask = "create${variantName.capitalize()}CoverageReport"
    
                    tasks.create(name: "${unitTestTask}Coverage", type: JacocoReport, dependsOn: [
                            "$unitTestTask",
                            "$androidTestCoverageTask"
                    ]) {
                        group = "Reporting"
                        description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build"
    
                        reports {
                            html.enabled(true)
                            xml.enabled(true)
                            csv.enabled(true)
                        }
    
                        def excludes = [
                                // data binding
                                'android/databinding/**/*.class',
                                '**/android/databinding/*Binding.class',
                                '**/android/databinding/*',
                                '**/androidx/databinding/*',
                                '**/BR.*',
                                // android
                                '**/R.class',
                                '**/R$*.class',
                                '**/BuildConfig.*',
                                '**/Manifest*.*',
                                '**/*Test*.*',
                                'android/**/*.*',
                                // butterKnife
                                '**/*$ViewInjector*.*',
                                '**/*$ViewBinder*.*',
                                // dagger
                                '**/*_MembersInjector.class',
                                '**/Dagger*Component.class',
                                '**/Dagger*Component$Builder.class',
                                '**/*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*.*',
                                // sealed and data classes
                                '**/*$Result.*',
                                '**/*$Result$*.*'
                        ]
    
                        def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir,
                                excludes: excludes)
                        def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}",
                                excludes: excludes)
    
                        classDirectories.setFrom(files([
                                javaClasses,
                                kotlinClasses
                        ]))
    
                        def variantSourceSets = variant.sourceSets.java.srcDirs.collect { it.path }.flatten()
                        sourceDirectories.setFrom(project.files(variantSourceSets))
    
                        def androidTestsData = fileTree(dir: "${buildDir}/outputs/code_coverage/${variantName}AndroidTest/connected/", includes: ["**/*.ec"])
    
                        executionData(files([
                                "$project.buildDir/jacoco/${unitTestTask}.exec",
                                androidTestsData
                        ]))
                    }
    
                }
    }
    

    Take note of :

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

    Also, take note of project.afterEvaluate I'm not sure this is relevant but cause I think you must have run and generated your test before this section runs anyways. This solution also works for a library if not an actual android application.

    And then my sonarqube.gradle file below.

    apply plugin: "org.sonarqube"
    
        sonarqube {
            properties {
                property "sonar.host.url", "http://localhost:9000/"
                property "sonar.projectKey", "fair"
                property "sonar.projectName", "fair"
                property "sonar.login", "de9d79fe4d3aef9879567afc91a2ce465038d9be"
                property "sonar.projectVersion", "${android.defaultConfig.versionName}"
        
                property "sonar.junit.reportsPath", "build/test-results/testDebugUnitTest"
                property "sonar.java.coveragePlugin", "jacoco"
                property "sonar.sourceEncoding", "UTF-8"
                property "sonar.android.lint.report", "build/reports/lint-results.xml"
                property "sonar.jacoco.reportPaths", "build/jacoco/testDebugUnitTest.exec"
                property "sonar.jacoco.itReportPath", fileTree(dir: project.projectDir, includes: ["**/*.ec"])
                property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.xml"
            }
        }
        tasks.sonarqube.dependsOn ":app:testDebugUnitTestCoverage"
    

    Finally my build.gradle(project)

    buildscript {
        repositories {
            google()
            jcenter()
        }
        dependencies {
            classpath "com.android.tools.build:gradle:4.1.2"
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
            classpath "org.jacoco:org.jacoco.core:0.8.7"
            classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3")
        }
    }
    
    allprojects {
        repositories {
            google()
            jcenter()
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }