在多项目工程中统计子工程的覆盖率
背景介绍
阅读此文请优先确保已读懂Gradle构建系统简介及在Gradle中集成覆盖率工具Jacoco并使用
在前文中我们对如何在gradle编译体系的工程中加入Jacoco代码覆盖率统计的方法做了介绍,但是前文的方法仅能统计到主工程的代码覆盖率,而无法统计到库工程,其具体原因可以参考此文Issue 76373: Code Coverage does not work for library project,简单总结一下就是目前google提供的android plugin有bug(或者是设计如此):所有的库工程都会使用Release版本来进行编译,即使你声明了TestCompile,而Release版本是无法用于统计代码覆盖率的,因此我们需要一些手段让编译系统能够编译出Debug版本的库工程,参考资料中提供了一个不错的思路,我们在向Dolphin中植入时会遇到一些问题,在此将整个过程详细记录下来,以供参考。
实践与经验
先介绍一下整体思路再分步详述。
要完成植入我们大概需要做如下的几件事:
- 在所有库工程都引入了的DolphinBuild/common.gradle内增加配置项,打开debug模式下的覆盖率统计,同时让所有工程都能产生debug版本的编译结果
- 处理主工程及所有子工程(包括嵌套的子工程)中的依赖关系,使用debug的configuration来进行编译
- 处理插桩时可能存在的jacocoangent.jar重复的问题,删除重复的jar包
- 处理jacocoReport任务,扩展需要分析的文件的范围
配置共用脚本common.gradle
观察Dolphin所有子工程的build.gradle脚本,发现都会有这样一行
apply from: "$project.rootDir/DolphinBuild/common.gradle"
意味着所有的脚本都会引用这个文件,通过修改这个文件我们可以直接修改到所有工程的build行为,而不用一一去更改每个build.gradle文件
为了能在所有工程中都能跑代码覆盖率,我们需要添加在debug版本下对覆盖率的支持:
android {
buildTypes {
debug {
testCoverageEnabled true
}
}
}
为了让库工程能够编译输出Debug版本,我们需要增加如下的部分,可以参考:http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Referencing-a-Library|Referencing-a-Library:
android {
publishNonDefault true
}
至此,这个共用文件的修改就完成了
处理依赖,使用debug编译库工程
如引文中所述,我们需要将所有的依赖项从:
compile project(':DolphinCoreLibrary')
的形式改变为
debugCompile project(path:':DolphinCoreLibrary', configuration: 'debug')
的形式
如果是少数几个文件,慢慢替换即可,可是像Dolphin这样拥有茫茫多库工程和相互依赖的项目来说,通过脚本一次性完成替换是必须的。可以在工程根目录(即shel_en_agile下)运行命令,内容如下:
find ./ -name build.gradle | xargs sed -i "s/^\(.*\)compile project(':\(.*\))/\1debugCompile project(path:':\2, configuration: 'debug')/g"
不要小看这小小的一行命令,里面有着多个重要的知识点,让我们娓娓道来(明白的同学和不关心细节可以自行跳过啦,这些知识和本文无关):
- find命令是linux下的查找命令,-name的参数声明我们要在当前目录(./)下查找所有名字为build.gradle的文件,如果仅执行find命令,输出的是该目录下所有满足条件的文件的路径
- |是linux下的重定向符,将前面命令的结果作为参数传递给后面的命令
- xargs命令的作用是将参数列表转换成小块分段传递给其他命令(这里传递的目标是sed命令)
- sed命令是一个简单的对文件逐行处理的程序,支持正则表达式,-i的参数表示操作会直接在文件中生效而不是显示在控制台上,其后的参数中s表示这次执行替换操作,/是分割符,分开了需要被替换的部分和替换的目标,g表示全局替换,会替换全部的匹配项
- 正则表达式分成了两个部分,匹配部分为<color red>^(.)compile project(':(.))</color>,匹配包含compile project且结尾为")"的行,两组括号(已转义)表示需要提取的group在替换时使用。替换部分为<color red>\1debugCompile project(path:':\2, configuration: 'debug')</color>,表示替换的目标其中的\1和\2表示之前匹配的两个group,其余部分用定义的文字替换
处理插桩时可能存在的jacocoangent.jar重复的问题,删除重复的jar包
我们尝试使用处理过的包进行编译是发现报错了:
Execution failed for task ':DolphinBrowserEN:proguardDebug'.
> java.io.IOException: Can't write [/home/pgao/dolphin/src/shell_en_agile/DolphinBrowserEN/build/intermediates/classes-proguard/debug/classes.jar] (Can't read [/home/pgao/dolphin/src/shell_en_agile/DolphinBrowserEN/build/intermediates/exploded-aar/shell_en_agile.common_library/ui_utils/unspecified/debug/libs/jacocoagent.jar(;;;;;;!META-INF/MANIFEST.MF)] (Duplicate zip entry [jacocoagent.jar:com/vladium/emma/rt/RT.class]))
原因是主工程已经集成了jacoco,库工程又集成会导致同时存在了多个jacocoagent.jar文件,我们需要在执行proguardDebug任务前删除多余的jacocoagent.jar,编译就可以继续进行了,需要在主工程的build.gradle中增加如下的脚本(先贴总脚本,再讲解过程):
task deleteJacocoagentJar {
doLast {
getTransitiveProjectDependencies(this, 'debugCompile').each { project ->
// println "**********" + "build/intermediates/exploded-aar/${rootProject.name}.services/${project.name}/unspecified/debug/libs/jacocoagent.jar" + "***********"
delete "build/intermediates/exploded-aar/${rootProject.name}.services/${project.name}/unspecified/debug/libs/jacocoagent.jar"
delete "build/intermediates/exploded-aar/${rootProject.name}.common_library/${project.name}/unspecified/debug/libs/jacocoagent.jar"
delete "build/intermediates/exploded-aar/${rootProject.name}.third_party/${project.name}/unspecified/debug/libs/jacocoagent.jar"
delete "build/intermediates/exploded-aar/${rootProject.name}/${project.name}/unspecified/debug/libs/jacocoagent.jar"
delete "build/intermediates/exploded-aar/shell_en_agile.third_party.animator/library/unspecified/debug/libs/jacocoagent.jar"
delete "build/intermediates/exploded-aar/shell_en_agile.third_party.svg-android/svgandroid/unspecified/debug/libs/jacocoagent.jar"
delete "build/intermediates/exploded-aar/shell_en_agile.services.promotion_service/promotion_link/unspecified/debug/libs/jacocoagent.jar"
//delete "/home/pgao/dolphin/src/topstory/DolphinNewsClient/build/intermediates/exploded-aar/topstory.services/news_service/unspecified/debug/libs/jacocoagent.jar"
}
}
}
def getTransitiveProjectDependencies(project, configuration) {
def projectDependencies = project.configurations."$configuration".getAllDependencies().withType(ProjectDependency)
def dependencyProjects = projectDependencies*.dependencyProject
dependencyProjects.each {
dependencyProjects += getTransitiveProjectDependencies(it, configuration)
}
return dependencyProjects.unique()
}
android {
applicationVariants.all { variant ->
variant.dex.dependsOn deleteJacocoagentJar
deleteJacocoagentJar.mustRunAfter variant.javaCompile
}
testVariants.all { variant ->
variant.dex.dependsOn deleteJacocoagentJar
deleteJacocoagentJar.mustRunAfter variant.javaCompile
}
}
我们定义了一个名字为:deleteJacocoagentJar的任务,和一个getTransitiveProjectDependencies的方法,并将这个任务绑定到了javaCompile任务之后强制执行。
getTransitiveProjectDependencies
这是我们自定义的一个方法,作用是获取到所有依赖项(包括递归依赖项)的工程,后续会把这些工程对应在主工程内生成的jacocoagent.jar删除,获取这个工程列表分成了4步
- 获取当前工程全部的依赖项
- 针对每一个依赖项获取其工程,并加入列表
- 递归处理每一个库工程,将所有的依赖工程加入列表
- 针对列表做去重处理并返回
deleteJacocoagentJar任务
在这个任务中我们针对之前获取的全部依赖项做删除处理,由于暂时没有弄清楚build/intermediates/exploded-aar下不同的工程编译文件生成的不同路径的原理,因此我暂时也没有找到一条通用的规则适配到全部的工程上,只能根据编译结果修改不同的模式,在浏览器工程下我总结了7条规则,如果要针对其他的项目做移植,需要自己判断是否需要对规则进行修改,甚至直接列出每一个工程的直接路径。后续在调查清楚这些文件生成的逻辑后可能会更新此块的内容。
将这个任务绑定到了javaCompile任务之后强制执行
我们定义的任务是需要被自动执行的,否则编译还是会报错,因此我们需要在android块中声明,这个任务必须在javaCompile后强制执行
variants是android plugin提供的操纵tasks的接口,为了让测试工程也能编译通过,我们还增加了在测试编译时也删除这些多余jar包的配置
至此,插桩的任务就完成了,测试包也可以编译通过并通过引导程序生成代码覆盖率文件了,接下来需要将文件和源码链接起来,即扩大生成报告使用的文件范围到整个项目中
处理jacocoReport任务,扩展需要分析的文件的范围
同只统计主工程的方法一致,我们需要对jacocoReort任务做修改,扩大sourceDirectories和classDirectories的范围,成品如下:
task jacocoReportNew(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled true
html.enabled true
}
def excludesFilter = ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/BuildConfig.*',
'**/Manifest*.*',]
sourceDirectories = files("src")
classDirectories = fileTree(dir: "./build/intermediates/classes/debug", excludes: excludesFilter)
project.rootProject.allprojects.each { project ->
if (project.name != "shell_en_agile" && project.name != "DolphinRecordTest" && project.name != "DolphinBrowserEN"){
sourceDirectories += files((project.projectDir).toString() + "/src")
classDirectories += fileTree(dir:(project.projectDir).toString() + "/build/intermediates/classes/debug", excludes: excludesFilter)
}
}
executionData = fileTree(dir: "/home/pgao/code-coverage/shell")
}
我们除了将主工程的相关文件加入以外,还通过遍历根工程下所有子工程的方法增加了其他工程的相关文件。
在实际操作工程中,sourceDirectories虽然在官方文档中给出的类型是FileCollection,按理说FileTree是他的子类,应该满足需求,但是使用FileTree死活无法链接到源文件,改为Files类型后即正常了,留做后续调研吧。
另外我尝试使用前面我们自定义的方法来递归获取所有以来子工程也是持续报错,暂时放弃这个智能的方法改用目前的手动剔除不需要的工程,同样留给后续调研吧,这两项都不影响我们的集成和使用