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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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