Skip to content

Commit

Permalink
Added aggregateTestCoverage support on testAggregatedReport
Browse files Browse the repository at this point in the history
  • Loading branch information
gmazzo committed Oct 18, 2023
1 parent dfd59b7 commit 7df194a
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 60 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions demo-project/login/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ android {
enableUnitTestCoverage = true
}
release {
aggregateTestCoverage = false

isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Project> {

internal abstract val extendedProperties: ListProperty<Property<*>>

override fun apply(target: Project): Unit = with(target) {
apply(plugin = "com.android.base")

android.buildTypes.configureEach {
extensions.add(
typeOf<Property<Boolean>>(),
::aggregateTestCoverage.name,
objects.property<Boolean>().also(extendedProperties::add)
)
}
android.productFlavors.configureEach {
extensions.add(
typeOf<Property<Boolean>>(),
::aggregateTestCoverage.name,
objects.property<Boolean>().also(extendedProperties::add)
)
}

androidComponents.finalizeDsl {
extendedProperties.finalizeValue()
extendedProperties.get().forEach { it.finalizeValue() }
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<Project> {

override fun apply(target: Project): Unit = with(target) {
apply(plugin = "com.android.base")
apply<AndroidTestBaseAggregationPlugin>()
addRobolectricTestsSupport()

// enables jacoco test coverage on `debug` build type by default
android.buildTypes["debug"].enableUnitTestCoverage = true

android.buildTypes.configureEach {
extensions.add(
typeOf<Property<Boolean>>(),
::aggregateTestCoverage.name,
objects.property()
)
}
android.productFlavors.configureEach {
extensions.add(
typeOf<Property<Boolean>>(),
::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)
}
}

Expand All @@ -106,13 +72,14 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
objects.named(VerificationType.JACOCO_RESULTS)
)
}
jacocoVariants.all variant@{
val execData = tasks
.named("test${this@variant.unitTest!!.name.capitalized()}")
.map { it.the<JacocoTaskExtension>().destinationFile!! }
afterEvaluate {
jacocoVariants.all variant@{
val execData = unitTestTaskOf(this@variant)!!
.map { it.the<JacocoTaskExtension>().destinationFile!! }

outgoing.artifact(execData) {
type = ArtifactTypeDefinition.BINARY_DATA_TYPE
outgoing.artifact(execData) {
type = ArtifactTypeDefinition.BINARY_DATA_TYPE
}
}
}
}
Expand Down Expand Up @@ -194,6 +161,7 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
val robolectricSupport = objects.property<Boolean>()
.convention(true)
.apply { finalizeValueOnRead() }
.also(plugins.getAt(AndroidTestBaseAggregationPlugin::class).extendedProperties::add)

(android as ExtensionAware).extensions
.add("coverageRobolectricSupport", robolectricSupport)
Expand All @@ -212,8 +180,4 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
}
}

private val Sequence<Property<Boolean>>.shouldAggregate
get() = mapNotNull { it.orNull }
.reduceOrNull { acc, aggregate -> acc || aggregate } != false

}
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
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

abstract class AndroidTestResultsAggregationPlugin : Plugin<Project> {

override fun apply(target: Project): Unit = with(target) {
apply(plugin = "com.android.base")
apply<AndroidTestBaseAggregationPlugin>()

configurations.create("testResultsElements") {
val testResultsElements = configurations.create("testResultsElements") {
isCanBeConsumed = true
isCanBeResolved = false
isVisible = false
Expand All @@ -34,10 +32,15 @@ abstract class AndroidTestResultsAggregationPlugin : Plugin<Project> {
objects.named(VerificationType.TEST_RESULTS)
)
}
(android as? TestedExtension)?.unitTestVariants?.all {
val testTask = tasks.named<Test>("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 })
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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<AbstractTestTask>("test${it.name.capitalized()}") }

0 comments on commit 7df194a

Please sign in to comment.