SonarQube 代码覆盖率无法解释 Android 项目中的 Kotlin 文件

Posted

技术标签:

【中文标题】SonarQube 代码覆盖率无法解释 Android 项目中的 Kotlin 文件【英文标题】:SonarQube Code Coverage Could not account for Kotlin files on an Android Project 【发布时间】:2021-09-22 00:04:13 【问题描述】:

下面是我的 sonarqube 属性 sn-p:

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


Jacoco 配置和属性工作正常,我如何确认这一点? 我创建了一个 java 类并为它编写了一个单元测试,sonarqube 识别了这一点并将其记录为代码覆盖率的一部分,而它基本上忽略了所有 Kotlin 文件测试。 我继续将 kotlin 文件更改为 java 并为它编写了一个 UnitTest,是的,它被重新识别并添加为代码覆盖率的一部分,再次忽略了 kotlin 文件测试。

下面是我的 Jacoco.gradle:

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'
    ])

【问题讨论】:

运行测试时,它会生成任何 .exec 文件吗?多少?它应该在 build/jacoco 目录中 是的,只有一个文件 testDebugUnitTest.exec 【参考方案1】:

请检查 java + kotlin 项目的 jacoco 配置:

http://vgaidarji.me/blog/2017/12/20/how-to-configure-jacoco-for-kotlin-and-java-project/

关键是在 sourceDirectories 和 classeDirectories 中包含 kotlin 目录

【讨论】:

我不确定这一点,但我能够通过玩弄你所说的来让它工作,现在它工作了,我也会把它作为答案发布。【参考方案2】:

经过这么多的研究和调试,以及上面@LarryX 回答的信息,我能够按照他所说的那样玩这些课程,下面是我的工作Jacoco.gradle 文件。

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: "$unitTestTaskCoverage", 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/$variantNameAndroidTest/connected/", includes: ["**/*.ec"])

                    executionData(files([
                            "$project.buildDir/jacoco/$unitTestTask.exec",
                            androidTestsData
                    ]))
                

            

注意:

  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
                ]))

另外,请注意project.afterEvaluate 我不确定这是否相关,但因为我认为您必须在本节运行之前运行并生成您的测试。 如果不是实际的 android 应用程序,此解决方案也适用于库。

然后是我的sonarqube.gradle 文件。

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"

最后是我的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

【讨论】:

以上是关于SonarQube 代码覆盖率无法解释 Android 项目中的 Kotlin 文件的主要内容,如果未能解决你的问题,请参考以下文章

SonarQube + Jacoco - 无法读取 Koin 模块测试覆盖率

SonarQube代码覆盖率的增量分析

SonarQube:从未计算过新代码的覆盖率

SonarQube 8.2 分析显示 0 代码覆盖率

如何在 sonarqube 中设置代码覆盖率值?

代码覆盖率的 SonarQube 增量分析