From d05a0566980819c02863f837c295d75bcaa45c05 Mon Sep 17 00:00:00 2001 From: Oscar Mira Date: Thu, 30 Jan 2025 13:53:35 +0100 Subject: [PATCH] build: add plugin for code coverage reporting --- build-logic/plugins/build.gradle.kts | 28 ++++++ .../main/kotlin/AndroidLibraryJacocoPlugin.kt | 14 +++ build-logic/plugins/src/main/kotlin/Jacoco.kt | 92 +++++++++++++++++++ build-logic/settings.gradle.kts | 28 ++++++ build.gradle.kts | 2 +- gradle/libs.versions.toml | 5 + lib/android/build.gradle.kts | 6 ++ settings.gradle.kts | 21 ++++- 8 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 build-logic/plugins/build.gradle.kts create mode 100644 build-logic/plugins/src/main/kotlin/AndroidLibraryJacocoPlugin.kt create mode 100644 build-logic/plugins/src/main/kotlin/Jacoco.kt create mode 100644 build-logic/settings.gradle.kts diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts new file mode 100644 index 0000000..d14f0b6 --- /dev/null +++ b/build-logic/plugins/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `kotlin-dsl` +} + +kotlin { + jvmToolchain(21) +} + +dependencies { + compileOnly(libs.android.gradle.plugin) + compileOnly(libs.kotlin.gradle.plugin) +} + +tasks { + validatePlugins { + enableStricterValidation = true + failOnWarning = true + } +} + +gradlePlugin { + plugins { + register("androidLibraryJacoco") { + id = libs.plugins.sdk.android.library.jacoco.get().pluginId + implementationClass = "AndroidLibraryJacocoPlugin" + } + } +} diff --git a/build-logic/plugins/src/main/kotlin/AndroidLibraryJacocoPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidLibraryJacocoPlugin.kt new file mode 100644 index 0000000..395661c --- /dev/null +++ b/build-logic/plugins/src/main/kotlin/AndroidLibraryJacocoPlugin.kt @@ -0,0 +1,14 @@ +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.getByType + +class AndroidLibraryJacocoPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + apply(plugin = "jacoco") + configureJacoco(extensions.getByType()) + } + } +} diff --git a/build-logic/plugins/src/main/kotlin/Jacoco.kt b/build-logic/plugins/src/main/kotlin/Jacoco.kt new file mode 100644 index 0000000..9c66a16 --- /dev/null +++ b/build-logic/plugins/src/main/kotlin/Jacoco.kt @@ -0,0 +1,92 @@ +import com.android.build.api.artifact.ScopedArtifact +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.ScopedArtifacts +import com.android.build.api.variant.SourceDirectories +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.assign +import org.gradle.kotlin.dsl.register +import org.gradle.testing.jacoco.tasks.JacocoReport +import java.util.Locale + +private val coverageExclusions = listOf( + // Android + "**/R.class", + "**/R\$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", +) + +private fun String.capitalize() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() +} + +/** + * Creates a new task that generates a combined coverage report with data from local and + * instrumented tests. + * + * `create{variant}CombinedCoverageReport` + * + * Note that coverage data must exist before running the task. This allows us to run device + * tests on CI using a different Github Action or an external device farm. + */ +internal fun Project.configureJacoco( + androidComponentsExtension: AndroidComponentsExtension<*, *, *>, +) { + androidComponentsExtension.onVariants { variant -> + val myObjFactory = project.objects + val buildDir = layout.buildDirectory.get().asFile + val allJars: ListProperty = myObjFactory.listProperty(RegularFile::class.java) + val allDirectories: ListProperty = + myObjFactory.listProperty(Directory::class.java) + val reportTask = + tasks.register( + "create${variant.name.capitalize()}CombinedCoverageReport", + JacocoReport::class, + ) { + + classDirectories.setFrom( + allJars, + allDirectories.map { dirs -> + dirs.map { dir -> + myObjFactory.fileTree().setDir(dir).exclude(coverageExclusions) + } + }, + ) + reports { + html.required = true + xml.required = true + } + + fun SourceDirectories.Flat?.toFilePaths(): Provider> = this + ?.all + ?.map { directories -> directories.map { it.asFile.path } } + ?: provider { emptyList() } + sourceDirectories.setFrom( + files( + variant.sources.java.toFilePaths(), + variant.sources.kotlin.toFilePaths() + ), + ) + + executionData.setFrom( + project.fileTree("$buildDir/outputs/unit_test_code_coverage/${variant.name}UnitTest") + .matching { include("**/*.exec") }, + + project.fileTree("$buildDir/outputs/code_coverage/${variant.name}AndroidTest") + .matching { include("**/*.ec") }, + ) + } + + variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT) + .use(reportTask) + .toGet( + ScopedArtifact.CLASSES, + { _ -> allJars }, + { _ -> allDirectories }, + ) + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..9ee0b57 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + } +} + +dependencyResolutionManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android(\\..*)?") + includeGroupByRegex("com\\.google(\\..*)?") + includeGroupByRegex("androidx?(\\..*)?") + } + } + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +include(":plugins") + +rootProject.name = "build-logic" diff --git a/build.gradle.kts b/build.gradle.kts index af906e8..d2f8450 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } allprojects { - group = "im.molly" + group = "im.molly.monero.sdk" } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7084f89..2c3ea9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ okhttp = "4.10.0" room = "2.6.1" [libraries] +android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } @@ -27,6 +28,7 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " androidx-ui = { module = "androidx.compose.ui:ui" } androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines"} kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } okhttp = { module = "com.squareup.okhttp3:okhttp" } @@ -40,3 +42,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } + +# Plugins defined by this project +sdk-android-library-jacoco = { id ="sdk.android.library.jacoco" } diff --git a/lib/android/build.gradle.kts b/lib/android/build.gradle.kts index 03a1eda..31b3678 100644 --- a/lib/android/build.gradle.kts +++ b/lib/android/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.sdk.android.library.jacoco) } kotlin { @@ -38,6 +39,11 @@ android { } buildTypes { + getByName("debug") { + enableUnitTestCoverage = true + enableAndroidTestCoverage = true + } + getByName("release") { isMinifyEnabled = false diff --git a/settings.gradle.kts b/settings.gradle.kts index 895419f..ec9788f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,24 +1,39 @@ pluginManagement { + includeBuild("build-logic") repositories { gradlePluginPortal() google() - mavenCentral() } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositories { - google() + google { + content { + includeGroupByRegex("com\\.android(\\..*)?") + includeGroupByRegex("com\\.google(\\..*)?") + includeGroupByRegex("androidx?(\\..*)?") + } + } mavenCentral() } versionCatalogs { + // "libs" is predefined by Gradle create("testLibs") { from(files("gradle/test-libs.versions.toml")) } } } +check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { + """ + This project requires JDK 21+ but it is currently using JDK ${JavaVersion.current()}. + Java Home: [${System.getProperty("java.home")}] + https://developer.android.com/build/jdks#jdk-config-in-studio + """.trimIndent() +} + includeProject("lib", "lib/android") includeProject("demo", "demo/android")