208 lines
6.3 KiB
Text
208 lines
6.3 KiB
Text
|
import groovy.xml.XmlSlurper
|
||
|
import groovy.xml.slurpersupport.NodeChild
|
||
|
import java.io.File
|
||
|
import java.util.Locale
|
||
|
import org.gradle.api.GradleException
|
||
|
import org.gradle.api.Project
|
||
|
import org.gradle.kotlin.dsl.dependencies
|
||
|
import org.gradle.kotlin.dsl.extra
|
||
|
import org.gradle.kotlin.dsl.register
|
||
|
import org.gradle.testing.jacoco.tasks.JacocoReport
|
||
|
import kotlin.math.roundToInt
|
||
|
|
||
|
plugins {
|
||
|
id("com.android.library")
|
||
|
id("jacoco")
|
||
|
}
|
||
|
|
||
|
private val limits = mutableMapOf(
|
||
|
"instruction" to 0.0,
|
||
|
"branch" to 0.0,
|
||
|
"line" to 0.0,
|
||
|
"complexity" to 0.0,
|
||
|
"method" to 0.0,
|
||
|
"class" to 0.0
|
||
|
)
|
||
|
|
||
|
extra.set("limits", limits)
|
||
|
|
||
|
dependencies {
|
||
|
"implementation"("org.jacoco:org.jacoco.core:${Versions.jacoco}")
|
||
|
}
|
||
|
|
||
|
project.afterEvaluate {
|
||
|
val buildTypes = android.buildTypes.map { type -> type.name }
|
||
|
var productFlavors = android.productFlavors.map { flavor -> flavor.name }
|
||
|
|
||
|
if (productFlavors.isEmpty()) {
|
||
|
productFlavors = productFlavors + ""
|
||
|
}
|
||
|
|
||
|
productFlavors.forEach { flavorName ->
|
||
|
buildTypes.forEach { buildTypeName ->
|
||
|
val sourceName: String
|
||
|
val sourcePath: String
|
||
|
|
||
|
if (flavorName.isEmpty()) {
|
||
|
sourceName = buildTypeName
|
||
|
sourcePath = buildTypeName
|
||
|
} else {
|
||
|
sourceName = "${flavorName}${buildTypeName.replaceFirstChar(Char::titlecase)}"
|
||
|
sourcePath = "${flavorName}/${buildTypeName}"
|
||
|
}
|
||
|
|
||
|
val testTaskName = "test${sourceName.replaceFirstChar(Char::titlecase)}UnitTest"
|
||
|
//println("Task -> $testTaskName")
|
||
|
|
||
|
registerCodeCoverageTask(
|
||
|
testTaskName = testTaskName,
|
||
|
sourceName = sourceName,
|
||
|
sourcePath = sourcePath,
|
||
|
flavorName = flavorName,
|
||
|
buildTypeName = buildTypeName
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
val excludedFiles = mutableSetOf(
|
||
|
// data binding
|
||
|
"android/databinding/**/*.class",
|
||
|
"**/android/databinding/*Binding.class",
|
||
|
"**/android/databinding/*",
|
||
|
"**/androidx/databinding/*",
|
||
|
"**/BR.*",
|
||
|
// android
|
||
|
"**/R.class",
|
||
|
"**/R$*.class",
|
||
|
"**/BuildConfig.*",
|
||
|
"**/Manifest*.*",
|
||
|
"**/*Test*.*",
|
||
|
"android/**/*.*",
|
||
|
// kotlin
|
||
|
"**/*MapperImpl*.*",
|
||
|
"**/*\$ViewInjector*.*",
|
||
|
"**/*\$ViewBinder*.*",
|
||
|
"**/BuildConfig.*",
|
||
|
"**/*Component*.*",
|
||
|
"**/*BR*.*",
|
||
|
"**/Manifest*.*",
|
||
|
"**/*\$Lambda\$*.*",
|
||
|
"**/*Companion*.*",
|
||
|
"**/*Module*.*",
|
||
|
"**/*Dagger*.*",
|
||
|
"**/*Hilt*.*",
|
||
|
"**/*MembersInjector*.*",
|
||
|
"**/*_MembersInjector.class",
|
||
|
"**/*_Factory*.*",
|
||
|
"**/*_Provide*Factory*.*",
|
||
|
"**/*Extensions*.*",
|
||
|
// sealed and data classes
|
||
|
"**/*\$Result.*",
|
||
|
"**/*\$Result\$*.*",
|
||
|
// adapters generated by moshi
|
||
|
"**/*JsonAdapter.*"
|
||
|
)
|
||
|
|
||
|
fun Project.registerCodeCoverageTask(
|
||
|
testTaskName: String,
|
||
|
sourceName: String,
|
||
|
sourcePath: String,
|
||
|
flavorName: String,
|
||
|
buildTypeName: String
|
||
|
) {
|
||
|
tasks.register<JacocoReport>("${testTaskName}Coverage") {
|
||
|
dependsOn(testTaskName)
|
||
|
group = "Reporting"
|
||
|
description = "Generate Jacoco coverage reports on the ${sourceName.replaceFirstChar(Char::titlecase)} build."
|
||
|
|
||
|
val javaDirectories = fileTree(
|
||
|
"${project.buildDir}/intermediates/classes/${sourcePath}"
|
||
|
) { exclude(excludedFiles) }
|
||
|
|
||
|
val kotlinDirectories = fileTree(
|
||
|
"${project.buildDir}/tmp/kotlin-classes/${sourcePath}"
|
||
|
) { exclude(excludedFiles) }
|
||
|
|
||
|
val coverageSrcDirectories = listOf(
|
||
|
"src/main/java",
|
||
|
"src/main/kotlin",
|
||
|
"src/$flavorName/java",
|
||
|
"src/$flavorName/kotlin",
|
||
|
"src/$buildTypeName/java",
|
||
|
"src/$buildTypeName/kotlin"
|
||
|
)
|
||
|
|
||
|
classDirectories.setFrom(files(javaDirectories, kotlinDirectories))
|
||
|
additionalClassDirs.setFrom(files(coverageSrcDirectories))
|
||
|
sourceDirectories.setFrom(files(coverageSrcDirectories))
|
||
|
executionData.setFrom(
|
||
|
files("${project.buildDir}/jacoco/${testTaskName}.exec")
|
||
|
)
|
||
|
|
||
|
reports {
|
||
|
xml.required.set(true)
|
||
|
html.required.set(true)
|
||
|
}
|
||
|
|
||
|
doLast {
|
||
|
jacocoTestReport("${testTaskName}Coverage")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Suppress("UNCHECKED_CAST")
|
||
|
fun Project.jacocoTestReport(testTaskName: String) {
|
||
|
val reportsDirectory = jacoco.reportsDirectory.asFile.get()
|
||
|
val report = file("$reportsDirectory/${testTaskName}/${testTaskName}.xml")
|
||
|
|
||
|
logger.lifecycle("Checking coverage results: $report")
|
||
|
|
||
|
val metrics = report.extractTestsCoveredByType()
|
||
|
val limits = project.extra["limits"] as Map<String, Double>
|
||
|
|
||
|
val failures = metrics.filter { entry ->
|
||
|
entry.value < limits[entry.key]!!
|
||
|
}.map { entry ->
|
||
|
"- ${entry.key} coverage rate is: ${entry.value}%, minimum is ${limits[entry.key]}%"
|
||
|
}
|
||
|
|
||
|
if (failures.isNotEmpty()) {
|
||
|
logger.quiet("------------------ Code Coverage Failed -----------------------")
|
||
|
failures.forEach { logger.quiet(it) }
|
||
|
logger.quiet("---------------------------------------------------------------")
|
||
|
throw GradleException("Code coverage failed")
|
||
|
}
|
||
|
|
||
|
logger.quiet("------------------ Code Coverage Success -----------------------")
|
||
|
metrics.forEach { entry ->
|
||
|
logger.quiet("- ${entry.key} coverage rate is: ${entry.value}%")
|
||
|
}
|
||
|
logger.quiet("---------------------------------------------------------------")
|
||
|
}
|
||
|
|
||
|
@Suppress("UNCHECKED_CAST")
|
||
|
fun File.extractTestsCoveredByType(): Map<String, Double> {
|
||
|
val xmlReader = XmlSlurper().apply {
|
||
|
setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
|
||
|
setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
|
||
|
}
|
||
|
|
||
|
val counterNodes: List<NodeChild> = xmlReader
|
||
|
.parse(this).parent()
|
||
|
.children()
|
||
|
.filter {
|
||
|
(it as NodeChild).name() == "counter"
|
||
|
} as List<NodeChild>
|
||
|
|
||
|
return counterNodes.associate { nodeChild ->
|
||
|
val type = nodeChild.attributes()["type"].toString().lowercase(Locale.ENGLISH)
|
||
|
|
||
|
val covered = nodeChild.attributes()["covered"].toString().toDouble()
|
||
|
val missed = nodeChild.attributes()["missed"].toString().toDouble()
|
||
|
val percentage = ((covered / (covered + missed)) * 10000.0).roundToInt() / 100.0
|
||
|
|
||
|
Pair(type, percentage)
|
||
|
}
|
||
|
}
|