##本地覆盖率
####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分支),筛选出当前需求增加的代码;
- 根据增量代码,去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,展示图表,可以包括:所有项目单侧覆盖率表,单个项目单侧覆盖率趋势图等。