-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjacoco.gradle
284 lines (244 loc) · 11.1 KB
/
jacoco.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/*
* 用于生成 jacoco 测试覆盖率报告( 合并 本地单元测试 及 androidTest )
*
* 引入:
* apply from: "jacoco.gradle"
*
* 配置:
* 支持配置自定义的过滤规则,配置方式如下:
* - 可在模块根目录下添加一个名为 jacoco-exclude 的文件,其中每一行代表一个过滤条件,可使用正则表达式进行编写
*
*
* 运行:
* - 执行 gradle 任务 : jacocoTestReport 生成测试覆盖率报告
* - 生成的报告位于: build/jacoco/jacocoHtml
* - 执行 jacocoExcludes 任务输出所有的过滤规则,直接在任务执行输出中查看
* - 执行 jacocoMergeDebugExec 任务合并androidTest及本地单元测试的jacoco的执行exec文件,后续可在AndroidStudio中加载覆盖率数据
* - 合并后的文件位于 build/jacoco/ 目录中 xx-jacocoMergeXXXExec.exec 文件
* - AS 加载exec文件的路径: 菜单 - Run - Show Coverage Data - 选取 exec 文件
* - 注意: 由于AGP插件问题,目前library项目的exec合并有问题,升级到AGP7.2.0-beta04之后正常
* - 执行 archiverJacocoTestReport 会执行jacocoTestReport任务,然后将build/jacoco拷贝到项目下面的 reporting 目录
*
* 注意:
* - !!!测试pass后才会生成报告!!!
* - 执行期间请保持屏幕开启状态,同时,注意手机上可能弹出的测试应用安装确认弹框,以确保 androidTest 能正常执行
* - MIUI需要关闭 开发者选项-MIUI优化 选项(自动测试需要,单独测试时可只手动开启对应测试app的自启动及后台弹出界面的权限)
*
* 其他说明:
* 根据模块的不同类型,在AGP版本为7.2.0以下时,可选如下执行方式:
* library 项目中:
* 执行 gradle 任务 : jacocoTestReport | jacocoTestReleaseReport | acrhiveJacocoTestReleaseReport
* app 项目中
* 执行 gradle 任务: jacocoTestDebugReport | acrhiveJacocoTestDebugReport
*
*/
import java.text.SimpleDateFormat
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.7"
}
android {
buildTypes {
debug {
testCoverageEnabled = true
}
}
}
def groupName = "reporting"
afterEvaluate { project ->
project.extensions.create("jacocoAndroid",
JacocoAndroidExtension, loadExcludesFromFile(project) /*[]*/)
project.plugins.apply(JacocoPlugin)
Plugin plugin = findAndroidPluginOrThrow(project.plugins)
def variants = getVariants(project, plugin)
// agp 7.2.0-beta04 之前的版本,library模块中可使用时,使用此任务可规避无法合并的问题
Task jacocoTestReportTask = findOrCreateJacocoTestReportTask(project.tasks)
variants.all { variant ->
def unitTestTask = tasks.getByName("test${variant.name.capitalize()}UnitTest")
def createDebugCoverageReportTask = tasks.getByName("createDebugCoverageReport")
def sourceDirs = variant.sourceSets.java.srcDirs.collect { it.path }.flatten()
def classesDir
if (variant.hasProperty('javaCompileProvider')) {
classesDir = variant.javaCompileProvider.get().destinationDir
} else {
classesDir = variant.javaCompile.destinationDir
}
def androidTestPath = "${project.buildDir}/outputs/code_coverage/debugAndroidTest/connected/"
def unitTestExecutionData = unitTestTask.jacoco.destinationFile.path
FileTree javaTree = project.fileTree(dir: classesDir, excludes: project.jacocoAndroid.allExcludes())
def kotlinClassesDir = "${project.buildDir}/tmp/kotlin-classes/${variant.name}"
def kotlinTree = project.fileTree(dir: kotlinClassesDir, excludes: project.jacocoAndroid.allExcludes())
JacocoMerge mergeTask = null;
if (jacocoAndroid.enableMergeExecTask) {
// keep mergeTask because android studio can not recognise androidTest *.ec (Run - Show Coverage Data)
mergeTask = tasks.create("jacocoMerge${variant.name.capitalize()}Exec", JacocoMerge.class) {
description("merge ${unitTestTask.name} and ${createDebugCoverageReportTask.name} executionData")
executionData(fileTree(unitTestExecutionData), fileTree(androidTestPath))
destinationFile = new File("${project.buildDir}/jacoco/${variant.name}-${name}.exec")
group(groupName)
dependsOn(unitTestTask, createDebugCoverageReportTask)
doFirst {
executionData.forEach {
logger.log(LogLevel.LIFECYCLE, "-mergeTask exec: $it.absolutePath")
}
}
}
}
JacocoReport reportTask = tasks.create("jacocoTest${variant.name.capitalize()}Report", JacocoReport.class) {
group(groupName)
description("create merged ${unitTestTask.name} and ${createDebugCoverageReportTask.name} jacocoTestReport")
if (mergeTask != null) {
dependsOn(mergeTask)
executionData.from(fileTree(mergeTask.destinationFile))
} else {
dependsOn(unitTestTask, createDebugCoverageReportTask)
executionData.from(fileTree(unitTestExecutionData), fileTree(androidTestPath))
}
sourceDirectories.from(files(sourceDirs))
classDirectories.from(files(javaTree), files(kotlinTree))
reports {
def destination = project.jacocoAndroid.destination
xml.enabled = true
html.enabled true
html.destination new File((destination == null)
? "${project.buildDir}/jacoco/jacocoHtml"
: "${destination.trim()}/jacocoHtml")
}
}
jacocoTestReportTask.dependsOn reportTask
String destinationDir = reportTask.reports.html.destination.getParent()
// 添加辅助任务
if (tasks.findByName("acrhive${jacocoTestReportTask.name.capitalize()}") == null) {
tasks.create("acrhive${jacocoTestReportTask.name.capitalize()}", Copy.class) {
group("${groupName}Archive")
description("archive ${jacocoTestReportTask.name} to projectDir")
from("${destinationDir}")
into("${project.projectDir}/reporting/")
dependsOn jacocoTestReportTask
}
}
tasks.create("acrhive${reportTask.name.capitalize()}", Copy.class) {
group("${groupName}Archive")
description("archive ${reportTask.name} to projectDir")
from("${destinationDir}")
into("${project.projectDir}/reporting/")
dependsOn reportTask
}
}
tasks.create("jacocoExcludes") {
group(groupName)
description("print jacoco exclude rules")
doLast {
for (item in project.jacocoAndroid.allExcludes()) {
project.logger.log(LogLevel.LIFECYCLE, item)
}
}
}
}
class JacocoAndroidExtension {
public static final Collection<String> androidDataBindingExcludes =
['android/databinding/**/*.class',
'**/android/databinding/*Binding.class',
'**/databinding/*Binding.class',
'**/databinding/*BindingImpl.class',
'**/databinding/*Sw600dpImpl.class',
'**/DataBinder*.class',
'**/DataBindingTriggerClass.class',
'**/BR.*'].asImmutable()
public static final Collection<String> androidExcludes =
['**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*'].asImmutable()
public static final Collection<String> butterKnifeExcludes =
['**/*$ViewInjector*.*',
'**/*$ViewBinder*.*'].asImmutable()
public static final Collection<String> dagger2Excludes =
['**/*_MembersInjector.class',
'**/*_Factory.class',
'**/Dagger*Component.class',
'**/Dagger*Component$*.class',
'**/Dagger*Component$Builder.class',
'**/*Module_*Factory.class',
// dagger-android
'**/*_ContributeModule_*.*',
].asImmutable()
public static final Collection<String> aRouterExcludes =
["com/alibaba/android/arouter/routes/**"].asImmutable()
public static final Collection<String> defaultExcludes =
(androidDataBindingExcludes + androidExcludes + butterKnifeExcludes + dagger2Excludes)
.asImmutable()
Collection<String> excludes
boolean csv
boolean html
boolean xml
String destination
boolean enableMergeExecTask
Collection<String> allExcludes() {
return (defaultExcludes + excludes)
}
JacocoAndroidExtension(Collection<String> excludes) {
this.excludes = excludes
this.csv = true
this.html = true
this.xml = true
this.destination = null
this.enableMergeExecTask = true
}
}
private static Collection<String> loadExcludesFromFile(Project project) {
File file = new File("${project.projectDir}/jacoco-excludes")
if (file.exists()) {
project.logger.log(LogLevel.DEBUG, "${file.absolutePath} found!")
} else {
project.logger.log(LogLevel.WARN, "${file.absolutePath} not found!")
return []
}
List<String> excludeList = new ArrayList<>()
BufferedReader fileReader;
try {
fileReader = new BufferedReader(new FileReader(file))
String line
while ((line = fileReader.readLine()) != null) {
if (line.trim().isEmpty()) {
continue
}
if (!line.trim().startsWith("#")) {
excludeList.add(line.trim() + "")
}
}
} catch (Exception ignored) {
project.logger.log(LogLevel.ERROR, "add exclude failed!")
} finally {
if (fileReader != null) {
fileReader.close()
}
}
excludeList.forEach {
project.logger.log(LogLevel.DEBUG, "add exclude:" + it)
}
return excludeList
}
private static def getVariants(Project project, Plugin plugin) {
boolean isLibraryPlugin = plugin.class.name.endsWith('.LibraryPlugin')
project.android[isLibraryPlugin ? "libraryVariants" : "applicationVariants"]
}
private static Plugin findAndroidPluginOrThrow(PluginContainer plugins) {
Plugin plugin = plugins.findPlugin('android') ?: plugins.findPlugin('android-library')
if (!plugin) {
throw new GradleException(
'You must apply the Android plugin or the Android library plugin before using the jacoco-android plugin')
}
plugin
}
private static Task findOrCreateJacocoTestReportTask(TaskContainer tasks) {
Task jacocoTestReportTask = tasks.findByName("jacocoTestReport")
if (!jacocoTestReportTask) {
jacocoTestReportTask = tasks.create("jacocoTestReport")
jacocoTestReportTask.group = "Reporting"
}
jacocoTestReportTask
}
private static String timeStamp() {
return new SimpleDateFormat("YYYY-MM-dd_HH-mm").format(new Date())
}