Skip to content

Plugin: Become more lenient when test tasks are absent #227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 24, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ class AndroidJUnitPlatformPlugin : Plugin<Project> {
}

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 ->
Expand Down Expand Up @@ -133,7 +134,7 @@ class AndroidJUnitPlatformPlugin : Plugin<Project> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = setOf(
)

// Test permutations for projects (default: empty set, which will exercise all)
private val testedProjects: Set<String> = 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<DynamicNode> =
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<TestedAgp>.filterAgpVersions(): List<TestedAgp> =
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<FunctionalTestProjectCreator.Spec>.filterSpecs(): List<FunctionalTestProjectCreator.Spec> =
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 {
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

/**
Expand All @@ -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 ->
Expand All @@ -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) {
Expand All @@ -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<Any?>)
?.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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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()"
Expand All @@ -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()

Expand All @@ -75,29 +82,44 @@ 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]
val returnDefaultValues = config[TomlSpec.Settings.returnDefaultValues]
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) {
Expand Down Expand Up @@ -125,11 +147,13 @@ class FunctionalTestProjectCreator(private val rootFolder: File,
val expectations by required<List<ExpectedTests>>()

object Settings : ConfigSpec() {
val minAgpVersion by optional<String?>(default = null)
val useFlavors by optional(default = false)
val useKotlin by optional(default = false)
val useCustomBuildType by optional<String?>(default = null)
val returnDefaultValues by optional(default = false)
val includeAndroidResources by optional(default = false)
val disableTestsForBuildTypes by optional<String?>(default = null)
}
}

Expand All @@ -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)
}
Expand Down
Loading