Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build-logic/src/main/kotlin/Environment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ enum class SupportedAgp(
AGP_8_9("8.9.3", gradle = "8.11.1"),
AGP_8_10("8.10.1", gradle = "8.11.1"),
AGP_8_11("8.11.1", gradle = "8.13"),
AGP_8_12("8.12.1", gradle = "8.13"),
AGP_8_13("8.13.0-rc01", gradle = "8.13"),
AGP_9_0("9.0.0-alpha02", gradle = "9.0.0"),
AGP_8_12("8.12.2", gradle = "8.13"),
AGP_8_13("8.13.0", gradle = "8.13"),
AGP_9_0("9.0.0-alpha04", gradle = "9.0.0"),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.BasePlugin
import com.android.build.gradle.DynamicFeaturePlugin
import com.android.build.gradle.LibraryExtension
Expand All @@ -16,13 +15,12 @@ import com.android.build.gradle.api.BaseVariant
import de.mannodermaus.gradle.plugins.junit5.internal.providers.DirectoryProvider
import de.mannodermaus.gradle.plugins.junit5.internal.providers.JavaDirectoryProvider
import de.mannodermaus.gradle.plugins.junit5.internal.providers.KotlinDirectoryProvider
import org.gradle.api.DomainObjectSet
import org.gradle.api.Project

internal class PluginConfig
private constructor(
private val project: Project,
private val legacyVariants: DomainObjectSet<out BaseVariant>,
private val legacyPlugin: BasePlugin,
private val componentsExtension: AndroidComponentsExtension<*, *, *>
) {

Expand All @@ -32,18 +30,7 @@ private constructor(
.findByName("androidComponents") as? AndroidComponentsExtension<*, *, *>
?: return null

val legacyExtension = project.extensions
.findByName("android") as? BaseExtension
?: return null

val legacyVariants = when (plugin) {
is AppPlugin -> (legacyExtension as AppExtension).applicationVariants
is LibraryPlugin -> (legacyExtension as LibraryExtension).libraryVariants
is DynamicFeaturePlugin -> (legacyExtension as AppExtension).applicationVariants
else -> return null
}

return PluginConfig(project, legacyVariants, componentsExtension)
return PluginConfig(project, plugin, componentsExtension)
}
}

Expand All @@ -63,9 +50,26 @@ private constructor(
fun directoryProvidersOf(variant: Variant): Set<DirectoryProvider> {
// Locate the legacy variant for the given one, since the new API
// does not give access to variant-specific source sets and class outputs
return legacyVariants.firstOrNull { it.name == variant.name }
?.run { directoryProvidersOf(this) }
?: emptySet()
val legacyExtension = project.extensions.findByName("android")

val legacyVariants = try {
when (legacyPlugin) {
is AppPlugin -> (legacyExtension as AppExtension).applicationVariants
is LibraryPlugin -> (legacyExtension as LibraryExtension).libraryVariants
is DynamicFeaturePlugin -> (legacyExtension as AppExtension).applicationVariants
else -> null
}
} catch (_: ClassCastException) {
// AGP 9 removes access to the legacy API and thus, Jacoco integration
// is deprecated henceforth. When the above block yields a ClassCastException,
// we know that we're using exclusively against the new DSL and return an empty set to the caller
null
}

return legacyVariants
?.firstOrNull { it.name == variant.name }
?.let(::directoryProvidersOf)
.orEmpty()
}

/* Private */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import de.mannodermaus.gradle.plugins.junit5.dsl.AndroidJUnitPlatformExtension
import de.mannodermaus.gradle.plugins.junit5.internal.config.ANDROID_JUNIT5_RUNNER_BUILDER_CLASS
import de.mannodermaus.gradle.plugins.junit5.internal.config.JUnit5TaskConfig
import de.mannodermaus.gradle.plugins.junit5.internal.config.PluginConfig
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.getAsList
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.getTaskName
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.instrumentationTestVariant
Expand Down Expand Up @@ -97,16 +96,7 @@ private fun AndroidJUnitPlatformExtension.prepareUnitTests(project: Project, and
// so that consumers don't need to do this explicitly
val options = excludedPackagingOptions()

try {
android.packaging.resources.excludes.addAll(options)
} catch (e: NoSuchMethodError) {
// TODO Because of https://issuetracker.google.com/issues/263387063,
// there is a breaking API change in AGP 8.x that causes a NoSuchMethodError
// (renaming PackagingOptions to Packaging without any fallback).
// Fall back to the old DSL when this happens
options.forEach(project.android.packagingOptions::exclude)
}

android.packaging.resources.excludes.addAll(options)
attachDependencies(project, "testImplementation", includeRunner = false)
}

Expand Down Expand Up @@ -206,10 +196,48 @@ private fun AndroidJUnitPlatformExtension.configureJacoco(
// Create a Jacoco friend task
val enabledVariants = jacocoOptions.onlyGenerateTasksForVariants.get()
if (enabledVariants.isEmpty() || variant.name in enabledVariants) {
// Capture an empty return value here and highlight
// the unavailability of Jacoco integration on certain AGP versions
// (namely, AGP 9.0.0+ with the new DSL). This feature is effectively deprecated
val directoryProviders = config.directoryProvidersOf(variant)
val registered = AndroidJUnit5JacocoReport.register(project, variant, testTask, directoryProviders)
if (!registered) {
project.logger.junit5Warn("Jacoco task for variant '${variant.name}' already exists. Disabling customization for JUnit 5...")
val registeredTask = AndroidJUnit5JacocoReport.register(
project = project,
variant = variant,
testTask = testTask,
directoryProviders = directoryProviders
)

if (directoryProviders.isNotEmpty()) {
// Log a warning if Jacoco tasks already existed
if (registeredTask == null) {
project.logger.junit5Warn(
"Jacoco task for variant '${variant.name}' already exists." +
"Disabling customization for JUnit 5..."
)
}
} else {
// Disable any task that may have been registered above
registeredTask?.configure { it.enabled = false }

project.logger.junit5Warn(
buildString {
append(
"Cannot configure Jacoco for this project because directory providers cannot be found."
)

if (config.currentAgpVersion.major >= 9) {
append(
" This integration is deprecated from AGP 9.0.0 onwards because of the new DSL."
)
append(
" Please consult the link below for more information: "
)
append(
"https://developer.android.com/build/releases/agp-preview"
)
}
}
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
package de.mannodermaus.gradle.plugins.junit5.internal.extensions

import com.android.build.gradle.BaseExtension
import com.android.build.gradle.BasePlugin
import de.mannodermaus.gradle.plugins.junit5.dsl.AndroidJUnitPlatformExtension
import de.mannodermaus.gradle.plugins.junit5.internal.config.EXTENSION_NAME
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

internal val Project.junitPlatform
get() = extensionByName<AndroidJUnitPlatformExtension>(EXTENSION_NAME)

internal val Project.android
get() = extensionByName<BaseExtension>("android")

@OptIn(ExperimentalContracts::class)
internal fun Project.whenAndroidPluginAdded(block: (BasePlugin) -> Unit) {
val configured = AtomicBoolean(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ public abstract class AndroidJUnit5JacocoReport : JacocoReport() {
variant: Variant,
testTask: Test,
directoryProviders: Collection<DirectoryProvider>
): Boolean {
): TaskProvider<AndroidJUnit5JacocoReport>? {
val configAction = ConfigAction(project, variant, testTask, directoryProviders)
if (project.tasks.namedOrNull<Task>(configAction.name) != null) {
project.tasks.namedOrNull<Task>(configAction.name)?.let {
// Already exists; abort
return false
return null
}

val provider = project.tasks.register(
Expand All @@ -57,7 +57,7 @@ public abstract class AndroidJUnit5JacocoReport : JacocoReport() {
provider.dependsOn(testTask.name)
findOrRegisterDefaultJacocoTask(project).dependsOn(provider)

return true
return provider
}

private fun findOrRegisterDefaultJacocoTask(project: Project): TaskProvider<Task> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,18 @@ class FunctionalTests {
.withProjectDir(project)
.build()

// Print Gradle logs from the embedded invocation
result.prettyPrint()

// Check that the task execution was successful in general
when (val outcome = result.task(":$taskName")?.outcome) {
TaskOutcome.UP_TO_DATE -> {
val outcome = result.task(":$taskName")?.outcome
when {
outcome == TaskOutcome.UP_TO_DATE -> {
// Nothing to do, a previous build already checked this
println("Test task up-to-date; skipping assertions.")
println("Task '$taskName' up-to-date; skipping assertions.")
}

TaskOutcome.SUCCESS -> {
outcome == TaskOutcome.SUCCESS -> {
// Based on the spec's configuration in the test project,
// assert that all test classes have been executed as expected
for (expectation in spec.expectedTests) {
Expand All @@ -109,11 +113,14 @@ class FunctionalTests {
}
}

outcome == TaskOutcome.SKIPPED && spec.allowSkipped -> {
// It might be acceptable to allow "skipped" as the result depending on the test spec
println("Task '$taskName' was skipped.")
}

else -> {
// Unexpected result; fail
fail {
"Unexpected task outcome: $outcome\n\nRaw output:\n\n${result.output}"
}
fail { "Unexpected task outcome: $outcome\n\nRaw output:\n\n${result.output}" }
}
}
}
Expand Down Expand Up @@ -164,8 +171,6 @@ class FunctionalTests {
productFlavor: String? = null,
tests: List<String>
) {
this.prettyPrint()

// Construct task name from given build type and/or product flavor
// Examples:
// - buildType="debug", productFlavor=null --> ":testDebugUnitTest"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.mannodermaus.gradle.plugins.junit5.extensions

import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.extensionByName
import org.gradle.api.Project

internal val Project.android
get() = extensionByName<CommonExtension<*, *, *, *, *>>("android")

internal val Project.androidApp
get() = extensionByName<ApplicationExtension>("android")
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package de.mannodermaus.gradle.plugins.junit5.plugin

import com.google.common.truth.Truth.assertThat
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android
import de.mannodermaus.gradle.plugins.junit5.extensions.android
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.capitalized
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junitPlatform
import de.mannodermaus.gradle.plugins.junit5.util.evaluate
import de.mannodermaus.gradle.plugins.junit5.util.get
Expand Down Expand Up @@ -35,7 +36,7 @@ interface AgpFilterTests : AgpVariantAwareTests {
}
}
) { project, buildType ->
val task = project.tasks.get<Test>("test${buildType.capitalize()}UnitTest")
val task = project.tasks.get<Test>("test${buildType.capitalized()}UnitTest")
assertThat(task.junitPlatformOptions.includeTags).contains("global-include-tag")
assertThat(task.junitPlatformOptions.excludeTags).contains("global-exclude-tag")
assertThat(task.junitPlatformOptions.includeEngines).contains("global-include-engine")
Expand All @@ -48,8 +49,8 @@ interface AgpFilterTests : AgpVariantAwareTests {
fun `using custom build types & multiple flavor dimensions`(): List<DynamicTest> {
val project = createProject().build()
project.registerProductFlavors(advancedFlavorList)
project.android.buildTypes { container ->
container.create("ci").initWith(container.getByName("debug"))
with(project.android.buildTypes) {
create("ci").initWith(getByName("debug"))
}
project.evaluate()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package de.mannodermaus.gradle.plugins.junit5.plugin

import com.android.build.gradle.TestedExtension
import com.google.common.truth.Truth.assertThat
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android
import de.mannodermaus.gradle.plugins.junit5.extensions.android
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junitPlatform
import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5WriteFilters
import de.mannodermaus.gradle.plugins.junit5.util.assertAll
Expand Down Expand Up @@ -41,19 +41,19 @@ interface AgpInstrumentationSupportTests : AgpVariantAwareTests {
project.evaluate()

return listOf(
dynamicTest("has a task for writing the debug filters DSL to a resource file") {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersDebugAndroidTest")
assertAll(
{ assertThat(task).isNotNull() },
{ assertThat(task.includeTags.get()).containsExactly("global-include-tag") },
{ assertThat(task.excludeTags.get()).containsExactly("debug-exclude-tag") }
)
},
dynamicTest("has a task for writing the debug filters DSL to a resource file") {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersDebugAndroidTest")
assertAll(
{ assertThat(task).isNotNull() },
{ assertThat(task.includeTags.get()).containsExactly("global-include-tag") },
{ assertThat(task.excludeTags.get()).containsExactly("debug-exclude-tag") }
)
},

dynamicTest("has no task for writing the release DSL to a resource file") {
val task = project.tasks.findByName("writeFiltersReleaseAndroidTest")
assertThat(task).isNull()
}
dynamicTest("has no task for writing the release DSL to a resource file") {
val task = project.tasks.findByName("writeFiltersReleaseAndroidTest")
assertThat(task).isNull()
}
)
}

Expand Down Expand Up @@ -96,31 +96,31 @@ interface AgpInstrumentationSupportTests : AgpVariantAwareTests {
project.evaluate()

return listOf(
dynamicTest("has a task for writing the freeDebug filters DSL to a resource file") {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersFreeDebugAndroidTest")
assertThat(task).isNotNull()
assertThat(task.includeTags.get()).containsExactly("global-include-tag", "freeDebug-include-tag")
assertThat(task.excludeTags.get()).containsExactly("global-exclude-tag")
},
dynamicTest("has a task for writing the freeDebug filters DSL to a resource file") {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersFreeDebugAndroidTest")
assertThat(task).isNotNull()
assertThat(task.includeTags.get()).containsExactly("global-include-tag", "freeDebug-include-tag")
assertThat(task.excludeTags.get()).containsExactly("global-exclude-tag")
},

dynamicTest("has a task for writing the paidDebug filters DSL to a resource file") {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersPaidDebugAndroidTest")
assertThat(task).isNotNull()
assertThat(task.includeTags.get()).containsExactly("global-include-tag")
assertThat(task.excludeTags.get()).containsExactly("global-exclude-tag")
},
dynamicTest("has a task for writing the paidDebug filters DSL to a resource file") {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersPaidDebugAndroidTest")
assertThat(task).isNotNull()
assertThat(task.includeTags.get()).containsExactly("global-include-tag")
assertThat(task.excludeTags.get()).containsExactly("global-exclude-tag")
},

dynamicTest("doesn't have tasks for writing the release filters DSL to a resource file") {
assertThat(project.tasks.findByName("writeFiltersFreeReleaseAndroidTest")).isNull()
assertThat(project.tasks.findByName("writeFiltersPaidReleaseAndroidTest")).isNull()
}
dynamicTest("doesn't have tasks for writing the release filters DSL to a resource file") {
assertThat(project.tasks.findByName("writeFiltersFreeReleaseAndroidTest")).isNull()
assertThat(project.tasks.findByName("writeFiltersPaidReleaseAndroidTest")).isNull()
}
)
}
}

private fun Project.setupInstrumentationTests() {
android.defaultConfig {
it.testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder")
testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder"
}
dependencies.add("androidTestRuntimeOnly", "de.mannodermaus.junit5:android-test-runner:+")
}
Loading