diff --git a/build-logic/src/main/kotlin/Environment.kt b/build-logic/src/main/kotlin/Environment.kt index 637652c6..10eac031 100644 --- a/build-logic/src/main/kotlin/Environment.kt +++ b/build-logic/src/main/kotlin/Environment.kt @@ -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 { diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/config/PluginConfig.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/config/PluginConfig.kt index 759e9d7f..80461da3 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/config/PluginConfig.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/config/PluginConfig.kt @@ -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 @@ -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, + private val legacyPlugin: BasePlugin, private val componentsExtension: AndroidComponentsExtension<*, *, *> ) { @@ -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) } } @@ -63,9 +50,26 @@ private constructor( fun directoryProvidersOf(variant: Variant): Set { // 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 */ diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt index 41e8e249..ca893a28 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt @@ -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 @@ -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) } @@ -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" + ) + } + } + ) } } } diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt index ddb3b92f..b57c39da 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt @@ -1,6 +1,5 @@ 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 @@ -8,15 +7,10 @@ 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(EXTENSION_NAME) -internal val Project.android - get() = extensionByName("android") - @OptIn(ExperimentalContracts::class) internal fun Project.whenAndroidPluginAdded(block: (BasePlugin) -> Unit) { val configured = AtomicBoolean(false) diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5JacocoReport.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5JacocoReport.kt index ebf23dd1..9f52ff28 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5JacocoReport.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5JacocoReport.kt @@ -40,11 +40,11 @@ public abstract class AndroidJUnit5JacocoReport : JacocoReport() { variant: Variant, testTask: Test, directoryProviders: Collection - ): Boolean { + ): TaskProvider? { val configAction = ConfigAction(project, variant, testTask, directoryProviders) - if (project.tasks.namedOrNull(configAction.name) != null) { + project.tasks.namedOrNull(configAction.name)?.let { // Already exists; abort - return false + return null } val provider = project.tasks.register( @@ -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 = diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt index ec9c3e61..3971cfac 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt @@ -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) { @@ -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}" } } } } @@ -164,8 +171,6 @@ class FunctionalTests { productFlavor: String? = null, tests: List ) { - this.prettyPrint() - // Construct task name from given build type and/or product flavor // Examples: // - buildType="debug", productFlavor=null --> ":testDebugUnitTest" diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/extensions/AgpExtensions.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/extensions/AgpExtensions.kt new file mode 100644 index 00000000..3de9fb47 --- /dev/null +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/extensions/AgpExtensions.kt @@ -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>("android") + +internal val Project.androidApp + get() = extensionByName("android") diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpFilterTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpFilterTests.kt index 1d2309fd..78e5090a 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpFilterTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpFilterTests.kt @@ -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 @@ -35,7 +36,7 @@ interface AgpFilterTests : AgpVariantAwareTests { } } ) { project, buildType -> - val task = project.tasks.get("test${buildType.capitalize()}UnitTest") + val task = project.tasks.get("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") @@ -48,8 +49,8 @@ interface AgpFilterTests : AgpVariantAwareTests { fun `using custom build types & multiple flavor dimensions`(): List { 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() diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpInstrumentationSupportTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpInstrumentationSupportTests.kt index f30e198f..7f411c08 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpInstrumentationSupportTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpInstrumentationSupportTests.kt @@ -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 @@ -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("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("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() + } ) } @@ -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("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("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("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("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:+") } diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoExclusionRuleTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoExclusionRuleTests.kt index 587b5b09..01574562 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoExclusionRuleTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoExclusionRuleTests.kt @@ -8,6 +8,7 @@ import de.mannodermaus.gradle.plugins.junit5.util.evaluate import de.mannodermaus.gradle.plugins.junit5.util.get import org.gradle.api.Project import org.gradle.configurationcache.extensions.capitalized +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.TestFactory diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt index 62792c2f..e5ab1d0a 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt @@ -2,7 +2,7 @@ package de.mannodermaus.gradle.plugins.junit5.plugin import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -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.AndroidJUnit5JacocoReport import de.mannodermaus.gradle.plugins.junit5.tasks.JACOCO_TASK_NAME @@ -20,14 +20,12 @@ import org.junit.jupiter.api.TestFactory interface AgpJacocoVariantTests : AgpVariantAwareTests { private fun jacocoVariantTaskName(variant: String) = - "${JACOCO_TASK_NAME}${variant.capitalize()}" + "${JACOCO_TASK_NAME}${variant.capitalized()}" @Test fun `create jacoco task for custom build type`() { val project = createProject().applyJacocoPlugin().build() - project.android.buildTypes { - it.create("staging") - } + project.android.buildTypes.create("staging") project.evaluate() assertWithMessage("create a child task") @@ -84,18 +82,20 @@ interface AgpJacocoVariantTests : AgpVariantAwareTests { @TestFactory fun `hook in build-type-specific jacoco task to parent`() = forEachBuildType( - beforeBuild = { it.applyJacocoPlugin() } + beforeBuild = { it.applyJacocoPlugin() } ) { project, buildType -> val name = jacocoVariantTaskName(buildType) - assertThat(project.tasks.getByName(JACOCO_TASK_NAME) + assertThat( + project.tasks.getByName(JACOCO_TASK_NAME) .getDependentTaskNames() - .contains(name)) + .contains(name) + ) } @TestFactory fun `create variant-specific jacoco task`() = forEachVariant( - beforeBuild = { it.applyJacocoPlugin() } + beforeBuild = { it.applyJacocoPlugin() } ) { project, variant -> val name = jacocoVariantTaskName(variant) assertThat(project.tasks.findByName(name)).isNotNull() @@ -103,58 +103,60 @@ interface AgpJacocoVariantTests : AgpVariantAwareTests { @TestFactory fun `hook in variant-specific jacoco task to parent`() = forEachVariant( - beforeBuild = { it.applyJacocoPlugin() } + beforeBuild = { it.applyJacocoPlugin() } ) { project, variant -> - assertThat(project.tasks.getByName(JACOCO_TASK_NAME) - .getDependentTaskNames()) - .contains(jacocoVariantTaskName(variant)) + assertThat( + project.tasks.getByName(JACOCO_TASK_NAME) + .getDependentTaskNames() + ) + .contains(jacocoVariantTaskName(variant)) } @TestFactory fun `jacoco task includes main-scoped source directories`() = forEachBuildType( - beforeBuild = { it.applyJacocoPlugin() } + beforeBuild = { it.applyJacocoPlugin() } ) { project, buildType -> val name = jacocoVariantTaskName(buildType) val sourceDirs = project.tasks.get(name) - .sourceDirectories!! - .map { it.absolutePath } + .sourceDirectories!! + .map { it.absolutePath } // Expected items: "src/main/java" & "src//java" val mainDir = sourceDirs.find { it.endsWith("src/main/java") } val typeDir = sourceDirs.find { it.endsWith("src/$buildType/java") } assertAll( - "Mismatch! Actual dirs: $sourceDirs", - { assertWithMessage("main").that(mainDir).isNotNull() }, - { assertWithMessage(buildType).that(typeDir).isNotNull() } + "Mismatch! Actual dirs: $sourceDirs", + { assertWithMessage("main").that(mainDir).isNotNull() }, + { assertWithMessage(buildType).that(typeDir).isNotNull() } ) } @TestFactory fun `jacoco task does not include test-scoped source directories`() = forEachBuildType( - beforeBuild = { it.applyJacocoPlugin() } + beforeBuild = { it.applyJacocoPlugin() } ) { project, buildType -> val name = jacocoVariantTaskName(buildType) val sourceDirs = project.tasks.get(name) - .sourceDirectories!!.asPath + .sourceDirectories!!.asPath // Expected omissions: "src/test/java" & "src/test/java" assertAll( - "Mismatch! Actual dirs: $sourceDirs", - { assertThat(sourceDirs).doesNotContain("src/test/java") }, - { - assertThat(sourceDirs).doesNotContain("src/test${buildType.capitalized()}/java") - } + "Mismatch! Actual dirs: $sourceDirs", + { assertThat(sourceDirs).doesNotContain("src/test/java") }, + { + assertThat(sourceDirs).doesNotContain("src/test${buildType.capitalized()}/java") + } ) } @TestFactory fun `jacoco task does not include test-scoped class directories`() = forEachBuildType( - beforeBuild = { it.applyJacocoPlugin() } + beforeBuild = { it.applyJacocoPlugin() } ) { project, buildType -> val name = jacocoVariantTaskName(buildType) val classDirs = project.tasks.get(name) - .classDirectories!!.asPath + .classDirectories!!.asPath // Expected omissions: "classes/test" assertThat(classDirs).doesNotContain("classes/test") @@ -169,9 +171,9 @@ interface AgpJacocoVariantTests : AgpVariantAwareTests { project.evaluate() return listOf( - JACOCO_TASK_NAME to true, - "${JACOCO_TASK_NAME}Debug" to true, - "${JACOCO_TASK_NAME}Release" to false + JACOCO_TASK_NAME to true, + "${JACOCO_TASK_NAME}Debug" to true, + "${JACOCO_TASK_NAME}Release" to false ).map { (taskName, shouldExist) -> dynamicTest("$taskName task is${if (shouldExist) " " else " not"} generated") { val task = project.tasks.findByName(taskName) @@ -195,11 +197,11 @@ interface AgpJacocoVariantTests : AgpVariantAwareTests { project.evaluate() return listOf( - JACOCO_TASK_NAME to true, - "${JACOCO_TASK_NAME}PaidDebug" to true, - "${JACOCO_TASK_NAME}FreeRelease" to true, - "${JACOCO_TASK_NAME}PaidRelease" to false, - "${JACOCO_TASK_NAME}FreeDebug" to false + JACOCO_TASK_NAME to true, + "${JACOCO_TASK_NAME}PaidDebug" to true, + "${JACOCO_TASK_NAME}FreeRelease" to true, + "${JACOCO_TASK_NAME}PaidRelease" to false, + "${JACOCO_TASK_NAME}FreeDebug" to false ).map { (taskName, shouldExist) -> dynamicTest("$taskName task is${if (shouldExist) " " else " not"} generated") { val task = project.tasks.findByName(taskName) diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpTests.kt index 71181483..7da388ac 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpTests.kt @@ -1,6 +1,7 @@ package de.mannodermaus.gradle.plugins.junit5.plugin -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.util.evaluate import de.mannodermaus.gradle.plugins.junit5.util.projects.PluginSpecProjectCreator import de.mannodermaus.gradle.plugins.junit5.util.times @@ -18,9 +19,9 @@ interface AgpVariantAwareTests : AgpTests { fun defaultProductFlavors(): List fun forEachBuildType( - beforeBuild: ((PluginSpecProjectCreator.Builder) -> Unit) = {}, - beforeEvaluate: (Project) -> Unit = {}, - testBody: (Project, String) -> Unit + beforeBuild: ((PluginSpecProjectCreator.Builder) -> Unit) = {}, + beforeEvaluate: (Project) -> Unit = {}, + testBody: (Project, String) -> Unit ): List { val parentTestName = parentTestName() @@ -36,10 +37,10 @@ interface AgpVariantAwareTests : AgpTests { } fun forEachProductFlavor( - flavorCreator: () -> List = { defaultProductFlavors() }, - beforeBuild: ((PluginSpecProjectCreator.Builder) -> Unit) = {}, - beforeEvaluate: (Project) -> Unit = {}, - testBody: (Project, String) -> Unit + flavorCreator: () -> List = { defaultProductFlavors() }, + beforeBuild: ((PluginSpecProjectCreator.Builder) -> Unit) = {}, + beforeEvaluate: (Project) -> Unit = {}, + testBody: (Project, String) -> Unit ): List { val parentTestName = parentTestName() val flavors = flavorCreator() @@ -57,10 +58,10 @@ interface AgpVariantAwareTests : AgpTests { } fun forEachVariant( - flavorCreator: () -> List = { defaultProductFlavors() }, - beforeBuild: ((PluginSpecProjectCreator.Builder) -> Unit) = {}, - beforeEvaluate: (Project) -> Unit = {}, - testBody: (Project, String) -> Unit + flavorCreator: () -> List = { defaultProductFlavors() }, + beforeBuild: ((PluginSpecProjectCreator.Builder) -> Unit) = {}, + beforeEvaluate: (Project) -> Unit = {}, + testBody: (Project, String) -> Unit ): List { val parentTestName = parentTestName() val flavors = flavorCreator() @@ -71,7 +72,7 @@ interface AgpVariantAwareTests : AgpTests { project.evaluate() return (defaultBuildTypes() * flavors).map { (buildType, flavor) -> - val variantName = "${flavor.name}${buildType.capitalize()}" + val variantName = "${flavor.name}${buildType.capitalized()}" DynamicTest.dynamicTest("$parentTestName for '$variantName'") { testBody(project, variantName) @@ -87,14 +88,14 @@ interface AgpVariantAwareTests : AgpTests { } fun Project.registerProductFlavors( - flavors: List = defaultProductFlavors() + flavors: List = defaultProductFlavors() ) { val dimensions = flavors.map(FlavorSpec::dimension).distinct() - project.android.flavorDimensions(*dimensions.toTypedArray()) - project.android.productFlavors { container -> + with(project.android) { + flavorDimensions.addAll(dimensions) flavors.forEach { flavor -> - container.create(flavor.name).dimension = flavor.dimension + productFlavors.create(flavor.name).dimension = flavor.dimension } } } diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt index 21579328..55d270de 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt @@ -2,8 +2,8 @@ package de.mannodermaus.gradle.plugins.junit5.plugin import com.google.common.truth.Truth.assertThat import de.mannodermaus.Libraries +import de.mannodermaus.gradle.plugins.junit5.extensions.android import de.mannodermaus.gradle.plugins.junit5.internal.config.ANDROID_JUNIT5_RUNNER_BUILDER_CLASS -import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junitPlatform import de.mannodermaus.gradle.plugins.junit5.util.assertThat import de.mannodermaus.gradle.plugins.junit5.util.evaluate diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/TestEnvironment.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/TestEnvironment.kt index a140d9ab..2e280a63 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/TestEnvironment.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/TestEnvironment.kt @@ -32,7 +32,7 @@ class TestEnvironment { val androidSdkFolder = loadAndroidSdkFolder() val envProps: Properties = loadAndroidEnvironment() - val compileSdkVersion: String + val compileSdkVersion: Int val minSdkVersion: Int val targetSdkVersion: Int @@ -43,7 +43,7 @@ class TestEnvironment { val supportedAgpVersions: List init { - compileSdkVersion = envProps.getProperty(COMPILE_SDK_PROP_NAME) + compileSdkVersion = envProps.getProperty(COMPILE_SDK_PROP_NAME).toInt() minSdkVersion = envProps.getProperty(MIN_SDK_PROP_NAME).toInt() targetSdkVersion = envProps.getProperty(TARGET_SDK_PROP_NAME).toInt() kotlinVersion = envProps.getProperty(KOTLIN_VERSION_PROP_NAME) diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/FunctionalTestProjectCreator.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/FunctionalTestProjectCreator.kt index 8ad0d20c..36455df0 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/FunctionalTestProjectCreator.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/FunctionalTestProjectCreator.kt @@ -134,6 +134,7 @@ class FunctionalTestProjectCreator( val useCustomBuildType = config[TomlSpec.Settings.useCustomBuildType] val returnDefaultValues = config[TomlSpec.Settings.returnDefaultValues] val includeAndroidResources = config[TomlSpec.Settings.includeAndroidResources] + val allowSkipped = config[TomlSpec.Settings.allowSkipped] val expectedTests = config[TomlSpec.expectations] val disableTestsForBuildTypes = config[TomlSpec.Settings.disableTestsForBuildTypes] @@ -172,6 +173,7 @@ class FunctionalTestProjectCreator( val useFlavors by optional(default = false) val useKotlin by optional(default = false) val useJacoco by optional(default = false) + val allowSkipped by optional(default = false) val useCustomBuildType by optional(default = null) val returnDefaultValues by optional(default = false) val includeAndroidResources by optional(default = false) diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/PluginSpecProjectCreator.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/PluginSpecProjectCreator.kt index 57b89db0..6bfcb0f6 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/PluginSpecProjectCreator.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/PluginSpecProjectCreator.kt @@ -1,7 +1,8 @@ package de.mannodermaus.gradle.plugins.junit5.util.projects import de.mannodermaus.gradle.plugins.junit5.dsl.AndroidJUnitPlatformExtension -import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android +import de.mannodermaus.gradle.plugins.junit5.extensions.android +import de.mannodermaus.gradle.plugins.junit5.extensions.androidApp import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junitPlatform import de.mannodermaus.gradle.plugins.junit5.util.TestEnvironment import de.mannodermaus.gradle.plugins.junit5.util.applyPlugin @@ -62,13 +63,14 @@ class PluginSpecProjectCreator(private val environment: TestEnvironment) { fun asAndroidLibrary() = setProjectTypeIfUnsetTo(Type.Library) - fun applyJUnit5Plugin(state: Boolean = true, configuration: ((AndroidJUnitPlatformExtension) -> Unit)? = null) = apply { - this.applyJUnit5Plugin = if (state) { - configuration ?: {} - } else { - null + fun applyJUnit5Plugin(state: Boolean = true, configuration: ((AndroidJUnitPlatformExtension) -> Unit)? = null) = + apply { + this.applyJUnit5Plugin = if (state) { + configuration ?: {} + } else { + null + } } - } fun applyJacocoPlugin(state: Boolean = true) = apply { this.applyJacocoPlugin = state @@ -105,19 +107,21 @@ class PluginSpecProjectCreator(private val environment: TestEnvironment) { // Add default configuration try { - project.android.compileSdkVersion(environment.compileSdkVersion) - project.android.namespace = appId - - if (projectType == Type.Application) { - project.android.defaultConfig.apply { - applicationId = appId - minSdkVersion(environment.minSdkVersion) - targetSdkVersion(environment.targetSdkVersion) - versionCode = 1 - versionName = "1.0" + with(project.android) { + compileSdk = environment.compileSdkVersion + namespace = appId + + if (projectType == Type.Application) { + project.androidApp.defaultConfig.apply { + applicationId = appId + minSdk = environment.minSdkVersion + targetSdk = environment.targetSdkVersion + versionCode = 1 + versionName = "1.0" + } } } - } catch (e: UnknownDomainObjectException) { + } catch (_: UnknownDomainObjectException) { // Expected when the Android plugin is not applied to a project; // swallow this particular error } @@ -125,7 +129,7 @@ class PluginSpecProjectCreator(private val environment: TestEnvironment) { // Configure JUnit 5 with custom configuration clause, if any try { applyJUnit5Plugin?.invoke(project.junitPlatform) - } catch (e: UnknownDomainObjectException) { + } catch (_: UnknownDomainObjectException) { // Expected when the JUnit 5 plugin is not applied to a project; // swallow this particular error } diff --git a/plugin/android-junit5/src/test/resources/de/mannodermaus/gradle/plugins/junit5/testenv.properties b/plugin/android-junit5/src/test/resources/de/mannodermaus/gradle/plugins/junit5/testenv.properties index 579b941a..adef2987 100644 --- a/plugin/android-junit5/src/test/resources/de/mannodermaus/gradle/plugins/junit5/testenv.properties +++ b/plugin/android-junit5/src/test/resources/de/mannodermaus/gradle/plugins/junit5/testenv.properties @@ -1,6 +1,6 @@ # suppress inspection "UnusedProperty" for whole file # (Populated from the Gradle "processTestResources" task, used by the test runtime) -COMPILE_SDK_VERSION = android-@COMPILE_SDK_VERSION@ +COMPILE_SDK_VERSION = @COMPILE_SDK_VERSION@ MIN_SDK_VERSION = @MIN_SDK_VERSION@ TARGET_SDK_VERSION = @TARGET_SDK_VERSION@ diff --git a/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template b/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template index 38ab876c..123ef0e3 100644 --- a/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template +++ b/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template @@ -34,7 +34,12 @@ plugins { id("com.android.application") {% if USE_KOTLIN %} - id("org.jetbrains.kotlin.android") + {% if atLeastAgp("9.0.0-alpha04") %} + // AGP 9 introduces a built-in Kotlin distribution, + // making it obsolete to declare Kotlin explicitly alongside it + {% else %} + id("org.jetbrains.kotlin.android") + {% endif %} {% endif %} {% if USE_JACOCO %} @@ -64,20 +69,20 @@ android { val minSdkVersion: Int = {{ MIN_SDK_VERSION }} {% if OVERRIDE_SDK_VERSION %} - val compileSdk: String = "android-{{ OVERRIDE_SDK_VERSION }}" - val targetSdkVersion: Int = {{ OVERRIDE_SDK_VERSION }} + val usedCompileSdk: Int = {{ OVERRIDE_SDK_VERSION }} + val usedTargetSdkVersion: Int = {{ OVERRIDE_SDK_VERSION }} {% else %} - val compileSdk: String = "{{ COMPILE_SDK_VERSION }}" - val targetSdkVersion: Int = {{ TARGET_SDK_VERSION }} + val usedCompileSdk: Int = {{ COMPILE_SDK_VERSION }} + val usedTargetSdkVersion: Int = {{ TARGET_SDK_VERSION }} {% endif %} - compileSdkVersion("${compileSdk}") + compileSdk = usedCompileSdk defaultConfig { namespace = "de.mannodermaus.app" minSdk = minSdkVersion - targetSdk = targetSdkVersion + targetSdk = usedTargetSdkVersion {% if INCLUDE_ANDROID_RESOURCES %} testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -97,7 +102,7 @@ android { } {% if USE_FLAVORS %} - flavorDimensions("environment") + flavorDimensions.add("environment") productFlavors { register("free") { dimension = "environment" diff --git a/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml b/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml index 98ea62f5..b6b5c091 100644 --- a/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml +++ b/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml @@ -1,3 +1,4 @@ [settings] useJacoco = true +allowSkipped = true task = "jacocoTestReportDebug"