单侧覆盖率方案

139 阅读3分钟

##本地覆盖率

####jacoco大致原理 jacoco使用ASM字节码框架在原有class字节码中的指定位置插入探针,jacoco的探针实际是一 个布尔值,当代码执行到探针位置时,将其置为true,该探针前面的代码会被认为执行过,然后 对该部分代码对应的html文件中的css样式进行染色(红色表示未覆盖,绿色表示已覆盖,黄色 表示部分覆盖),形成最终的覆盖率报告。

###覆盖率大致原理 全量覆盖率: 1,主工程app 的build.gradle引入jacoco插件,执行单侧用例,jacoco记录单侧覆盖率,生成exec文件, 2,jacoco通过解析exec文件,生成全量覆盖率报告;

增量覆盖率 3. 利用diff-cover库(Python库),根据当前分支(push代码会生成临时分支,例如:unittest分支)和目标分支(develop分支),筛选出当前需求增加的代码;

  1. 根据增量代码,去exec文件里面查找哪些代码时被调用过的,生成增量覆盖率报告;

#####全量覆盖率 给项目应用jacoco插件,利用jacoco进行全量覆盖率统计,并可进行task封装,例如:

task testReport(type: JacocoReport, dependsOn: javaUnitTestTasks, group: group) {
    enabled = true

    additionalSourceDirs.from = (files(getAllAssignedSrcDirs()))
    sourceDirectories.from = (files(getAllAssignedSrcDirs()))
    def fileFilter = [
            '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
            '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*', '**/dagger/**', '**/databinding/**'
    ]

    if (project.ext.has("unitTest_excludeFile")
            && project.ext.get("unitTest_excludeFile") != null
            && project.ext.get("unitTest_excludeFile") instanceof List) {
        fileFilter.addAll(project.ext.unitTest_excludeFile)
    }

    classDirectories.from = (files(getAllAssignedClassDirs()).collect {
        fileTree(dir: it, exclude: fileFilter)
    })
    executionData.from = fileTree("$rootProject.rootDir", {
        includes = ['**/*.exec', '**/*coverage.ec']
    })

    onlyIf = {
        true
    }

    reports {
        csv.enabled false
        html.enabled = true
        xml.enabled = true
        if (xml.enabled) {
            xml.destination new File("${project.buildDir}/jacoco/testReportWithNecessary/testReportWithNecessary.xml")
        }
    }
}

执行gradlew testReport指令,即可在module build->reports目录生成全量覆盖率文件;下图b

#####增量覆盖率

增量覆盖率是利用一个diff-cover的库,针对jacoco生成的统计结果,筛选出增量代码,进行增量覆盖率统计,task封装,例如:

task testDiffReport(dependsOn: [testReport], group: group) {
    doFirst {
        if (isGitLibCi()) {
            println("run with gitlib ci, so your python and pip3 diff_cover module environment will not be auto install.")
        } else {
            try {
                def cmdOutput = "pip3 show --files diff_cover".execute().text
                if (cmdOutput == null || cmdOutput.length() == 0) {
                    println("you don't have diff_cover,Start installing...")
                    cmdOutput = "pip3 install diff-cover".execute().text
                    println("$cmdOutput")
                    cmdOutput = "pip3 show --files diff_cover".execute().text
                    if (cmdOutput == null || cmdOutput.length() == 0) {
                        throw new GradleException("diff-cover install failed,Please execute pip3 install diff_cover manually. \n$e")
                    }
                } else {
                    println("check you python and pip diff_cover module environment success.")
                }
            } catch (IOException e) {
                throw new GradleException("python environment is not available,ensure  Python and pip3 commands are available。\n$e")
            }
        }
    }

    doLast {
        def baseBranch = project.getProperties().get("baseBranch")
        if (baseBranch == null) {
            baseBranch = "origin/dev"
        }

        def reportXmlFile = file("${project.buildDir.path}/jacoco/testReportWithNecessary/testReportWithNecessary.xml")
        if (reportXmlFile.exists() && reportXmlFile.size() > 0) {
            println "Preparing to output diff-cover Report..."

            def srcDirs = debugForAllAssignedSrcDirs().join(" ")
            if (!diffReportHtmlFile.parentFile.exists()) {
                diffReportHtmlFile.parentFile.mkdirs()
            }
            def expectIncrementCoverage = getExpectIncrementCoverage()
            println "expectIncrementCoverage = $expectIncrementCoverage"
            if (expectIncrementCoverage != null) {
                minDiffCoverage = expectIncrementCoverage
            }
            //https://github.com/edx/edx-platform/pull/26879/commits/fde1b57d9378f01924014062e0896374087bb44f
            def cmd = "diff-cover ${reportXmlFile.absolutePath} --diff-range-notation .. --compare-branch=${baseBranch} --src-roots ${srcDirs} --html-report ${diffReportHtmlFile} --fail-under=${minDiffCoverage}"
            println "${cmd}"

            def sout = new StringBuilder(), serr = new StringBuilder()
            def proc = cmd.execute()
            proc.consumeProcessOutput(sout, serr)
            proc.waitFor()
            println "${sout}\n${serr}"
        }
}

执行gradlew testDiffReport -PbaseBranch=origin/{分支名} 指令,即可在module build->reports目录生成增量代码覆盖率文件,下图c

testDiffReport任务依赖testReport,testReport任务依赖testDebugUnitTest任务, 所以可以直接执行gradlew testDiffReport指令生成增量覆盖率,全量覆盖率和单测结果文件 (下图a)。

报告

全量覆盖率,例如下图: 全量覆盖率

增量覆盖率,例如下图: 增量覆盖率

#####排除文件 如果某个文件夹的内容或者某个类不需要进行单测,可以在build.gradle中使用 ext.unitTest_excludeFile字段排除,例如想排除internal下的所有类和WAppRuntime类,使用 ext.unitTest_excludeFile = ['/internal/','**/xx.class']

#####一个项目多个module 一个项目多个module,统计多个module单侧覆盖率,可以在app module中增加全局增量覆盖率task,收集所有module中的覆盖率task,并执行,最后将结果拷贝到根目录,例如:

def testModules = []
def testReportTasks = []
def testDiffReportTasks = []

def excludeModule = []
if (project.ext.has("unitTest_excludeModule")
        && project.ext.get("unitTest_excludeModule") != null
        && project.ext.get("unitTest_excludeModule") instanceof List) {
    excludeModule = project.ext.get("unitTest_excludeModule")
}

task getTestModule {
    rootProject.subprojects.forEach {
        if (it.name != "app" && !excludeModule.contains(it.name)) {
            it.afterEvaluate {
                def taskNames = it.getTasks().collect { it.name }
                if (taskNames.contains("testReport")) {
                    testModules.add(":${it.name}")
                    testReportTasks.add(":${it.name}:testReport")
                    testDiffReportTasks.add(":${it.name}:testDiffReport")
                }
            }
        }
    }
}

task testAllModuleReport(dependsOn: [getTestModule, testReportTasks], group: group) {
}

task testAllModuleDiffReport(dependsOn: [getTestModule, testDiffReportTasks], group: group) {
}

gradle.buildFinished {
    testModules.forEach { modulePath ->
        copy {
            from "${project(modulePath).buildDir.absolutePath}/reports/"
            into "${project.buildDir.absolutePath}/reports/submodules/${modulePath.replace(':', '$')}/"
        }
    }
}

###gerrit关联覆盖率 1,jenkins 电脑上安装diff-cover软件 2,gerrit push代码时,通过webhook出发jenkins任务,根据临时分支拉取项目源码(gerrit提交代码后会生成临时分支),执行项目中的增量覆盖率task:gradlew testDiffReport -xxx, 3,生成报告之后,将报告上传到jenkins页面 4,通过gerrit api将增量覆盖率以及报告链接,评论到gerrit。

###覆盖率报告 1,代码合并之后,可触发jenkins流水线执行全量覆盖率task; 2,生成全量覆盖率报告之后,可通过xmlparser解析报告,获取全量覆盖率值, 3,可将全量覆盖率,以及对应字段,比如,项目名称,commit信息等存到数据库,例如(redash数据库),redash图表,或者h5文件进行展示; 4,展示图表,可以包括:所有项目单侧覆盖率表,单个项目单侧覆盖率趋势图等。