Skip to content

Commit 5fedf9a

Browse files
authored
Improve test discovery and align it with JUnit Platform (#335)
* Clean up some internal method names * Properly report disabled dynamic tests and containers to Android instrumentation * Remove need for Jupiter test discovery for API 26+ devices This should pave the road for supporting non-Jupiter test engines for instrumentation tests * Clean up comment in tests * Avoid AbstractMethodError in launcher discovery request * Changelog
1 parent 38ef72c commit 5fedf9a

File tree

15 files changed

+343
-269
lines changed

15 files changed

+343
-269
lines changed

instrumentation/runner/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ dependencies {
100100

101101
testImplementation(project(":testutil"))
102102
testImplementation(libs.robolectric)
103-
testRuntimeOnly(libs.junitVintageEngine)
104103
testRuntimeOnly(libs.junitJupiterEngine)
105104
}
106105

instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnit5Builder.kt

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package de.mannodermaus.junit5
22

33
import android.util.Log
44
import de.mannodermaus.junit5.internal.LOG_TAG
5+
import de.mannodermaus.junit5.internal.LibcoreAccess
6+
import de.mannodermaus.junit5.internal.runners.AndroidJUnit5RunnerParams
57
import de.mannodermaus.junit5.internal.runners.tryCreateJUnit5Runner
68
import org.junit.runner.Runner
79
import org.junit.runners.model.RunnerBuilder
@@ -56,11 +58,23 @@ public class AndroidJUnit5Builder : RunnerBuilder() {
5658
}
5759
}
5860

61+
// One-time parsing setup for runner params, taken from instrumentation arguments
62+
private val params by lazy {
63+
AndroidJUnit5RunnerParams.create().also { params ->
64+
// Apply all environment variables & system properties to the running process
65+
params.registerEnvironmentVariables()
66+
params.registerSystemProperties()
67+
}
68+
}
69+
5970
@Throws(Throwable::class)
6071
override fun runnerForClass(testClass: Class<*>): Runner? {
72+
// Ignore a bunch of class in internal packages
73+
if (testClass.isInIgnorablePackage) return null
74+
6175
try {
6276
return if (junit5Available) {
63-
tryCreateJUnit5Runner(testClass)
77+
tryCreateJUnit5Runner(testClass) { params }
6478
} else {
6579
null
6680
}
@@ -76,4 +90,38 @@ public class AndroidJUnit5Builder : RunnerBuilder() {
7690
throw e
7791
}
7892
}
93+
94+
/* Private */
95+
96+
private val ignorablePackages = setOf(
97+
"java.",
98+
"javax.",
99+
"androidx.",
100+
"com.android.",
101+
"kotlin.",
102+
)
103+
104+
private val Class<*>.isInIgnorablePackage: Boolean get() {
105+
return ignorablePackages.any { name.startsWith(it) }
106+
}
107+
108+
private fun AndroidJUnit5RunnerParams.registerEnvironmentVariables() {
109+
environmentVariables.forEach { (key, value) ->
110+
try {
111+
LibcoreAccess.setenv(key, value)
112+
} catch (t: Throwable) {
113+
Log.w(LOG_TAG, "Error while setting up environment variables.", t)
114+
}
115+
}
116+
}
117+
118+
private fun AndroidJUnit5RunnerParams.registerSystemProperties() {
119+
systemProperties.forEach { (key, value) ->
120+
try {
121+
System.setProperty(key, value)
122+
} catch (t: Throwable) {
123+
Log.w(LOG_TAG, "Error while setting up system properties.", t)
124+
}
125+
}
126+
}
79127
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package de.mannodermaus.junit5.internal.dummy
2+
3+
import android.util.Log
4+
import de.mannodermaus.junit5.internal.LOG_TAG
5+
import org.junit.jupiter.api.RepeatedTest
6+
import org.junit.jupiter.api.Test
7+
import org.junit.jupiter.api.TestFactory
8+
import org.junit.jupiter.api.TestTemplate
9+
import org.junit.jupiter.params.ParameterizedTest
10+
import java.lang.reflect.Method
11+
import java.lang.reflect.Modifier
12+
13+
/**
14+
* Algorithm to find all methods annotated with a JUnit Jupiter annotation
15+
* for devices running below API level 26 (i.e. those that cannot run Jupiter).
16+
* We're unable to rely on JUnit Platform's own reflection utilities since they rely on Java 8 stuff
17+
*/
18+
internal object JupiterTestMethodFinder {
19+
private val jupiterTestAnnotations = listOf(
20+
Test::class.java,
21+
TestFactory::class.java,
22+
RepeatedTest::class.java,
23+
TestTemplate::class.java,
24+
ParameterizedTest::class.java,
25+
)
26+
27+
fun find(cls: Class<*>): Set<Method> = cls.doFind(includeInherited = true)
28+
29+
private fun Class<*>.doFind(includeInherited: Boolean): Set<Method> = buildSet {
30+
try {
31+
// Check each method in the Class for the presence
32+
// of the well-known list of JUnit Jupiter annotations.
33+
addAll(declaredMethods.filter(::isApplicableMethod))
34+
35+
// Recursively check non-private inner classes as well
36+
declaredClasses.filter(::isApplicableClass).forEach { inner ->
37+
addAll(inner.doFind(includeInherited = false))
38+
}
39+
40+
// Attach methods from inherited superclass or (for Java) implemented interfaces, too
41+
if (includeInherited) {
42+
addAll(superclass?.doFind(includeInherited = true).orEmpty())
43+
interfaces.forEach { i -> addAll(i.doFind(includeInherited = true)) }
44+
}
45+
} catch (t: Throwable) {
46+
Log.w(
47+
LOG_TAG,
48+
"Encountered ${t.javaClass.simpleName} while finding Jupiter test methods for ${this@doFind.name}",
49+
t
50+
)
51+
}
52+
}
53+
54+
private fun isApplicableMethod(method: Method): Boolean {
55+
// The method must not be static...
56+
if (Modifier.isStatic(method.modifiers)) return false
57+
58+
// ...and have at least one of the recognized JUnit 5 annotations
59+
return hasJupiterAnnotation(method)
60+
}
61+
62+
private fun hasJupiterAnnotation(method: Method): Boolean {
63+
return jupiterTestAnnotations.any { method.getAnnotation(it) != null }
64+
}
65+
66+
private fun isApplicableClass(cls: Class<*>): Boolean {
67+
// A class must not be private to be considered
68+
return !Modifier.isPrivate(cls.modifiers)
69+
}
70+
}

instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/extensions/ClassExt.kt

Lines changed: 0 additions & 50 deletions
This file was deleted.

instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5.kt

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.annotation.VisibleForTesting
66
import de.mannodermaus.junit5.internal.LOG_TAG
77
import de.mannodermaus.junit5.internal.LibcoreAccess
88
import de.mannodermaus.junit5.internal.runners.notification.ParallelRunNotifier
9+
import org.junit.platform.engine.discovery.MethodSelector
910
import org.junit.platform.launcher.core.LauncherFactory
1011
import org.junit.runner.Runner
1112
import org.junit.runner.notification.RunNotifier
@@ -21,20 +22,16 @@ import org.junit.runner.notification.RunNotifier
2122
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
2223
internal class AndroidJUnit5(
2324
private val testClass: Class<*>,
24-
private val runnerParams: AndroidJUnit5RunnerParams = createRunnerParams(testClass),
25+
paramsSupplier: () -> AndroidJUnit5RunnerParams = AndroidJUnit5RunnerParams.Companion::create,
2526
) : Runner() {
2627

2728
private val launcher = LauncherFactory.create()
28-
private val testTree by lazy { generateTestTree(runnerParams) }
29+
private val testTree by lazy { generateTestTree(paramsSupplier()) }
2930

3031
override fun getDescription() =
3132
testTree.suiteDescription
3233

3334
override fun run(notifier: RunNotifier) {
34-
// Apply all environment variables & system properties to the running process
35-
registerEnvironmentVariables()
36-
registerSystemProperties()
37-
3835
// Finally, launch the test plan on the JUnit Platform
3936
launcher.execute(
4037
testTree.testPlan,
@@ -44,33 +41,18 @@ internal class AndroidJUnit5(
4441

4542
/* Private */
4643

47-
private fun registerEnvironmentVariables() {
48-
runnerParams.environmentVariables.forEach { (key, value) ->
49-
try {
50-
LibcoreAccess.setenv(key, value)
51-
} catch (t: Throwable) {
52-
Log.w(LOG_TAG, "Error while setting up environment variables.", t)
53-
}
54-
}
55-
}
44+
private fun generateTestTree(params: AndroidJUnit5RunnerParams): AndroidJUnitPlatformTestTree {
45+
val selectors = params.createSelectors(testClass)
46+
val isIsolatedMethodRun = selectors.size == 1 && selectors.first() is MethodSelector
47+
val request = params.createDiscoveryRequest(selectors)
5648

57-
private fun registerSystemProperties() {
58-
runnerParams.systemProperties.forEach { (key, value) ->
59-
try {
60-
System.setProperty(key, value)
61-
} catch (t: Throwable) {
62-
Log.w(LOG_TAG, "Error while setting up system properties.", t)
63-
}
64-
}
65-
}
66-
67-
private fun generateTestTree(params: AndroidJUnit5RunnerParams) =
68-
AndroidJUnitPlatformTestTree(
69-
testPlan = launcher.discover(params.createDiscoveryRequest()),
49+
return AndroidJUnitPlatformTestTree(
50+
testPlan = launcher.discover(request),
7051
testClass = testClass,
71-
isIsolatedMethodRun = params.isIsolatedMethodRun,
52+
isIsolatedMethodRun = isIsolatedMethodRun,
7253
isParallelExecutionEnabled = params.isParallelExecutionEnabled,
7354
)
55+
}
7456

7557
private fun createNotifier(nextNotifier: RunNotifier) =
7658
if (testTree.isParallelExecutionEnabled) {
Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,76 @@
11
package de.mannodermaus.junit5.internal.runners
22

3+
import android.os.Bundle
34
import androidx.test.platform.app.InstrumentationRegistry
45
import de.mannodermaus.junit5.internal.discovery.GeneratedFilters
56
import de.mannodermaus.junit5.internal.discovery.ParsedSelectors
67
import de.mannodermaus.junit5.internal.discovery.PropertiesParser
78
import de.mannodermaus.junit5.internal.discovery.ShardingFilter
89
import org.junit.platform.engine.DiscoverySelector
910
import org.junit.platform.engine.Filter
10-
import org.junit.platform.engine.discovery.MethodSelector
1111
import org.junit.platform.launcher.LauncherDiscoveryRequest
1212
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
1313

1414
internal data class AndroidJUnit5RunnerParams(
15-
private val selectors: List<DiscoverySelector> = emptyList(),
15+
private val arguments: Bundle = Bundle(),
1616
private val filters: List<Filter<*>> = emptyList(),
1717
val environmentVariables: Map<String, String> = emptyMap(),
1818
val systemProperties: Map<String, String> = emptyMap(),
1919
private val configurationParameters: Map<String, String> = emptyMap()
2020
) {
21+
fun createSelectors(testClass: Class<*>): List<DiscoverySelector> {
22+
return ParsedSelectors.fromBundle(testClass, arguments)
23+
}
2124

22-
fun createDiscoveryRequest(): LauncherDiscoveryRequest =
23-
LauncherDiscoveryRequestBuilder.request()
24-
.selectors(this.selectors)
25+
fun createDiscoveryRequest(selectors: List<DiscoverySelector>): LauncherDiscoveryRequest {
26+
return LauncherDiscoveryRequestBuilder.request()
27+
.selectors(selectors)
2528
.filters(*this.filters.toTypedArray())
2629
.configurationParameters(this.configurationParameters)
2730
.build()
28-
29-
val isIsolatedMethodRun: Boolean
30-
get() = selectors.size == 1 && selectors.first() is MethodSelector
31+
}
3132

3233
val isParallelExecutionEnabled: Boolean
3334
get() = configurationParameters["junit.jupiter.execution.parallel.enabled"] == "true"
34-
}
35-
36-
private const val ARG_ENVIRONMENT_VARIABLES = "environmentVariables"
37-
private const val ARG_SYSTEM_PROPERTIES = "systemProperties"
38-
private const val ARG_CONFIGURATION_PARAMETERS = "configurationParameters"
3935

40-
internal fun createRunnerParams(testClass: Class<*>): AndroidJUnit5RunnerParams {
41-
val instrumentation = InstrumentationRegistry.getInstrumentation()
42-
val arguments = InstrumentationRegistry.getArguments()
36+
internal companion object {
37+
fun create(): AndroidJUnit5RunnerParams {
38+
val instrumentation = InstrumentationRegistry.getInstrumentation()
39+
val arguments = InstrumentationRegistry.getArguments()
4340

44-
// Parse environment variables & pass them to the JVM
45-
val environmentVariables = arguments.getString(ARG_ENVIRONMENT_VARIABLES)
46-
?.run { PropertiesParser.fromString(this) }
47-
?: emptyMap()
41+
// Parse environment variables & pass them to the JVM
42+
val environmentVariables = arguments.getString(ARG_ENVIRONMENT_VARIABLES)
43+
?.run { PropertiesParser.fromString(this) }
44+
?: emptyMap()
4845

49-
// Parse system properties & pass them to the JVM
50-
val systemProperties = arguments.getString(ARG_SYSTEM_PROPERTIES)
51-
?.run { PropertiesParser.fromString(this) }
52-
?: emptyMap()
46+
// Parse system properties & pass them to the JVM
47+
val systemProperties = arguments.getString(ARG_SYSTEM_PROPERTIES)
48+
?.run { PropertiesParser.fromString(this) }
49+
?: emptyMap()
5350

54-
// Parse configuration parameters
55-
val configurationParameters = arguments.getString(ARG_CONFIGURATION_PARAMETERS)
56-
?.run { PropertiesParser.fromString(this) }
57-
?: emptyMap()
51+
// Parse configuration parameters
52+
val configurationParameters = arguments.getString(ARG_CONFIGURATION_PARAMETERS)
53+
?.run { PropertiesParser.fromString(this) }
54+
?: emptyMap()
5855

59-
// Parse the selectors to use from what's handed to the runner.
60-
val selectors = ParsedSelectors.fromBundle(testClass, arguments)
56+
// The user may apply test filters to their instrumentation tests through the Gradle plugin's DSL,
57+
// which aren't subject to the filtering imposed through adb.
58+
// A special resource file may be looked up at runtime, containing
59+
// the filters to apply by the AndroidJUnit5 runner.
60+
val filters = GeneratedFilters.fromContext(instrumentation.context) +
61+
listOfNotNull(ShardingFilter.fromArguments(arguments))
6162

62-
// The user may apply test filters to their instrumentation tests through the Gradle plugin's DSL,
63-
// which aren't subject to the filtering imposed through adb.
64-
// A special resource file may be looked up at runtime, containing
65-
// the filters to apply by the AndroidJUnit5 runner.
66-
val filters = GeneratedFilters.fromContext(instrumentation.context) +
67-
listOfNotNull(ShardingFilter.fromArguments(arguments))
68-
69-
return AndroidJUnit5RunnerParams(
70-
selectors,
71-
filters,
72-
environmentVariables,
73-
systemProperties,
74-
configurationParameters
75-
)
63+
return AndroidJUnit5RunnerParams(
64+
arguments,
65+
filters,
66+
environmentVariables,
67+
systemProperties,
68+
configurationParameters
69+
)
70+
}
71+
}
7672
}
73+
74+
private const val ARG_ENVIRONMENT_VARIABLES = "environmentVariables"
75+
private const val ARG_SYSTEM_PROPERTIES = "systemProperties"
76+
private const val ARG_CONFIGURATION_PARAMETERS = "configurationParameters"

0 commit comments

Comments
 (0)