阅读此文请优先确保已读懂Gradle构建系统简介及在Gradle中集成覆盖率工具Jacoco并使用
在前文中我们对如何在gradle编译体系的工程中加入Jacoco代码覆盖率统计的方法做了介绍,但是前文的方法仅能统计到主工程的代码覆盖率,而无法统计到库工程,其具体原因可以参考此文Issue 76373: Code Coverage does not work for library project,简单总结一下就是目前google提供的android plugin有bug(或者是设计如此):所有的库工程都会使用Release版本来进行编译,即使你声明了TestCompile,而Release版本是无法用于统计代码覆盖率的,因此我们需要一些手段让编译系统能够编译出Debug版本的库工程,参考资料中提供了一个不错的思路,我们在向Dolphin中植入时会遇到一些问题,在此将整个过程详细记录下来,以供参考。
先介绍一下整体思路再分步详述。
要完成植入我们大概需要做如下的几件事:
观察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
}
至此,这个共用文件的修改就完成了
如引文中所述,我们需要将所有的依赖项从:
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"
不要小看这小小的一行命令,里面有着多个重要的知识点,让我们娓娓道来(明白的同学和不关心细节可以自行跳过啦,这些知识和本文无关):
我们尝试使用处理过的包进行编译是发现报错了:
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任务之后强制执行。
这是我们自定义的一个方法,作用是获取到所有依赖项(包括递归依赖项)的工程,后续会把这些工程对应在主工程内生成的jacocoagent.jar删除,获取这个工程列表分成了4步
在这个任务中我们针对之前获取的全部依赖项做删除处理,由于暂时没有弄清楚build/intermediates/exploded-aar下不同的工程编译文件生成的不同路径的原理,因此我暂时也没有找到一条通用的规则适配到全部的工程上,只能根据编译结果修改不同的模式,在浏览器工程下我总结了7条规则,如果要针对其他的项目做移植,需要自己判断是否需要对规则进行修改,甚至直接列出每一个工程的直接路径。后续在调查清楚这些文件生成的逻辑后可能会更新此块的内容。
我们定义的任务是需要被自动执行的,否则编译还是会报错,因此我们需要在android块中声明,这个任务必须在javaCompile后强制执行
variants是android plugin提供的操纵tasks的接口,为了让测试工程也能编译通过,我们还增加了在测试编译时也删除这些多余jar包的配置
至此,插桩的任务就完成了,测试包也可以编译通过并通过引导程序生成代码覆盖率文件了,接下来需要将文件和源码链接起来,即扩大生成报告使用的文件范围到整个项目中
同只统计主工程的方法一致,我们需要对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类型后即正常了,留做后续调研吧。
另外我尝试使用前面我们自定义的方法来递归获取所有以来子工程也是持续报错,暂时放弃这个智能的方法改用目前的手动剔除不需要的工程,同样留给后续调研吧,这两项都不影响我们的集成和使用