Skip to content

Commit

Permalink
Use new Variant API for generated instrumentation filter file (#336)
Browse files Browse the repository at this point in the history
* Add unit tests for write-filters task & fix a bug with generating the filters file for instrumentation tests

* Replace resource folder generation for instrumentation filters with new Variant API

Since the minimum AGP version bump, we can finally safely access the
source directories across all supported versions

* Update API definition to reflect changes to the filters task

* Changelog
  • Loading branch information
mannodermaus authored May 22, 2024
1 parent 5fedf9a commit 81280fd
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 83 deletions.
1 change: 1 addition & 0 deletions plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Change Log
- Decouple discovery of instrumentation tests from Jupiter, allowing non-Jupiter test engines to be discovered as well
- Update lifecycle of instrumentation runner params to only be set once, instead of once per test
- Properly reported disabled dynamic tests to Android instrumentation
- Use new Variant API to register generated resource folder for instrumentation filters file

## 1.10.0.0 (2023-11-05)
- JUnit 5.10.0
Expand Down
9 changes: 3 additions & 6 deletions plugin/android-junit5/api/android-junit5.api
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,8 @@ public abstract class de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5J
public abstract class de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5WriteFilters : org/gradle/api/DefaultTask {
public fun <init> ()V
public final fun execute ()V
public final fun getExcludeTags ()Ljava/util/List;
public final fun getIncludeTags ()Ljava/util/List;
public final fun getOutputFolder ()Ljava/io/File;
public final fun setExcludeTags (Ljava/util/List;)V
public final fun setIncludeTags (Ljava/util/List;)V
public final fun setOutputFolder (Ljava/io/File;)V
public abstract fun getExcludeTags ()Lorg/gradle/api/provider/ListProperty;
public abstract fun getIncludeTags ()Lorg/gradle/api/provider/ListProperty;
public abstract fun getOutputFolder ()Lorg/gradle/api/file/DirectoryProperty;
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import com.android.build.gradle.DynamicFeaturePlugin
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.TestVariant
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.instrumentationTestVariant
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
Expand Down Expand Up @@ -68,11 +66,6 @@ private constructor(
?: emptySet()
}

fun instrumentationTestVariantOf(variant: Variant): TestVariant? {
return legacyVariants.firstOrNull { it.name == variant.name }
?.run { this.instrumentationTestVariant }
}

/* Private */

private fun directoryProvidersOf(legacyVariant: BaseVariant): Set<DirectoryProvider> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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.hasDependency
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.instrumentationTestVariant
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junit5Warn
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.namedOrNull
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.usesComposeIn
Expand All @@ -23,7 +23,6 @@ import de.mannodermaus.gradle.plugins.junit5.internal.utils.excludedPackagingOpt
import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5JacocoReport
import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5WriteFilters
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.tasks.testing.Test

internal fun configureJUnit5(
Expand Down Expand Up @@ -51,7 +50,7 @@ internal fun configureJUnit5(
variants.forEach { variant ->
configureUnitTests(it, variant)
configureJacoco(it, config, variant)
configureInstrumentationTests(it, config, variant)
configureInstrumentationTests(it, variant)
}
}
}
Expand Down Expand Up @@ -197,12 +196,11 @@ private fun AndroidJUnitPlatformExtension.configureJacoco(

private fun AndroidJUnitPlatformExtension.configureInstrumentationTests(
project: Project,
config: PluginConfig,
variant: Variant,
) {
if (!instrumentationTests.enabled.get()) return

config.instrumentationTestVariantOf(variant)?.let { instrumentationTestVariant ->
AndroidJUnit5WriteFilters.register(project, variant, instrumentationTestVariant)
variant.instrumentationTestVariant?.sources?.res?.let { sourceDirs ->
AndroidJUnit5WriteFilters.register(project, variant, sourceDirs)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package de.mannodermaus.gradle.plugins.junit5.internal.extensions

import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.TestVariant
import com.android.build.gradle.api.UnitTestVariant
import com.android.build.gradle.internal.api.TestedVariant

Expand All @@ -15,12 +14,3 @@ internal val BaseVariant.unitTestVariant: UnitTestVariant

return requireNotNull(this.unitTestVariant)
}

internal val BaseVariant.instrumentationTestVariant: TestVariant?
get() {
if (this !is TestedVariant) {
throw IllegalArgumentException("Argument is not TestedVariant: $this")
}

return this.testVariant
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.mannodermaus.gradle.plugins.junit5.internal.extensions

import com.android.build.api.variant.AndroidTest
import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.Variant

internal fun Variant.getTaskName(prefix: String = "", suffix: String = ""): String {
Expand All @@ -18,3 +20,6 @@ internal fun Variant.getTaskName(prefix: String = "", suffix: String = ""): Stri
append(suffix.capitalized())
}.toString()
}

internal val Variant.instrumentationTestVariant: AndroidTest?
get() = (this as? HasAndroidTest)?.androidTest
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
@file:Suppress("DEPRECATION")

package de.mannodermaus.gradle.plugins.junit5.tasks

import com.android.build.api.variant.SourceDirectories
import com.android.build.api.variant.Variant
import com.android.build.gradle.api.TestVariant
import de.mannodermaus.gradle.plugins.junit5.internal.config.INSTRUMENTATION_FILTER_RES_FILE_NAME
import de.mannodermaus.gradle.plugins.junit5.internal.config.JUnit5TaskConfig
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.getTaskName
import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junitPlatform
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
Expand All @@ -25,20 +25,20 @@ import java.io.File
* This only allows tests to be filtered with @Tag annotations even in the instrumentation test realm.
* Other plugin DSL settings, like includeEngines/excludeEngines or includePattern/excludePattern
* are not copied out to file. This has to do with limitations of the backport implementation
* of the JUnit Platform Runner, as well as some incompatibilities between Gradle and Java with regards to
* how class name patterns are formatted.
* of the JUnit Platform Runner, as well as some incompatibilities between Gradle and Java
* regarding how class name patterns are formatted.
*/
@CacheableTask
public abstract class AndroidJUnit5WriteFilters : DefaultTask() {

internal companion object {
@Suppress("UnstableApiUsage")
fun register(
project: Project,
variant: Variant,
instrumentationTestVariant: TestVariant
sourceDirs: SourceDirectories.Layered,
): Boolean {
val outputFolder = File("${project.buildDir}/generated/res/android-junit5/${instrumentationTestVariant.name}")
val configAction = ConfigAction(project, variant, outputFolder)
val configAction = ConfigAction(project, variant)

val provider = project.tasks.register(
configAction.name,
Expand All @@ -48,67 +48,66 @@ public abstract class AndroidJUnit5WriteFilters : DefaultTask() {

// Connect the output folder of the task to the instrumentation tests
// so that they are bundled into the built test application
instrumentationTestVariant.registerGeneratedResFolders(
project.files(outputFolder).builtBy(provider)
sourceDirs.addGeneratedSourceDirectory(
taskProvider = provider,
wiredWith = AndroidJUnit5WriteFilters::outputFolder,
)
instrumentationTestVariant.mergeResourcesProvider.configure { it.dependsOn(provider) }

return true
}
}

@Input
public var includeTags: List<String> = emptyList()
@get:Input
public abstract val includeTags: ListProperty<String>

@Input
public var excludeTags: List<String> = emptyList()
@get:Input
public abstract val excludeTags: ListProperty<String>

@OutputDirectory
public var outputFolder: File? = null
@get:OutputDirectory
public abstract val outputFolder: DirectoryProperty

@TaskAction
public fun execute() {
this.outputFolder?.let { folder ->
// Clear out current contents of the generated folder
folder.deleteRecursively()

if (this.hasFilters()) {
folder.mkdirs()

// Re-write the new file structure into it;
// the generated file will have a fixed name & is located
// as a "raw" resource inside the output folder
val rawFolder = File(folder, "raw").apply { mkdirs() }
File(rawFolder, INSTRUMENTATION_FILTER_RES_FILE_NAME)
.bufferedWriter()
.use { writer ->
// This format is a nod towards the real JUnit 5 ConsoleLauncher's arguments
includeTags.forEach { tag -> writer.write("-t $tag") }
excludeTags.forEach { tag -> writer.write("-T $tag") }
}
}
// Clear out current contents of the generated folder
val folder = outputFolder.get().asFile
folder.deleteRecursively()

val includeTags = includeTags.get()
val excludeTags = excludeTags.get()

if (includeTags.isNotEmpty() || excludeTags.isNotEmpty()) {
folder.mkdirs()

// Re-write the new file structure into it;
// the generated file will have a fixed name & is located
// as a "raw" resource inside the output folder
val rawFolder = File(folder, "raw").apply { mkdirs() }
File(rawFolder, INSTRUMENTATION_FILTER_RES_FILE_NAME)
.bufferedWriter()
.use { writer ->
// This format is a nod towards the real JUnit 5 ConsoleLauncher's arguments
includeTags.forEach { tag -> writer.appendLine("-t $tag") }
excludeTags.forEach { tag -> writer.appendLine("-T $tag") }
}
}
}

private fun hasFilters() = includeTags.isNotEmpty() || excludeTags.isNotEmpty()

private class ConfigAction(
private val project: Project,
private val variant: Variant,
private val outputFolder: File
) {

val name: String = variant.getTaskName(prefix = "writeFilters", suffix = "androidTest")

val type = AndroidJUnit5WriteFilters::class.java

fun execute(task: AndroidJUnit5WriteFilters) {
task.outputFolder = outputFolder

// Access filters for this particular variant & provide them to the task, too
// Access filters for this particular variant & provide them to the task
val configuration = JUnit5TaskConfig(variant, project.junitPlatform)
task.includeTags = configuration.combinedIncludeTags.toList()
task.excludeTags = configuration.combinedExcludeTags.toList()
task.includeTags.set(configuration.combinedIncludeTags.toList())
task.excludeTags.set(configuration.combinedExcludeTags.toList())

// Output folder is applied by Android Gradle Plugin, so there is no reason to provide a value ourselves
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ interface AgpInstrumentationSupportTests : AgpVariantAwareTests {
val task = project.tasks.get<AndroidJUnit5WriteFilters>("writeFiltersDebugAndroidTest")
assertAll(
{ assertThat(task).isNotNull() },
{ assertThat(task.includeTags).containsExactly("global-include-tag") },
{ assertThat(task.excludeTags).containsExactly("debug-exclude-tag") }
{ assertThat(task.includeTags.get()).containsExactly("global-include-tag") },
{ assertThat(task.excludeTags.get()).containsExactly("debug-exclude-tag") }
)
},

Expand Down Expand Up @@ -99,15 +99,15 @@ interface AgpInstrumentationSupportTests : AgpVariantAwareTests {
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).containsExactly("global-include-tag", "freeDebug-include-tag")
assertThat(task.excludeTags).containsExactly("global-exclude-tag")
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).containsExactly("global-include-tag")
assertThat(task.excludeTags).containsExactly("global-exclude-tag")
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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ class InstrumentationSupportTests {
assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary())
}

@Test
fun `register the filter-write tasks`() {
project.addJUnitJupiterApi()
project.evaluate()

// AGP only registers androidTest tasks for the debug build type
assertThat(project).task("writeFiltersDebugAndroidTest").exists()
assertThat(project).task("writeFiltersReleaseAndroidTest").doesNotExist()
}

/* Private */

private fun Project.addJUnitJupiterApi() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package de.mannodermaus.gradle.plugins.junit5.tasks

import com.google.common.truth.Truth.assertThat
import de.mannodermaus.gradle.plugins.junit5.internal.config.INSTRUMENTATION_FILTER_RES_FILE_NAME
import de.mannodermaus.gradle.plugins.junit5.plugin.TestProjectProviderExtension
import de.mannodermaus.gradle.plugins.junit5.util.assertAll
import de.mannodermaus.gradle.plugins.junit5.util.evaluate
import org.gradle.api.Project
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import java.io.File
import java.nio.file.Paths
import kotlin.io.path.readLines

class AndroidJUnit5WriteFiltersTests {
@RegisterExtension
@JvmField
val projectExtension = TestProjectProviderExtension()

private lateinit var project: Project

@BeforeEach
fun beforeEach() {
project = projectExtension.newProject()
.asAndroidApplication()
.applyJUnit5Plugin(true) { junitPlatform ->
junitPlatform.filters().includeTags("included")
junitPlatform.filters().excludeTags("excluded", "another-group")
}
.build()
project.evaluate()
}

@Test
fun `generates file structure correctly`() {
// Expect a 'raw' folder inside the output, then the actual filters file in that sub-folder
val output = project.runTaskAndGetOutputFolder()

File(output, "raw").apply {
assertAll(
"output contains 'raw' folder",
{ assertThat(exists()).isTrue() },
{ assertThat(isDirectory).isTrue() },
)

File(this, INSTRUMENTATION_FILTER_RES_FILE_NAME).apply {
assertAll(
"'raw' folder contains filters file'",
{ assertThat(exists()).isTrue() },
{ assertThat(isFile).isTrue() },
)
}
}
}

@Test
fun `file contains expected content`() {
val output = project.runTaskAndGetOutputFolder()
val file = Paths.get(output.absolutePath, "raw", INSTRUMENTATION_FILTER_RES_FILE_NAME)

val content = file.readLines()
assertThat(content).containsExactly(
"-t included",
"-T excluded",
"-T another-group",
)
}

/* Private */

private fun Project.runTaskAndGetOutputFolder(): File {
val task = project.tasks.getByName("writeFiltersDebugAndroidTest") as AndroidJUnit5WriteFilters
task.execute()
return requireNotNull(task.outputFolder.get().asFile)
}
}
Loading

0 comments on commit 81280fd

Please sign in to comment.