From 7df194a72f72687ec6eae89d4faba98057a6bf3b Mon Sep 17 00:00:00 2001 From: Guillermo Mazzola Date: Wed, 18 Oct 2023 10:11:58 +0200 Subject: [PATCH] Added `aggregateTestCoverage` support on `testAggregatedReport` --- README.md | 3 + demo-project/login/build.gradle.kts | 2 + .../AndroidTestBaseAggregationPlugin.kt | 42 ++++++++++++ .../AndroidTestCoverageAggregationPlugin.kt | 68 +++++-------------- .../AndroidTestResultsAggregationPlugin.kt | 19 +++--- .../android/test/aggregation/InternalDSL.kt | 30 ++++++++ 6 files changed, 104 insertions(+), 60 deletions(-) create mode 100644 plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestBaseAggregationPlugin.kt diff --git a/README.md b/README.md index 906ef4e..fbaccea 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,6 @@ You may choose which flavors participates in the aggregated report by doing: } ``` where it effectively only run `:app:testStageDebugUnitTest` + +> [!NOTE] +> The `aggregateTestCoverage` DSL applies for both `:jacocoAggregatedReport` and `:testAggregatedReport` tasks diff --git a/demo-project/login/build.gradle.kts b/demo-project/login/build.gradle.kts index 618aa35..0868828 100644 --- a/demo-project/login/build.gradle.kts +++ b/demo-project/login/build.gradle.kts @@ -21,6 +21,8 @@ android { enableUnitTestCoverage = true } release { + aggregateTestCoverage = false + isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), diff --git a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestBaseAggregationPlugin.kt b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestBaseAggregationPlugin.kt new file mode 100644 index 0000000..81d62a7 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestBaseAggregationPlugin.kt @@ -0,0 +1,42 @@ +@file:Suppress("UnstableApiUsage") + +package io.github.gmazzo.android.test.aggregation + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.aggregateTestCoverage +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.property +import org.gradle.kotlin.dsl.typeOf + +internal abstract class AndroidTestBaseAggregationPlugin : Plugin { + + internal abstract val extendedProperties: ListProperty> + + override fun apply(target: Project): Unit = with(target) { + apply(plugin = "com.android.base") + + android.buildTypes.configureEach { + extensions.add( + typeOf>(), + ::aggregateTestCoverage.name, + objects.property().also(extendedProperties::add) + ) + } + android.productFlavors.configureEach { + extensions.add( + typeOf>(), + ::aggregateTestCoverage.name, + objects.property().also(extendedProperties::add) + ) + } + + androidComponents.finalizeDsl { + extendedProperties.finalizeValue() + extendedProperties.get().forEach { it.finalizeValue() } + } + } + +} diff --git a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestCoverageAggregationPlugin.kt b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestCoverageAggregationPlugin.kt index af8fe35..3d2dcab 100644 --- a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestCoverageAggregationPlugin.kt +++ b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestCoverageAggregationPlugin.kt @@ -3,6 +3,7 @@ package io.github.gmazzo.android.test.aggregation import com.android.build.api.artifact.ScopedArtifact +import com.android.build.api.variant.HasUnitTest import com.android.build.api.variant.ScopedArtifacts import com.android.build.api.variant.Variant import org.gradle.api.Plugin @@ -17,14 +18,12 @@ import org.gradle.api.file.Directory import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.file.RegularFile import org.gradle.api.plugins.ExtensionAware -import org.gradle.api.provider.Property import org.gradle.api.tasks.Sync -import org.gradle.configurationcache.extensions.capitalized import org.gradle.kotlin.dsl.USAGE_TEST_AGGREGATION -import org.gradle.kotlin.dsl.aggregateTestCoverage import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getAt import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.listProperty import org.gradle.kotlin.dsl.named @@ -33,60 +32,27 @@ import org.gradle.kotlin.dsl.property import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.registering import org.gradle.kotlin.dsl.the -import org.gradle.kotlin.dsl.typeOf import org.gradle.testing.jacoco.plugins.JacocoTaskExtension abstract class AndroidTestCoverageAggregationPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { - apply(plugin = "com.android.base") + apply() addRobolectricTestsSupport() // enables jacoco test coverage on `debug` build type by default android.buildTypes["debug"].enableUnitTestCoverage = true - android.buildTypes.configureEach { - extensions.add( - typeOf>(), - ::aggregateTestCoverage.name, - objects.property() - ) - } - android.productFlavors.configureEach { - extensions.add( - typeOf>(), - ::aggregateTestCoverage.name, - objects.property() - ) - } - val jacocoVariants = objects.namedDomainObjectSet(Variant::class) androidComponents.onVariants { variant -> val buildType = android.buildTypes[variant.buildType!!] - if (variant.unitTest != null && buildType.enableUnitTestCoverage) { - afterEvaluate { - /** - * `aggregateTestCoverage` applies to `BuildType`s and `Flavor`s and - * can take 3 possible values: `true`, `false` or `null` (missing). - * - * Because of this, we may found conflicting declarations where a - * `BuildType` is set to `true` but a `Flavor` to `false`. - * The following logic is no honor the precedence order: - * - If any component of the variant (buildType/flavor) says `true`, then `true` - * - If any component of the variant says `false` (and other says nothing `null`), then `false` - * - If no component says anything (`null`), then `true` (because its `BuildType` has `enableUnitTestCoverage = true`) - */ - val aggregateSources = sequenceOf(buildType.aggregateTestCoverage) + - variant.productFlavors.asSequence() - .map { (_, flavor) -> android.productFlavors[flavor] } - .map { it.aggregateTestCoverage } - - if (aggregateSources.shouldAggregate) { - jacocoVariants.add(variant) - } - } + if ((variant as? HasUnitTest)?.unitTest != null && + buildType.enableUnitTestCoverage && + android.shouldAggregate(variant) + ) { + jacocoVariants.add(variant) } } @@ -106,13 +72,14 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin { objects.named(VerificationType.JACOCO_RESULTS) ) } - jacocoVariants.all variant@{ - val execData = tasks - .named("test${this@variant.unitTest!!.name.capitalized()}") - .map { it.the().destinationFile!! } + afterEvaluate { + jacocoVariants.all variant@{ + val execData = unitTestTaskOf(this@variant)!! + .map { it.the().destinationFile!! } - outgoing.artifact(execData) { - type = ArtifactTypeDefinition.BINARY_DATA_TYPE + outgoing.artifact(execData) { + type = ArtifactTypeDefinition.BINARY_DATA_TYPE + } } } } @@ -194,6 +161,7 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin { val robolectricSupport = objects.property() .convention(true) .apply { finalizeValueOnRead() } + .also(plugins.getAt(AndroidTestBaseAggregationPlugin::class).extendedProperties::add) (android as ExtensionAware).extensions .add("coverageRobolectricSupport", robolectricSupport) @@ -212,8 +180,4 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin { } } - private val Sequence>.shouldAggregate - get() = mapNotNull { it.orNull } - .reduceOrNull { acc, aggregate -> acc || aggregate } != false - } diff --git a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestResultsAggregationPlugin.kt b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestResultsAggregationPlugin.kt index 30352bc..60401bd 100644 --- a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestResultsAggregationPlugin.kt +++ b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/AndroidTestResultsAggregationPlugin.kt @@ -1,14 +1,12 @@ package io.github.gmazzo.android.test.aggregation -import com.android.build.gradle.TestedExtension +import com.android.build.api.variant.HasUnitTest import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.attributes.Category import org.gradle.api.attributes.TestSuiteType import org.gradle.api.attributes.Usage import org.gradle.api.attributes.VerificationType -import org.gradle.api.tasks.testing.Test -import org.gradle.configurationcache.extensions.capitalized import org.gradle.kotlin.dsl.USAGE_TEST_AGGREGATION import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.named @@ -16,9 +14,9 @@ import org.gradle.kotlin.dsl.named abstract class AndroidTestResultsAggregationPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { - apply(plugin = "com.android.base") + apply() - configurations.create("testResultsElements") { + val testResultsElements = configurations.create("testResultsElements") { isCanBeConsumed = true isCanBeResolved = false isVisible = false @@ -34,10 +32,15 @@ abstract class AndroidTestResultsAggregationPlugin : Plugin { objects.named(VerificationType.TEST_RESULTS) ) } - (android as? TestedExtension)?.unitTestVariants?.all { - val testTask = tasks.named("test${name.capitalized()}") + } + + androidComponents.onVariants { variant -> + if ((variant as? HasUnitTest)?.unitTest != null && android.shouldAggregate(variant)) { + afterEvaluate { + val testTask = unitTestTaskOf(variant)!! - outgoing.artifact(testTask.flatMap { it.binaryResultsDirectory }) + testResultsElements.outgoing.artifact(testTask.flatMap { it.binaryResultsDirectory }) + } } } } diff --git a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/InternalDSL.kt b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/InternalDSL.kt index 2fec740..bd30d71 100644 --- a/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/InternalDSL.kt +++ b/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/InternalDSL.kt @@ -1,12 +1,19 @@ package io.github.gmazzo.android.test.aggregation import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.HasUnitTest +import com.android.build.api.variant.Variant import com.android.build.gradle.BaseExtension import org.gradle.api.Project import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.testing.AbstractTestTask +import org.gradle.configurationcache.extensions.capitalized +import org.gradle.kotlin.dsl.aggregateTestCoverage import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.getByName +import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.testAggregation import org.gradle.kotlin.dsl.the @@ -27,9 +34,32 @@ internal fun Project.ensureItsNotJava() = plugins.withId("java-base") { error("This plugin can not work with `java` plugin as well. It's recommended to apply it at the root project with at most the `base` plugin") } +/** + * `aggregateTestCoverage` applies to `BuildType`s and `Flavor`s and + * can take 3 possible values: `true`, `false` or `null` (missing). + * + * Because of this, we may found conflicting declarations where a + * `BuildType` is set to `true` but a `Flavor` to `false`. + * The following logic is no honor the precedence order: + * - If any component of the variant (buildType/flavor) says `true`, then `true` + * - If any component of the variant says `false` (and other says nothing `null`), then `false` + * - If no component says anything (`null`), then `true` (because its `BuildType` has `enableUnitTestCoverage = true`) + */ +internal fun BaseExtension.shouldAggregate(variant: Variant) = + (sequenceOf(buildTypes[variant.buildType!!].aggregateTestCoverage) + + variant.productFlavors.asSequence() + .map { (_, flavor) -> productFlavors[flavor] } + .map { it.aggregateTestCoverage }) + .mapNotNull { it.orNull } + .reduceOrNull { acc, aggregate -> acc || aggregate } != false + internal fun TestAggregationExtension.aggregateProject(project: Project, config: Configuration) = modules.includes(project) && config.dependencies.add(project.dependencies.testAggregation(project)) private fun TestAggregationExtension.Modules.includes(project: Project) = (includes.get().isEmpty() || project in includes.get()) && project !in excludes.get() + +internal fun Project.unitTestTaskOf(variant: Variant) = (variant as? HasUnitTest) + ?.unitTest + ?.let { tasks.named("test${it.name.capitalized()}") }