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 Nov 12, 2023
1 parent 12980d9 commit 6945d09
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 38 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ now promoted to dedicated Gradle plugins:
[io.github.gmazzo.test.aggregation.coverage](https://plugins.gradle.org/plugin/io.github.gmazzo.test.aggregation.coverage) and
[io.github.gmazzo.test.aggregation.results](https://plugins.gradle.org/plugin/io.github.gmazzo.test.aggregation.results)


## `UnitTest` and `AndroidTest` code coverage support
By default, the plugin will do `buildTypes["debug]".enableUnitTestCoverage = true` when applied, allowing coverage to be collected.

Coverage on `AndroidTest` variant is also support and will be automatically collected, if `buildType["debug"].enableAndroidTestCoverage = true` is also set (it applies for any `BuildType`)

## Filtering content
The plugins will automatically aggregate `android` modules and `java` modules that also apply `jacoco` plugin on the
`jacocoAggregation` and the `testReportAggregation` configurations.
Expand Down
8 changes: 6 additions & 2 deletions demo-project/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ android {

buildTypes {
debug {
enableUnitTestCoverage = true
// FIXME UI tests requires an emulator on CI
enableAndroidTestCoverage = true
}
release {
aggregateTestCoverage = false
Expand All @@ -39,7 +40,7 @@ android {
}
create("prod") {
dimension = "environment"
//aggregateTestCoverage.set(false)
aggregateTestCoverage.set(false)
}
}

Expand Down Expand Up @@ -71,4 +72,7 @@ dependencies {

testImplementation(libs.junit)
testImplementation(libs.robolectric)

androidTestImplementation(libs.androidx.test.junit)
androidTestImplementation(libs.androidx.test.espresso)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import org.junit.runner.RunWith
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
class SomeSingletonStuffUITest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.app.test", appContext.packageName)

SomeSingleton.doStuff() // to mark coverage of UI tests
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.myapplication

object SomeSingleton {

fun doStuff() {
println("stuff")
}

fun doMoreStuff() {
println("more stuff")
}

}
16 changes: 16 additions & 0 deletions demo-project/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.ManagedVirtualDevice

plugins {
base
id("io.github.gmazzo.test.aggregation.results")
Expand Down Expand Up @@ -31,3 +34,16 @@ tasks.jacocoAggregatedCoverageVerification {
tasks.check {
dependsOn(tasks.jacocoAggregatedCoverageVerification)
}

// setups emulator for all android projects
allprojects {
plugins.withId("com.android.base") {
val android = extensions.getByName<CommonExtension<*, *, *, *, *>>("android")

val pixel2 by android.testOptions.managedDevices.devices.creating(ManagedVirtualDevice::class) {
device = "Pixel 6"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
10 changes: 5 additions & 5 deletions demo-project/ui-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ android {
}
}

val pixel2 by android.testOptions.managedDevices.devices.creating(ManagedVirtualDevice::class) {
device = "Pixel 6"
apiLevel = 30
systemImageSource = "aosp-atd"
androidComponents.finalizeDsl {
// added this here, because the `baselineprofile` plugin resets it back to false for some reason
android.buildTypes["nonMinifiedRelease"].enableAndroidTestCoverage = true
}

baselineProfile {
useConnectedDevices = false
managedDevices += pixel2.name
managedDevices += "pixel2"
}

dependencies {
implementation(projects.demoProject.app)
implementation(libs.androidx.test.junit)
implementation(libs.androidx.test.espresso)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.myapplication

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class SomeSingletonMoreStuffUITest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.app", appContext.packageName)

SomeSingleton.doMoreStuff() // to mark coverage of UI tests
}
}

2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "1.9.20"
agp = "8.1.2"
agp = "8.3.0-alpha13"
android-minSDK = "21"
android-compileSDK = "34"
androidx-lifecycle = "2.6.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@file:Suppress("PrivateAPI", "ObjectPropertyName")

/**
* Helper to access AGP internal API as the required wiring to get coverage data is not public
*/

package io.github.gmazzo.android.test.aggregation

import com.android.build.api.artifact.Artifact
import com.android.build.api.artifact.Artifacts
import com.android.build.api.artifact.impl.ArtifactsImpl
import com.android.build.api.artifact.impl.InternalScopedArtifact
import com.android.build.api.artifact.impl.ScopedArtifactsImpl
import com.android.build.api.component.analytics.AnalyticsEnabledArtifacts
import com.android.build.api.component.analytics.AnalyticsEnabledScopedArtifacts
import com.android.build.api.variant.ScopedArtifacts
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import kotlin.reflect.full.functions
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.typeOf

internal fun <FILE_TYPE : FileSystemLocation> Artifacts.get(
type: Artifact.Single<FILE_TYPE>
): Provider<FILE_TYPE> = when (this) {
is ArtifactsImpl -> get(type)
is AnalyticsEnabledArtifacts -> delegate.get(type)
else -> error("Unknown `Artifacts` type: $this")
}

internal fun ScopedArtifacts.get(type: InternalScopedArtifact): ConfigurableFileCollection =
when (this) {
is ScopedArtifactsImpl -> _getScopedArtifactsContainer(type).finalScopedContent
is AnalyticsEnabledScopedArtifacts -> _delegate.get(type)
else -> error("Unknown `ScopedArtifacts` type: $this")
}

private fun ScopedArtifactsImpl._getScopedArtifactsContainer(type: InternalScopedArtifact) =
ScopedArtifactsImpl::class
.functions
.firstOrNull {
it.name == "getScopedArtifactsContainer" &&
it.parameters.size == 2 &&
it.parameters[0].type == typeOf<ScopedArtifactsImpl>() &&
it.parameters[1].type == typeOf<InternalScopedArtifact>()
}
?.apply { isAccessible = true }
?.call(this, type) as? ScopedArtifactsImpl.ScopedArtifactsContainer
?: error("Can't get `InternalScopedArtifact` type `$type`")

private val AnalyticsEnabledScopedArtifacts._delegate
get() = AnalyticsEnabledScopedArtifacts::class
.memberProperties
.firstOrNull { it.name == "delegate" }
?.apply { isAccessible = true }
?.get(this) as ScopedArtifacts?
?: error("Can't get `AnalyticsEnabledScopedArtifacts`'s delegate")
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
package io.github.gmazzo.android.test.aggregation

import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.HasUnitTest
import com.android.build.api.variant.ScopedArtifacts
import com.android.build.api.variant.TestVariant
import com.android.build.api.variant.Variant
import com.android.build.gradle.internal.scope.InternalArtifactType
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
Expand All @@ -31,7 +34,6 @@ import org.gradle.kotlin.dsl.namedDomainObjectSet
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.testing.jacoco.plugins.JacocoTaskExtension

abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
Expand All @@ -43,16 +45,41 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
// enables jacoco test coverage on `debug` build type by default
android.buildTypes["debug"].enableUnitTestCoverage = true

val jacocoVariants = objects.namedDomainObjectSet(Variant::class)
val aggregatedVariants = objects.namedDomainObjectSet(Variant::class)

androidComponents.onVariants { variant ->
val allVariantsExecDataForCoverageReport by tasks.registering(Sync::class) {
destinationDir = temporaryDir
}

androidComponents.onVariants(androidComponents.selector().all()) { variant ->
val buildType = android.buildTypes[variant.buildType!!]

if ((variant as? HasUnitTest)?.unitTest != null &&
buildType.enableUnitTestCoverage &&
android.shouldAggregate(variant)
) {
jacocoVariants.add(variant)
if (android.shouldAggregate(variant)) {
val unitTest = (variant as? HasUnitTest)?.unitTest
val androidTest = (variant as? HasAndroidTest)?.androidTest

if (buildType.enableUnitTestCoverage && unitTest != null) {
aggregatedVariants.add(variant)
allVariantsExecDataForCoverageReport.configure {
from(unitTest.artifacts.get(InternalArtifactType.UNIT_TEST_CODE_COVERAGE)) {
into("unitTest")
}
}
}
if (buildType.enableAndroidTestCoverage && androidTest != null) {
aggregatedVariants.add(variant)
allVariantsExecDataForCoverageReport.configure {
from(androidTest.artifacts.get(InternalArtifactType.CODE_COVERAGE)) {
into("androidTest")
}
}
}
if (buildType.enableAndroidTestCoverage && variant is TestVariant) {
aggregatedVariants.add(variant)
allVariantsExecDataForCoverageReport.configure {
from(variant.artifacts.get(InternalArtifactType.CODE_COVERAGE))
}
}
}
}

Expand All @@ -72,22 +99,15 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
objects.named(VerificationType.JACOCO_RESULTS)
)
}
afterEvaluate {
jacocoVariants.all variant@{
val execData = unitTestTaskOf(this@variant)!!
.map { it.the<JacocoTaskExtension>().destinationFile!! }

outgoing.artifact(execData) {
type = ArtifactTypeDefinition.BINARY_DATA_TYPE
}
}
outgoing.artifact(allVariantsExecDataForCoverageReport.map { it.destinationDir }) {
type = ArtifactTypeDefinition.BINARY_DATA_TYPE
}
}

val allVariantsSourcesForCoverageReport by tasks.registering(Sync::class) {
destinationDir = temporaryDir
duplicatesStrategy = DuplicatesStrategy.INCLUDE // in case of duplicated classes
jacocoVariants.all {
aggregatedVariants.all {
from(sources.java?.all, sources.kotlin?.all)
}
}
Expand Down Expand Up @@ -132,7 +152,7 @@ abstract class AndroidTestCoverageAggregationPlugin : Plugin<Project> {
)
}

jacocoVariants.all task@{
aggregatedVariants.all task@{
artifacts
.forScope(ScopedArtifacts.Scope.PROJECT)
.use(allVariantsClassesForCoverageReport)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.gmazzo.android.test.aggregation

import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.HasUnitTest
import org.gradle.api.Plugin
import org.gradle.api.Project
Expand Down Expand Up @@ -35,12 +36,14 @@ abstract class AndroidTestResultsAggregationPlugin : Plugin<Project> {
}

androidComponents.onVariants { variant ->
if ((variant as? HasUnitTest)?.unitTest != null && android.shouldAggregate(variant)) {
afterEvaluate {
val testTask = unitTestTaskOf(variant)!!
val unitTest = (variant as? HasUnitTest)?.unitTest
val androidTest = (variant as? HasAndroidTest)?.androidTest

testResultsElements.outgoing.artifact(testTask.flatMap { it.binaryResultsDirectory })
}
if (unitTest != null && android.shouldAggregate(variant)) {
testResultsElements.outgoing.artifact(unitTestTaskOf(unitTest).flatMap { it.binaryResultsDirectory })
}
if (androidTest != null && android.shouldAggregate(variant)) {
// TODO testResultsElements.outgoing.artifact(androidTestTaskOf(androidTest).flatMap { it.binaryResultsDirectory })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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.ComponentIdentity
import com.android.build.api.variant.GeneratesTestApk
import com.android.build.api.variant.UnitTest
import com.android.build.api.variant.Variant
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.coverage.JacocoReportTask
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.tasks.testing.AbstractTestTask
Expand All @@ -13,7 +16,6 @@ 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 Down Expand Up @@ -60,6 +62,10 @@ internal fun TestAggregationExtension.aggregateProject(project: Project, config:
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()}") }
internal fun Project.unitTestTaskOf(variant: UnitTest) = provider {
tasks.getByName<AbstractTestTask>("test${variant.name.capitalized()}")
}

internal fun <Type> Project.androidTestTaskOf(variant: Type) where Type : GeneratesTestApk, Type: ComponentIdentity = provider {
tasks.getByName<JacocoReportTask>("create${variant.name.capitalized()}CoverageReport")
}

0 comments on commit 6945d09

Please sign in to comment.