Skip to content

Commit

Permalink
migrate to global kts plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
MilosKozak committed Oct 20, 2023
1 parent 741f3a9 commit 9bd4a06
Show file tree
Hide file tree
Showing 46 changed files with 458 additions and 189 deletions.
3 changes: 1 addition & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ plugins {
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("android-app-dependencies")
id("jacoco-app-dependencies")
}

apply(from = "${project.rootDir}/core/main/jacoco_global.gradle")

repositories {
mavenCentral()
google()
Expand Down
3 changes: 0 additions & 3 deletions buildSrc/src/main/kotlin/Libs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ object Libs {

const val kotlin = "1.9.10"

const val platformBom = "org.jetbrains.kotlin:kotlin-bom"
const val stdlibJdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin"
const val test = "org.jetbrains.kotlin:kotlin-test:$kotlin"
const val testJunit5 = "org.jetbrains.kotlin:kotlin-test-junit5"
const val reflect = "org.jetbrains.kotlin:kotlin-reflect:$kotlin"
}

Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ object Versions {
const val wearTargetSdk = 29

val javaVersion = JavaVersion.VERSION_11
const val jacoco = "0.8.11"
}
207 changes: 207 additions & 0 deletions buildSrc/src/main/kotlin/jacoco-app-dependencies.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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.application")
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)
}
}
Loading

0 comments on commit 9bd4a06

Please sign in to comment.