diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt index 0276c3cd..4c24199b 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt @@ -56,9 +56,10 @@ class AndroidJUnitPlatformPlugin : Plugin { } private fun Project.configureUnitTests() { - // Configure JUnit 5 for each variant-specific unit test task + // Configure JUnit 5 for each variant-specific unit test task, + // unless that variant has its tests disabled projectConfig.variants.all { variant -> - val testTask = tasks.testTaskOf(variant) + val testTask = tasks.testTaskOf(variant) ?: return@all val configuration = junit5ConfigurationOf(variant) testTask.useJUnitPlatform { options -> @@ -133,7 +134,7 @@ class AndroidJUnitPlatformPlugin : Plugin { if (isJacocoApplied && jacocoOptions.taskGenerationEnabled) { projectConfig.variants.all { variant -> val directoryProviders = collectDirectoryProviders(variant) - val testTask = tasks.testTaskOf(variant) + val testTask = tasks.testTaskOf(variant) ?: return@all // Create a Jacoco friend task val enabledVariants = jacocoOptions.onlyGenerateTasksForVariants diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/Extensions.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/Extensions.kt index 92a4373e..c69a6be4 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/Extensions.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/Extensions.kt @@ -217,13 +217,13 @@ internal fun BaseVariant.getTaskName(prefix: String = "", suffix: String = ""): /** * Obtains the {AndroidUnitTest} for the provided variant. */ -internal fun TaskContainer.testTaskOf(variant: BaseVariant): AndroidUnitTest { +internal fun TaskContainer.testTaskOf(variant: BaseVariant): AndroidUnitTest? { // From AGP 4.1 onwards, there is no Scope API on VariantData anymore. // Task names must be constructed manually val taskName = variant.getTaskName( prefix = VariantTypeCompat.UNIT_TEST_PREFIX, suffix = VariantTypeCompat.UNIT_TEST_SUFFIX) - return getByName(taskName) as AndroidUnitTest + return findByName(taskName) as? AndroidUnitTest } /** 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 e3bbd4a6..cbeac554 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 @@ -18,67 +18,102 @@ import java.io.File class FunctionalTests { private val environment = TestEnvironment() - private lateinit var projectCreator: FunctionalTestProjectCreator + private lateinit var folder: File + + // Test permutations for AGP (default: empty set, which will exercise all) + private val testedAgpVersions: Set = setOf( + ) + + // Test permutations for projects (default: empty set, which will exercise all) + private val testedProjects: Set = setOf( + ) @BeforeAll fun beforeAll() { // The "project provider" is responsible for the construction // of all virtual Gradle projects, using a template file located in // the project's test resources. - val folder = File("build/tmp/virtualProjectsRoot") - folder.mkdirs() - projectCreator = FunctionalTestProjectCreator(folder, environment) + folder = File("build/tmp/virtualProjectsRoot").also { it.mkdirs() } } @TestFactory fun execute(): List = - environment.supportedAgpVersions.map { agp -> - dynamicContainer( - "AGP ${agp.shortVersion}", - projectCreator.allSpecs.map { spec -> - dynamicTest(spec.name) { - // Required for visibility inside IJ's logging console (display names are still bugged in the IDE) - println("AGP: ${agp.version}, Project: ${spec.name}, Forced Gradle: ${agp.requiresGradle ?: "no"}") - - // Create a virtual project with the given settings & AGP version - val project = projectCreator.createProject(spec, agp) - - // Execute the tests of the virtual project with Gradle - val result = runGradle(agp) - .withProjectDir(project) - .build() - - // Check that the task execution was successful in general - when (val outcome = result.task(":test")?.outcome) { - TaskOutcome.UP_TO_DATE -> { - // Nothing to do, a previous build already checked this - println("Test task up-to-date; skipping assertions.") - } + // Create a matrix of permutations between the AGP versions to test + // and the language of the project's build script + environment.supportedAgpVersions.filterAgpVersions() + .map { agp -> + val projectCreator = FunctionalTestProjectCreator(folder, environment) - 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) { - result.assertAgpTests( - buildType = expectation.buildType, - productFlavor = expectation.productFlavor, - tests = expectation.testsList - ) - } - } + // Generate a container for all tests with this specific AGP/Language combination + dynamicContainer("AGP ${agp.shortVersion}", + + // Exercise each test project within the given environment + projectCreator.allSpecs.filterSpecs().map { spec -> + dynamicTest(spec.name) { + // Required for visibility inside IJ's logging console (display names are still bugged in the IDE) + println("AGP: ${agp.version}, Project: ${spec.name}, Forced Gradle: ${agp.requiresGradle ?: "no"}") + + // Create a virtual project with the given settings & AGP version. + // This call will throw a TestAbortedException if the spec is not eligible for this version, + // marking the test as ignored in the process + val project = projectCreator.createProject(spec, agp) + + // Execute the tests of the virtual project with Gradle + val result = runGradle(agp) + .withProjectDir(project) + .build() + + // Check that the task execution was successful in general + when (val outcome = result.task(":test")?.outcome) { + TaskOutcome.UP_TO_DATE -> { + // Nothing to do, a previous build already checked this + println("Test task up-to-date; skipping assertions.") + } + + 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) { + result.assertAgpTests( + buildType = expectation.buildType, + productFlavor = expectation.productFlavor, + tests = expectation.testsList + ) + } + } - else -> { - // Unexpected result; fail - fail { "Unexpected task outcome: $outcome" } + else -> { + // Unexpected result; fail + fail { "Unexpected task outcome: $outcome" } + } + } } } - } - } - ) - } + ) + } /* Private */ + private fun List.filterAgpVersions(): List = + if (testedAgpVersions.isEmpty()) { + // Nothing to do, exercise functional tests on all AGP versions + this + } else { + filter { agp -> + testedAgpVersions.any { it == agp.shortVersion } + } + } + + private fun List.filterSpecs(): List = + if (testedProjects.isEmpty()) { + // Nothing to do, exercise all different projects + this + } else { + filter { spec -> + testedProjects.any { it == spec.name } + } + } + private fun runGradle(agpVersion: TestedAgp) = GradleRunner.create() .apply { @@ -109,7 +144,9 @@ class FunctionalTests { .ofTask(taskName) .apply { tests.forEach { expectedClass -> - contains("$expectedClass > test() PASSED") + val line = "$expectedClass > test() PASSED" + contains(line) + println(line) } executedTestCount().isEqualTo(tests.size) } diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/BuildScriptTemplateProcessor.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/BuildScriptTemplateProcessor.kt index 86fa7d6d..ec049076 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/BuildScriptTemplateProcessor.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/projects/BuildScriptTemplateProcessor.kt @@ -7,6 +7,7 @@ private val GET_MATCHER = Regex("//\\\$GET\\{(.*)}") private val IF_MATCHER = Regex("//\\\$IF\\{(.*)}") private val ELSE_MATCHER = Regex("//\\\$ELSE") private val IFGRADLE_MATCHER = Regex("//\\\$IFGRADLE\\{(.*)}") +private val FOREACH_MATCHER = Regex("//\\\$FOREACH\\{(.+)}") private val END_MATCHER = Regex("//\\\$END") /** @@ -19,6 +20,7 @@ class BuildScriptTemplateProcessor(private val targetGradleVersion: String?, fun process(rawText: String): String { var ignoredBlockCount = 0 + var loopBlockCount = 0 // Replace GET tokens first val text1 = rawText.replace(GET_MATCHER) { result -> @@ -37,6 +39,7 @@ class BuildScriptTemplateProcessor(private val targetGradleVersion: String?, val ifMatch = IF_MATCHER.find(line) val elseMatch = ELSE_MATCHER.find(line) val ifgradleMatch = IFGRADLE_MATCHER.find(line) + val foreachMatch = FOREACH_MATCHER.find(line) val endMatch = END_MATCHER.find(line) if (ignoredBlockCount == 0 && ifMatch != null) { @@ -62,6 +65,25 @@ class BuildScriptTemplateProcessor(private val targetGradleVersion: String?, } } + if (ignoredBlockCount == 0 && foreachMatch != null) { + val foreachKey = foreachMatch.groupValues.last() + + // Started a loop. Find the associated list of elements first + val value = (replacements[foreachKey] as? List) + ?.joinToString(separator = ",") { "\"$it\""} + ?: throw IllegalArgumentException("FOREACH replacement value should be a list, but got: ${replacements[foreachKey]}") + + // Emit the beginning of a foreach loop and continue onward. + // As soon as the next END marker is detected, the loop will be closed again + if (value.isNotEmpty()) { + text2.append("listOf($value).forEach { it ->").appendln() + loopBlockCount++ + } else { + // Empty list == ignore the entire block and don't emit it + ignoredBlockCount++ + } + } + if (elseMatch != null) { ignoredBlockCount = if (ignoredBlockCount == 0) { 1 @@ -78,7 +100,13 @@ class BuildScriptTemplateProcessor(private val targetGradleVersion: String?, } if (endMatch != null) { - ignoredBlockCount = max(0, ignoredBlockCount - 1) + if (loopBlockCount > 0) { + // Emit the end of a prior loop + text2.append("}").appendln() + loopBlockCount = max(0, loopBlockCount - 1) + } else { + ignoredBlockCount = max(0, ignoredBlockCount - 1) + } } } return text2.toString() 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 4ad449a0..8fec2b3c 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 @@ -5,13 +5,16 @@ import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.source.toml import de.mannodermaus.gradle.plugins.junit5.util.TestedAgp import de.mannodermaus.gradle.plugins.junit5.util.TestEnvironment +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.opentest4j.TestAbortedException import java.io.File private const val TEST_PROJECTS_RESOURCE = "/test-projects" -private const val BUILD_GRADLE_TEMPLATE_NAME = "build.gradle.template" -private const val SETTINGS_GRADLE_TEMPLATE_NAME = "settings.gradle.template" -private const val BUILD_GRADLE_NAME = "build.gradle.kts" -private const val SETTINGS_GRADLE_NAME = "settings.gradle.kts" +private const val BUILD_GRADLE_TEMPLATE_NAME = "build.gradle.kts.template" +private const val SETTINGS_GRADLE_TEMPLATE_NAME = "settings.gradle.kts.template" +private const val OUTPUT_BUILD_GRADLE_NAME = "build.gradle.kts" +private const val OUTPUT_SETTINGS_GRADLE_NAME = "settings.gradle.kts" private const val PROJECT_CONFIG_FILE_NAME = "config.toml" private const val SRC_FOLDER_NAME = "src" @@ -49,7 +52,11 @@ class FunctionalTestProjectCreator(private val rootFolder: File, ?: emptyList() } + @Throws(TestAbortedException::class) fun createProject(spec: Spec, agp: TestedAgp): File { + // Validate the spec requirement against the executing AGP version first + validateSpec(spec, agp) + // Construct the project folder, cleaning it if necessary. // If any Gradle or build caches already exist, we keep those around. // That's the reason for not doing "projectFolder.deleteRecursively()" @@ -58,8 +65,8 @@ class FunctionalTestProjectCreator(private val rootFolder: File, val projectFolder = File(rootFolder, projectName) if (projectFolder.exists()) { File(projectFolder, SRC_FOLDER_NAME).deleteRecursively() - File(projectFolder, BUILD_GRADLE_NAME).delete() - File(projectFolder, SETTINGS_GRADLE_NAME).delete() + File(projectFolder, OUTPUT_BUILD_GRADLE_NAME).delete() + File(projectFolder, OUTPUT_SETTINGS_GRADLE_NAME).delete() } projectFolder.mkdirs() @@ -75,22 +82,33 @@ class FunctionalTestProjectCreator(private val rootFolder: File, replacements["USE_CUSTOM_BUILD_TYPE"] = spec.useCustomBuildType replacements["RETURN_DEFAULT_VALUES"] = spec.returnDefaultValues replacements["INCLUDE_ANDROID_RESOURCES"] = spec.includeAndroidResources + replacements["DISABLE_TESTS_FOR_BUILD_TYPES"] = spec.disableTestsForBuildTypes val processor = BuildScriptTemplateProcessor(agp.requiresGradle, replacements) val processedBuildGradle = processor.process(rawBuildGradle) - File(projectFolder, BUILD_GRADLE_NAME).writeText(processedBuildGradle) + File(projectFolder, OUTPUT_BUILD_GRADLE_NAME).writeText(processedBuildGradle) val processedSettingsGradle = processor.process(rawSettingsGradle) - File(projectFolder, SETTINGS_GRADLE_NAME).writeText(processedSettingsGradle) + File(projectFolder, OUTPUT_SETTINGS_GRADLE_NAME).writeText(processedSettingsGradle) return projectFolder } + private fun validateSpec(spec: Spec, agp: TestedAgp) { + if (spec.minAgpVersion != null) { + // If the spec dictates a minimum version of the AGP, + // disable the test for plugin versions below that minimum requirement + assumeTrue( + SemanticVersion(agp.version) >= SemanticVersion(spec.minAgpVersion), + "This project requires AGP ${spec.minAgpVersion} and was disabled on ths version.") + } + } + /* Types */ class Spec private constructor(val name: String, val srcFolder: File, config: Config) { - + val minAgpVersion = config[TomlSpec.Settings.minAgpVersion] val useKotlin = config[TomlSpec.Settings.useKotlin] val useFlavors = config[TomlSpec.Settings.useFlavors] val useCustomBuildType = config[TomlSpec.Settings.useCustomBuildType] @@ -98,6 +116,10 @@ class FunctionalTestProjectCreator(private val rootFolder: File, val includeAndroidResources = config[TomlSpec.Settings.includeAndroidResources] val expectedTests = config[TomlSpec.expectations] + val disableTestsForBuildTypes = config[TomlSpec.Settings.disableTestsForBuildTypes] + ?.split(",")?.map(String::trim) + ?: emptyList() + companion object { fun tryCreate(folder: File): Spec? { if (folder.isFile) { @@ -125,11 +147,13 @@ class FunctionalTestProjectCreator(private val rootFolder: File, val expectations by required>() object Settings : ConfigSpec() { + val minAgpVersion by optional(default = null) val useFlavors by optional(default = false) val useKotlin by optional(default = false) val useCustomBuildType by optional(default = null) val returnDefaultValues by optional(default = false) val includeAndroidResources by optional(default = false) + val disableTestsForBuildTypes by optional(default = null) } } @@ -139,7 +163,7 @@ class FunctionalTestProjectCreator(private val rootFolder: File, data class ExpectedTests( val buildType: String, val productFlavor: String?, - val tests: String + private val tests: String ) { val testsList = tests.split(",").map(String::trim) } diff --git a/plugin/android-junit5/src/test/resources/test-projects/build.gradle.template b/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template similarity index 89% rename from plugin/android-junit5/src/test/resources/test-projects/build.gradle.template rename to plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template index ce88e451..d8597fb5 100644 --- a/plugin/android-junit5/src/test/resources/test-projects/build.gradle.template +++ b/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template @@ -16,6 +16,10 @@ // Evaluate the running Gradle version against the "versionmarker", excluding the block if it doesn't match. // The "versionmarker" is an expression in the form 'x.y', where x.y is a Gradle version. // An AGP version without a fixed Gradle requirement will always evaluate these blocks to false, skipping them +// $FOREACH{list} +// Emit the body of the enclosed block for each item in the provided "list", +// which is coerced into a list of strings by the template processor. +// Inside the enclosed block, the list element can be accessed with the `it` variable. // $ELSE // Alternative case for $IF or $GRADLE markers. // Inverts the status of excluding/including blocks based on the condition. @@ -115,6 +119,15 @@ android { }, unitTests)) //$END } + + // New Variant API, introduced with AGP 4.1 + //$FOREACH{DISABLE_TESTS_FOR_BUILD_TYPES} + onVariants.withBuildType(it) { + unitTest { + enabled = false + } + } + //$END } dependencies { diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/config.toml b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/config.toml new file mode 100644 index 00000000..c2e0e632 --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/config.toml @@ -0,0 +1,15 @@ +[settings] +minAgpVersion = "4.0" +useFlavors = true +useKotlin = true +disableTestsForBuildTypes = "debug" + +[[expectations]] +buildType = "release" +productFlavor = "free" +tests = "JavaTest,KotlinReleaseTest,JavaFreeReleaseTest" + +[[expectations]] +buildType = "release" +productFlavor = "paid" +tests = "JavaTest,KotlinReleaseTest" diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/main/AndroidManifest.xml b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..426c240f --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/main/java/de/mannodermaus/app/Adder.java b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/main/java/de/mannodermaus/app/Adder.java new file mode 100644 index 00000000..d21fca3d --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/main/java/de/mannodermaus/app/Adder.java @@ -0,0 +1,7 @@ +package de.mannodermaus.app; + +public class Adder { + public int add(int a, int b) { + return a + b; + } +} diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/test/java/de/mannodermaus/app/JavaTest.java b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/test/java/de/mannodermaus/app/JavaTest.java new file mode 100644 index 00000000..08c59a97 --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/test/java/de/mannodermaus/app/JavaTest.java @@ -0,0 +1,13 @@ +package de.mannodermaus.app; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class JavaTest { + @Test + void test() { + Adder adder = new Adder(); + assertEquals(4, adder.add(2, 2), "This should succeed!"); + } +} diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testFreeRelease/java/de/mannodermaus/app/JavaFreeReleaseTest.java b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testFreeRelease/java/de/mannodermaus/app/JavaFreeReleaseTest.java new file mode 100644 index 00000000..6631487c --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testFreeRelease/java/de/mannodermaus/app/JavaFreeReleaseTest.java @@ -0,0 +1,13 @@ +package de.mannodermaus.app; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class JavaFreeReleaseTest { + @Test + void test() { + Adder adder = new Adder(); + assertEquals(4, adder.add(2, 2), "This should succeed!"); + } +} diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testPaidDebug/java/de/mannodermaus/app/KotlinPaidDebugTest.kt b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testPaidDebug/java/de/mannodermaus/app/KotlinPaidDebugTest.kt new file mode 100644 index 00000000..8fb6cadd --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testPaidDebug/java/de/mannodermaus/app/KotlinPaidDebugTest.kt @@ -0,0 +1,12 @@ +package de.mannodermaus.app + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class KotlinPaidDebugTest { + @Test + fun test() { + val adder = Adder() + assertEquals(4, adder.add(2, 2), "This should succeed!") + } +} diff --git a/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testRelease/java/de/mannodermaus/app/KotlinReleaseTest.kt b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testRelease/java/de/mannodermaus/app/KotlinReleaseTest.kt new file mode 100644 index 00000000..cd36ba26 --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/new-variant-api/src/testRelease/java/de/mannodermaus/app/KotlinReleaseTest.kt @@ -0,0 +1,12 @@ +package de.mannodermaus.app + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class KotlinReleaseTest { + @Test + fun test() { + val adder = Adder() + assertEquals(4, adder.add(2, 2), "This should succeed!") + } +} diff --git a/plugin/android-junit5/src/test/resources/test-projects/settings.gradle.template b/plugin/android-junit5/src/test/resources/test-projects/settings.gradle.kts.template similarity index 100% rename from plugin/android-junit5/src/test/resources/test-projects/settings.gradle.template rename to plugin/android-junit5/src/test/resources/test-projects/settings.gradle.kts.template