Skip to content

Commit

Permalink
Remove need for Jupiter test discovery for API 26+ devices
Browse files Browse the repository at this point in the history
This should pave the road for supporting non-Jupiter test engines
for instrumentation tests
  • Loading branch information
mannodermaus committed May 5, 2024
1 parent 13ccb68 commit a8772ea
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 239 deletions.
1 change: 0 additions & 1 deletion instrumentation/runner/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ dependencies {

testImplementation(project(":testutil"))
testImplementation(libs.robolectric)
testRuntimeOnly(libs.junitVintageEngine)
testRuntimeOnly(libs.junitJupiterEngine)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package de.mannodermaus.junit5

import android.util.Log
import de.mannodermaus.junit5.internal.LOG_TAG
import de.mannodermaus.junit5.internal.LibcoreAccess
import de.mannodermaus.junit5.internal.runners.AndroidJUnit5RunnerParams
import de.mannodermaus.junit5.internal.runners.tryCreateJUnit5Runner
import org.junit.runner.Runner
import org.junit.runners.model.RunnerBuilder
Expand Down Expand Up @@ -56,11 +58,23 @@ public class AndroidJUnit5Builder : RunnerBuilder() {
}
}

// One-time parsing setup for runner params, taken from instrumentation arguments
private val params by lazy {
AndroidJUnit5RunnerParams.create().also { params ->
// Apply all environment variables & system properties to the running process
params.registerEnvironmentVariables()
params.registerSystemProperties()
}
}

@Throws(Throwable::class)
override fun runnerForClass(testClass: Class<*>): Runner? {
// Ignore a bunch of class in internal packages
if (testClass.isInIgnorablePackage) return null

try {
return if (junit5Available) {
tryCreateJUnit5Runner(testClass)
tryCreateJUnit5Runner(testClass) { params }
} else {
null
}
Expand All @@ -76,4 +90,38 @@ public class AndroidJUnit5Builder : RunnerBuilder() {
throw e
}
}

/* Private */

private val ignorablePackages = setOf(
"java.",
"javax.",
"androidx.",
"com.android.",
"kotlin.",
)

private val Class<*>.isInIgnorablePackage: Boolean get() {
return ignorablePackages.any { name.startsWith(it) }
}

private fun AndroidJUnit5RunnerParams.registerEnvironmentVariables() {
environmentVariables.forEach { (key, value) ->
try {
LibcoreAccess.setenv(key, value)
} catch (t: Throwable) {
Log.w(LOG_TAG, "Error while setting up environment variables.", t)
}
}
}

private fun AndroidJUnit5RunnerParams.registerSystemProperties() {
systemProperties.forEach { (key, value) ->
try {
System.setProperty(key, value)
} catch (t: Throwable) {
Log.w(LOG_TAG, "Error while setting up system properties.", t)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package de.mannodermaus.junit5.internal.dummy

import android.util.Log
import de.mannodermaus.junit5.internal.LOG_TAG
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.params.ParameterizedTest
import java.lang.reflect.Method
import java.lang.reflect.Modifier

/**
* Algorithm to find all methods annotated with a JUnit Jupiter annotation
* for devices running below API level 26 (i.e. those that cannot run Jupiter).
* We're unable to rely on JUnit Platform's own reflection utilities since they rely on Java 8 stuff
*/
internal object JupiterTestMethodFinder {
private val jupiterTestAnnotations = listOf(
Test::class.java,
TestFactory::class.java,
RepeatedTest::class.java,
TestTemplate::class.java,
ParameterizedTest::class.java,
)

fun find(cls: Class<*>): Set<Method> = cls.doFind(includeInherited = true)

private fun Class<*>.doFind(includeInherited: Boolean): Set<Method> = buildSet {
try {
// Check each method in the Class for the presence
// of the well-known list of JUnit Jupiter annotations.
addAll(declaredMethods.filter(::isApplicableMethod))

// Recursively check non-private inner classes as well
declaredClasses.filter(::isApplicableClass).forEach { inner ->
addAll(inner.doFind(includeInherited = false))
}

// Attach methods from inherited superclass or (for Java) implemented interfaces, too
if (includeInherited) {
addAll(superclass?.doFind(includeInherited = true).orEmpty())
interfaces.forEach { i -> addAll(i.doFind(includeInherited = true)) }
}
} catch (t: Throwable) {
Log.w(
LOG_TAG,
"Encountered ${t.javaClass.simpleName} while finding Jupiter test methods for ${this@doFind.name}",
t
)
}
}

private fun isApplicableMethod(method: Method): Boolean {
// The method must not be static...
if (Modifier.isStatic(method.modifiers)) return false

// ...and have at least one of the recognized JUnit 5 annotations
return hasJupiterAnnotation(method)
}

private fun hasJupiterAnnotation(method: Method): Boolean {
return jupiterTestAnnotations.any { method.getAnnotation(it) != null }
}

private fun isApplicableClass(cls: Class<*>): Boolean {
// A class must not be private to be considered
return !Modifier.isPrivate(cls.modifiers)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,16 @@ import org.junit.runner.notification.RunNotifier
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
internal class AndroidJUnit5(
private val testClass: Class<*>,
private val runnerParams: AndroidJUnit5RunnerParams = createRunnerParams(testClass),
paramsSupplier: () -> AndroidJUnit5RunnerParams = AndroidJUnit5RunnerParams.Companion::create,
) : Runner() {

private val launcher = LauncherFactory.create()
private val testTree by lazy { generateTestTree(runnerParams) }
private val testTree by lazy { generateTestTree(paramsSupplier()) }

override fun getDescription() =
testTree.suiteDescription

override fun run(notifier: RunNotifier) {
// Apply all environment variables & system properties to the running process
registerEnvironmentVariables()
registerSystemProperties()

// Finally, launch the test plan on the JUnit Platform
launcher.execute(
testTree.testPlan,
Expand All @@ -44,33 +40,16 @@ internal class AndroidJUnit5(

/* Private */

private fun registerEnvironmentVariables() {
runnerParams.environmentVariables.forEach { (key, value) ->
try {
LibcoreAccess.setenv(key, value)
} catch (t: Throwable) {
Log.w(LOG_TAG, "Error while setting up environment variables.", t)
}
}
}
private fun generateTestTree(params: AndroidJUnit5RunnerParams): AndroidJUnitPlatformTestTree {
val request = params.createDiscoveryRequest(testClass)

private fun registerSystemProperties() {
runnerParams.systemProperties.forEach { (key, value) ->
try {
System.setProperty(key, value)
} catch (t: Throwable) {
Log.w(LOG_TAG, "Error while setting up system properties.", t)
}
}
}

private fun generateTestTree(params: AndroidJUnit5RunnerParams) =
AndroidJUnitPlatformTestTree(
testPlan = launcher.discover(params.createDiscoveryRequest()),
return AndroidJUnitPlatformTestTree(
testPlan = launcher.discover(request),
testClass = testClass,
isIsolatedMethodRun = params.isIsolatedMethodRun,
isIsolatedMethodRun = request.isIsolatedMethodRun,
isParallelExecutionEnabled = params.isParallelExecutionEnabled,
)
}

private fun createNotifier(nextNotifier: RunNotifier) =
if (testTree.isParallelExecutionEnabled) {
Expand Down
Loading

0 comments on commit a8772ea

Please sign in to comment.