Skip to content

Commit ee85ae5

Browse files
committed
Integrated jacoco plugin.
Integrated jacoco report
1 parent c45ee37 commit ee85ae5

File tree

4 files changed

+245
-9
lines changed

4 files changed

+245
-9
lines changed

app/build.gradle.kts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ plugins {
33
id("org.jetbrains.kotlin.android")
44
kotlin("kapt")
55
id("com.google.dagger.hilt.android")
6+
id("jacoco-reports")
67
}
78

89
android {
@@ -27,13 +28,18 @@ android {
2728
isMinifyEnabled = false
2829
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
2930
}
31+
getByName("debug") {
32+
isMinifyEnabled = false
33+
enableUnitTestCoverage = true
34+
enableAndroidTestCoverage = true
35+
}
3036
}
3137
compileOptions {
32-
sourceCompatibility = JavaVersion.VERSION_1_8
33-
targetCompatibility = JavaVersion.VERSION_1_8
38+
sourceCompatibility = JavaVersion.VERSION_11
39+
targetCompatibility = JavaVersion.VERSION_11
3440
}
3541
kotlinOptions {
36-
jvmTarget = "1.8"
42+
jvmTarget = "11"
3743
}
3844
buildFeatures {
3945
compose = true

build.gradle.kts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
12
plugins {
23
val buildVersion = ytemplate.android.build.BuildPlugins
3-
id ("com.android.application") version buildVersion.PLUGIN_VERSION apply false
4-
id ("com.android.library") version buildVersion.PLUGIN_VERSION apply false
5-
id ("org.jetbrains.kotlin.android") version buildVersion.KOTLIN_VERSION apply false
64
id("com.google.dagger.hilt.android") version buildVersion.HILT_PLUGIN apply false
7-
}
5+
}
6+
7+
buildscript {
8+
repositories {
9+
google()
10+
mavenCentral()
11+
}
12+
}

buildSrc/build.gradle.kts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
repositories{
1+
repositories {
22
mavenCentral()
3+
google()
34
}
45

5-
plugins{
6+
plugins {
67
`kotlin-dsl`
8+
}
9+
gradlePlugin {
10+
plugins {
11+
register("jacoco-reports") {
12+
id = "jacoco-reports"
13+
implementationClass = "ytemplate.android.build.JacocoTestReportPlugin"
14+
}
15+
16+
}
17+
}
18+
dependencies {
19+
implementation("com.android.tools.build:gradle:7.3.1")
20+
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
21+
implementation("com.squareup:javapoet:1.13.0")
722
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package ytemplate.android.build
2+
3+
import com.android.build.gradle.BaseExtension
4+
import groovy.xml.XmlSlurper
5+
import groovy.xml.slurpersupport.NodeChild
6+
import org.gradle.api.GradleException
7+
import org.gradle.api.Plugin
8+
import org.gradle.api.Project
9+
import org.gradle.configurationcache.extensions.capitalized
10+
import org.gradle.kotlin.dsl.dependencies
11+
import org.gradle.kotlin.dsl.extra
12+
import org.gradle.kotlin.dsl.register
13+
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
14+
import org.gradle.testing.jacoco.tasks.JacocoReport
15+
import java.io.File
16+
import java.util.*
17+
import kotlin.math.roundToInt
18+
19+
class JacocoTestReportPlugin : Plugin<Project> {
20+
private val Project.android: BaseExtension
21+
get() = extensions.findByName("android") as? BaseExtension
22+
?: error("Not an Android module: $name")
23+
24+
private val Project.jacoco: JacocoPluginExtension
25+
get() = extensions.findByName("jacoco") as? JacocoPluginExtension
26+
?: error("Not a Jacoco module: $name")
27+
private val excludedFiles = mutableSetOf(
28+
// data binding
29+
"android/databinding/**/*.class",
30+
"**/android/databinding/*Binding.class",
31+
"**/android/databinding/*",
32+
"**/androidx/databinding/*",
33+
"**/BR.*",
34+
// android
35+
"**/R.class",
36+
"**/R$*.class",
37+
"**/BuildConfig.*",
38+
"**/Manifest*.*",
39+
"**/*Test*.*",
40+
"android/**/*.*",
41+
// butterKnife
42+
"**/*\$ViewInjector*.*",
43+
"**/*\$ViewBinder*.*",
44+
// dagger
45+
"**/*_MembersInjector.class",
46+
"**/Dagger*Component.class",
47+
"**/Dagger*Component\$Builder.class",
48+
"**/*Module_*Factory.class",
49+
"**/di/module/*",
50+
"**/*_Factory*.*",
51+
"**/*Module*.*",
52+
"**/*Dagger*.*",
53+
"**/*Hilt*.*",
54+
// kotlin
55+
"**/*MapperImpl*.*",
56+
"**/*\$ViewInjector*.*",
57+
"**/*\$ViewBinder*.*",
58+
"**/BuildConfig.*",
59+
"**/*Component*.*",
60+
"**/*BR*.*",
61+
"**/Manifest*.*",
62+
"**/*\$Lambda$*.*",
63+
"**/*Companion*.*",
64+
"**/*Module*.*",
65+
"**/*Dagger*.*",
66+
"**/*Hilt*.*",
67+
"**/*MembersInjector*.*",
68+
"**/*_MembersInjector.class",
69+
"**/*_Factory*.*",
70+
"**/*_Provide*Factory*.*",
71+
"**/*Extensions*.*",
72+
// sealed and data classes
73+
"**/*\$Result.*",
74+
"**/*\$Result$*.*"
75+
)
76+
77+
private val limits = mutableMapOf(
78+
"instruction" to 0.0,
79+
"branch" to 0.0,
80+
"line" to 0.0,
81+
"complexity" to 0.0,
82+
"method" to 0.0,
83+
"class" to 0.0
84+
)
85+
86+
87+
override fun apply(target: Project) {
88+
with(target) {
89+
plugins.run {
90+
apply("jacoco")
91+
}
92+
extra.set("limits", limits)
93+
setupJacocoPlugin()
94+
dependencies {
95+
"implementation"("org.jacoco:org.jacoco.core:0.8.7")
96+
}
97+
}
98+
99+
}
100+
101+
private fun Project.setupJacocoPlugin() {
102+
val buildTypes = android.buildTypes.map { type -> type.name }
103+
var productFlavors = android.productFlavors.map { flavor -> flavor.name }
104+
105+
if (productFlavors.isEmpty()) {
106+
productFlavors = productFlavors + ""
107+
}
108+
productFlavors.forEach { flavorName ->
109+
buildTypes.forEach { buildTypeName ->
110+
val sourceName: String
111+
val sourcePath: String
112+
if (flavorName.isEmpty()) {
113+
sourceName = buildTypeName
114+
sourcePath = buildTypeName
115+
} else {
116+
sourceName = "${flavorName}${buildTypeName.capitalized()}"
117+
sourcePath = "${flavorName}/${buildTypeName}"
118+
}
119+
val testTaskName = "test${sourceName.capitalized()}UnitTest"
120+
addTestCoverageTask(testTaskName, sourceName, sourcePath, flavorName, buildTypeName)
121+
}
122+
}
123+
}
124+
125+
private fun Project.addTestCoverageTask(
126+
taskName: String,
127+
sourceName: String,
128+
sourcePath: String,
129+
flavorName: String,
130+
buildTypeName: String
131+
) {
132+
tasks.register<JacocoReport>("${taskName}Coverage", ) {
133+
dependsOn(taskName)
134+
group = "Reporting"
135+
description = "Generate test coverage reports on the ${sourceName.capitalized()} build"
136+
val javaDirectories =
137+
fileTree("${project.buildDir}/intermediates/classes/$sourcePath") {
138+
exclude(excludedFiles)
139+
}
140+
val kotlinDirectories = fileTree("${project.buildDir}/tmp/kotlin-classes/$sourcePath") {
141+
exclude(excludedFiles)
142+
}
143+
val coverageDirectories = listOf(
144+
"src/main/java",
145+
"src/$flavorName/java",
146+
"src/$buildTypeName/java")
147+
148+
classDirectories.setFrom(files(javaDirectories, kotlinDirectories))
149+
additionalClassDirs.setFrom(files(coverageDirectories))
150+
sourceDirectories.setFrom(files(coverageDirectories))
151+
152+
reports {
153+
xml.required.set(true)
154+
html.required.set(true)
155+
}
156+
val executionDataFiles = fileTree(project.buildDir) {
157+
setIncludes(listOf("**/${taskName}.exec"))
158+
}
159+
executionData.setFrom(
160+
executionDataFiles.files
161+
)
162+
doLast {
163+
jacocoTestReport("${taskName}Coverage")
164+
}
165+
}
166+
}
167+
168+
private fun Project.jacocoTestReport(taskName: String) {
169+
val reportDir = jacoco.reportsDirectory.asFile.get()
170+
val report = file("$reportDir/$taskName/${taskName}.xml")
171+
logger.lifecycle("Checking coverage results:$report")
172+
val metrics = report.extractTestCoverage()
173+
val limits = project.extra["limits"] as Map<String, Double>
174+
val failures = metrics.filter { item ->
175+
item.value < limits[item.key]!!
176+
}.map { item ->
177+
"-${item.key} coverage is: ${item.value}%, minimum is ${limits[item.key]}%"
178+
}
179+
if (failures.isNotEmpty()) {
180+
logger.quiet("======Code coverage failed=========")
181+
failures.forEach {
182+
logger.quiet(it)
183+
}
184+
logger.quiet("===========================================")
185+
throw GradleException("Code coverage failed")
186+
} else {
187+
logger.quiet("======Code coverage success=========")
188+
metrics.forEach {
189+
logger.quiet("- ${it.key} coverage: ${it.value}")
190+
}
191+
logger.quiet("===========================================")
192+
}
193+
}
194+
195+
private fun File.extractTestCoverage(): Map<String, Double> {
196+
val xmlReader = XmlSlurper().apply {
197+
setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
198+
setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
199+
}
200+
val counterNodes: List<NodeChild> = xmlReader.parse(this).parent().children()
201+
.filter { (it as NodeChild).name() == "counter" } as List<NodeChild>
202+
return counterNodes.associate { child ->
203+
val type = child.attributes()["type"].toString().toLowerCase(Locale.US)
204+
val covered = child.attributes()["covered"].toString().toDouble()
205+
val missed = child.attributes()["missed"].toString().toDouble()
206+
val percentage = ((covered / (covered + missed)) * 10000.0).roundToInt() / 100.0
207+
Pair(type, percentage)
208+
}
209+
}
210+
}

0 commit comments

Comments
 (0)