背景介紹
閱讀此文請優(yōu)先確保已讀懂Gradle構(gòu)建系統(tǒng)簡介及在Gradle中集成覆蓋率工具Jacoco并使用
在前文中我們對如何在gradle編譯體系的工程中加入Jacoco代碼覆蓋率統(tǒng)計的方法做了介紹,但是前文的方法僅能統(tǒng)計到主工程的代碼覆蓋率,而無法統(tǒng)計到庫工程,其具體原因可以參考此文Issue 76373: Code Coverage does not work for library project,簡單總結(jié)一下就是目前google提供的android plugin有bug(或者是設計如此):所有的庫工程都會使用Release版本來進行編譯,即使你聲明了TestCompile,而Release版本是無法用于統(tǒng)計代碼覆蓋率的,因此我們需要一些手段讓編譯系統(tǒng)能夠編譯出Debug版本的庫工程,參考資料中提供了一個不錯的思路,我們在向Dolphin中植入時會遇到一些問題,在此將整個過程詳細記錄下來,以供參考。
實踐與經(jīng)驗
先介紹一下整體思路再分步詳述。
要完成植入我們大概需要做如下的幾件事:
- 在所有庫工程都引入了的DolphinBuild/common.gradle內(nèi)增加配置項,打開debug模式下的覆蓋率統(tǒng)計,同時讓所有工程都能產(chǎn)生debug版本的編譯結(jié)果
- 處理主工程及所有子工程(包括嵌套的子工程)中的依賴關系,使用debug的configuration來進行編譯
- 處理插樁時可能存在的jacocoangent.jar重復的問題,刪除重復的jar包
- 處理jacocoReport任務,擴展需要分析的文件的范圍
配置共用腳本common.gradle
觀察Dolphin所有子工程的build.gradle腳本,發(fā)現(xiàn)都會有這樣一行
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')
的形式改變?yōu)?br>
debugCompile project(path:':DolphinCoreLibrary', configuration: 'debug')
的形式
如果是少數(shù)幾個文件,慢慢替換即可,可是像Dolphin這樣擁有茫茫多庫工程和相互依賴的項目來說,通過腳本一次性完成替換是必須的。可以在工程根目錄(即shel_en_agile下)運行命令,內(nèi)容如下:
find ./ -name build.gradle | xargs sed -i "s/^\(.*\)compile project(':\(.*\))/\1debugCompile project(path:':\2, configuration: 'debug')/g"
不要小看這小小的一行命令,里面有著多個重要的知識點,讓我們娓娓道來(明白的同學和不關心細節(jié)可以自行跳過啦,這些知識和本文無關):
- find命令是linux下的查找命令,-name的參數(shù)聲明我們要在當前目錄(./)下查找所有名字為build.gradle的文件,如果僅執(zhí)行find命令,輸出的是該目錄下所有滿足條件的文件的路徑
- |是linux下的重定向符,將前面命令的結(jié)果作為參數(shù)傳遞給后面的命令
- xargs命令的作用是將參數(shù)列表轉(zhuǎn)換成小塊分段傳遞給其他命令(這里傳遞的目標是sed命令)
- sed命令是一個簡單的對文件逐行處理的程序,支持正則表達式,-i的參數(shù)表示操作會直接在文件中生效而不是顯示在控制臺上,其后的參數(shù)中s表示這次執(zhí)行替換操作,/是分割符,分開了需要被替換的部分和替換的目標,g表示全局替換,會替換全部的匹配項
- 正則表達式分成了兩個部分,匹配部分為<color red>^(.)compile project(':(.))</color>,匹配包含compile project且結(jié)尾為")"的行,兩組括號(已轉(zhuǎn)義)表示需要提取的group在替換時使用。替換部分為<color red>\1debugCompile project(path:':\2, configuration: 'debug')</color>,表示替換的目標其中的\1和\2表示之前匹配的兩個group,其余部分用定義的文字替換
處理插樁時可能存在的jacocoangent.jar重復的問題,刪除重復的jar包
我們嘗試使用處理過的包進行編譯是發(fā)現(xiàn)報錯了:
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]))
原因是主工程已經(jīng)集成了jacoco,庫工程又集成會導致同時存在了多個jacocoagent.jar文件,我們需要在執(zhí)行proguardDebug任務前刪除多余的jacocoagent.jar,編譯就可以繼續(xù)進行了,需要在主工程的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任務之后強制執(zhí)行。
getTransitiveProjectDependencies
這是我們自定義的一個方法,作用是獲取到所有依賴項(包括遞歸依賴項)的工程,后續(xù)會把這些工程對應在主工程內(nèi)生成的jacocoagent.jar刪除,獲取這個工程列表分成了4步
- 獲取當前工程全部的依賴項
- 針對每一個依賴項獲取其工程,并加入列表
- 遞歸處理每一個庫工程,將所有的依賴工程加入列表
- 針對列表做去重處理并返回
deleteJacocoagentJar任務
在這個任務中我們針對之前獲取的全部依賴項做刪除處理,由于暫時沒有弄清楚build/intermediates/exploded-aar下不同的工程編譯文件生成的不同路徑的原理,因此我暫時也沒有找到一條通用的規(guī)則適配到全部的工程上,只能根據(jù)編譯結(jié)果修改不同的模式,在瀏覽器工程下我總結(jié)了7條規(guī)則,如果要針對其他的項目做移植,需要自己判斷是否需要對規(guī)則進行修改,甚至直接列出每一個工程的直接路徑。后續(xù)在調(diào)查清楚這些文件生成的邏輯后可能會更新此塊的內(nèi)容。
將這個任務綁定到了javaCompile任務之后強制執(zhí)行
我們定義的任務是需要被自動執(zhí)行的,否則編譯還是會報錯,因此我們需要在android塊中聲明,這個任務必須在javaCompile后強制執(zhí)行
variants是android plugin提供的操縱tasks的接口,為了讓測試工程也能編譯通過,我們還增加了在測試編譯時也刪除這些多余jar包的配置
至此,插樁的任務就完成了,測試包也可以編譯通過并通過引導程序生成代碼覆蓋率文件了,接下來需要將文件和源碼鏈接起來,即擴大生成報告使用的文件范圍到整個項目中
處理jacocoReport任務,擴展需要分析的文件的范圍
同只統(tǒng)計主工程的方法一致,我們需要對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類型后即正常了,留做后續(xù)調(diào)研吧。
另外我嘗試使用前面我們自定義的方法來遞歸獲取所有以來子工程也是持續(xù)報錯,暫時放棄這個智能的方法改用目前的手動剔除不需要的工程,同樣留給后續(xù)調(diào)研吧,這兩項都不影響我們的集成和使用