Skip to content

Commit 621b4cc

Browse files
authored
Rework instrumentation filtering (#414)
* Remove dependency on suiteapi * Rework test tree pre-filtering The Android instrumentation throws all classes in the classpath at the RunnerBuilder and we must eliminate all classes that aren't relevant to JUnit Framework. Historically, each class received at least a TestTree and then the runtime decided to discard when no relevant test method is in the plan. Now, the tree is not generated in the first place. This avoids any need to look up (meta-)annotations on the test class, which in turn could raise errors in certain environments (ref #413). * Tweak local project dependency configurator * AGP 9.3.3, 9.4.0, 9.5.0 Alpha 6 * CI go brrr
1 parent 1fa3e6a commit 621b4cc

11 files changed

Lines changed: 57 additions & 71 deletions

File tree

‎.circleci/config.yml‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ defaults: &defaults
33
working_directory: ~/root/project
44
resource_class: large
55
docker:
6-
- image: cimg/android:2025.12.1
6+
- image: cimg/android:2026.08.1
77
environment:
8-
GRADLE_OPTS: -Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.daemon=false -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false
8+
GRADLE_OPTS: >-
9+
-Xmx4096m
10+
-XX:+HeapDumpOnOutOfMemoryError
11+
-Dorg.gradle.caching=true
12+
-Dorg.gradle.configureondemand=true
13+
-Dorg.gradle.workers.max=2
14+
-Dkotlin.compiler.execution.strategy=in-process
915
1016
cache_key: &cache_key
1117
key: jars-{{ checksum "build-logic/build.gradle.kts" }}-{{ checksum "plugin/build.gradle.kts" }}-{{ checksum "plugin/android-junit5/build.gradle.kts" }}-{{ checksum "plugin/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "instrumentation/build.gradle.kts" }}-{{ checksum "instrumentation/core/build.gradle.kts" }}-{{ checksum "instrumentation/compose/build.gradle.kts" }}-{{ checksum "instrumentation/extensions/build.gradle.kts" }}-{{ checksum "instrumentation/runner/build.gradle.kts" }}-{{ checksum "instrumentation/sample/build.gradle.kts" }}-{{ checksum "instrumentation/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "build-logic/src/main/kotlin/Environment.kt" }}-{{ checksum "build-logic/gradle/libs.versions.toml" }}
@@ -30,15 +36,6 @@ jobs:
3036
- run:
3137
name: (Plugin) Test
3238
command: cd plugin && ./gradlew :build-logic:test :android-junit5:check --stacktrace
33-
- run:
34-
name: (Instrumentation) Download Dependencies
35-
command: >
36-
cd instrumentation && ./gradlew \
37-
:core:dependencies \
38-
:compose:dependencies \
39-
:extensions:dependencies \
40-
:runner:dependencies \
41-
:testutil:dependencies
4239
- run:
4340
name: (Instrumentation) Build
4441
command: cd instrumentation && ./gradlew assemble :core:assembleAndroidTest --stacktrace

‎build-logic/gradle/libs.versions.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" }
7070
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
7171
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" }
7272
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
73-
junit-platform-suiteapi = { module = "org.junit.platform:junit-platform-suite-api" }
7473
junit-vintage-api = { module = "junit:junit", version.ref = "junit4" }
7574
junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" }
7675
kgp = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

‎build-logic/src/main/kotlin/Environment.kt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ enum class SupportedAgp(
3838
AGP_9_0("9.0.1", gradle = "9.1.0"),
3939
AGP_9_1("9.1.1", gradle = "9.3.1"),
4040
AGP_9_2("9.2.1", gradle = "9.4.1"),
41-
AGP_9_3("9.3.0-rc01", gradle = "9.5.0"),
42-
AGP_9_4("9.4.0-alpha01", gradle = "9.5.0"),
41+
AGP_9_3("9.3.3", gradle = "9.5.0"),
42+
AGP_9_4("9.4.0", gradle = "9.6.0"),
43+
AGP_9_5("9.5.0-alpha06", gradle = "9.6.0"),
4344
;
4445

4546
companion object {

‎instrumentation/buildSrc/src/main/java/LocalProjectDependencyConfigurator.kt‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import extensions.library
22
import extensions.libs
33
import org.gradle.api.Project
44

5+
private val applicableConfigurationNameRegex =
6+
Regex("[a-z]+[A-Z][a-z]+((Android)|(Unit))Test[A-Z][a-z]+Classpath")
7+
58
private val instrumentationLibraryRegex =
69
Regex("de\\.mannodermaus\\.junit5:android-test-([a-z0-9]+)(-.+)?:")
710

811
fun Project.replaceAndroidTestLibsWithLocalProjectDependencies() {
912
val self = this
1013

1114
configurations.all {
12-
if ("DebugAndroidTestRuntimeClasspath" !in name) {
15+
val configuration = this
16+
17+
if (!name.matches(applicableConfigurationNameRegex)) {
1318
return@all
1419
}
1520

@@ -32,8 +37,9 @@ fun Project.replaceAndroidTestLibsWithLocalProjectDependencies() {
3237
dependencySubstitution.all {
3338
instrumentationLibraryRegex.find(requested.toString())?.let { result ->
3439
val replacement = project(":${result.groupValues[1]}")
35-
println(
36-
"In $self, replace androidTest dependency '$requested' with $replacement"
40+
logger.lifecycle(
41+
"$self will replace dependency on '$requested' " +
42+
"with $replacement in $configuration"
3743
)
3844
useTarget(replacement, "Use $replacement to substitute dependency '$requested'")
3945
}

‎instrumentation/core/build.gradle.kts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ dependencies {
3030
// since it can't provide any JUnit 5 runtime libraries itself
3131
// due to fear of prematurely incrementing the minSdkVersion requirement.
3232
runtimeOnly(libs.junit.platform.launcher)
33-
runtimeOnly(libs.junit.platform.suiteapi)
3433
runtimeOnly(libs.junit.jupiter.engine)
3534

3635
// This transitive dependency of JUnit 5 is required to be on the runtime classpath,

‎instrumentation/runner/build.gradle.kts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ dependencies {
3333
compileOnly(libs.junit.jupiter.api)
3434
compileOnly(libs.junit.jupiter.params)
3535
compileOnly(libs.junit.platform.launcher)
36-
compileOnly(libs.junit.platform.suiteapi)
3736

3837
testImplementation(project(":testutil"))
3938
testImplementation(libs.robolectric)

‎instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilder.kt‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ public open class AndroidJUnitFrameworkBuilder internal constructor() : RunnerBu
5858

5959
@Throws(Throwable::class)
6060
override fun runnerForClass(testClass: Class<*>): Runner? {
61-
// Ignore a bunch of classes in internal packages
62-
if (testClass.isInIgnorablePackage) return null
63-
6461
try {
6562
return if (junitFrameworkAvailable) {
6663
tryCreateJUnitFrameworkRunner(testClass, params)
@@ -82,14 +79,6 @@ public open class AndroidJUnitFrameworkBuilder internal constructor() : RunnerBu
8279

8380
/* Private */
8481

85-
private val ignorablePackages =
86-
setOf("java.", "javax.", "androidx.", "com.android.", "kotlin.", "kotlinx.")
87-
88-
private val Class<*>.isInIgnorablePackage: Boolean
89-
get() {
90-
return ignorablePackages.any { name.startsWith(it) }
91-
}
92-
9382
private fun JUnitFrameworkRunnerParams.registerEnvironmentVariables() {
9483
environmentVariables.forEach { (key, value) ->
9584
try {

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

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import de.mannodermaus.junit5.internal.runners.notification.ParallelRunNotifier
77
import org.junit.platform.commons.JUnitException
88
import org.junit.platform.engine.discovery.MethodSelector
99
import org.junit.platform.launcher.core.LauncherFactory
10+
import org.junit.runner.Description
1011
import org.junit.runner.Runner
1112
import org.junit.runner.notification.RunNotifier
1213

@@ -20,22 +21,33 @@ internal class AndroidJUnitFramework(
2021
private val testClass: Class<*>,
2122
params: JUnitFrameworkRunnerParams,
2223
) : Runner() {
23-
private val launcher = LauncherFactory.create()
24+
private companion object {
25+
private val launcher = LauncherFactory.create()
26+
27+
// Fallback for irrelevant classes passed to JUnit 4's RunnerBuilder
28+
// (no test tree will be created for those, avoiding any potentially dangerous
29+
// runtime lookups that can cause issues like `mannodermaus/android-junit-framework/413`)
30+
private val emptyDescription = Description.createSuiteDescription("<empty>")
31+
}
32+
2433
private val testTree by lazy { generateTestTree(params) }
2534

26-
override fun getDescription() = testTree.suiteDescription
35+
override fun getDescription(): Description = testTree?.suiteDescription ?: emptyDescription
2736

2837
override fun run(notifier: RunNotifier) {
29-
// Finally, launch the test plan on the JUnit Platform
30-
launcher.execute(
31-
testTree.testPlan,
32-
AndroidJUnitPlatformRunnerListener(testTree, createNotifier(notifier)),
33-
)
38+
testTree?.let { tree ->
39+
launcher.execute(
40+
tree.testPlan,
41+
AndroidJUnitPlatformRunnerListener(tree, tree.createNotifier(notifier)),
42+
)
43+
}
3444
}
3545

3646
/* Private */
3747

38-
private fun generateTestTree(params: JUnitFrameworkRunnerParams): AndroidJUnitPlatformTestTree {
48+
private fun generateTestTree(
49+
params: JUnitFrameworkRunnerParams
50+
): AndroidJUnitPlatformTestTree? {
3951
val selectors = params.createSelectors(testClass)
4052
val isIsolatedMethodRun = selectors.size == 1 && selectors.first() is MethodSelector
4153
val isUsingOrchestrator = params.isUsingOrchestrator
@@ -67,16 +79,20 @@ internal class AndroidJUnitFramework(
6779
EmptyTestPlan
6880
}
6981

70-
return AndroidJUnitPlatformTestTree(
71-
testPlan = testPlan,
72-
testClass = testClass,
73-
needLegacyFormat = isIsolatedMethodRun || isUsingOrchestrator,
74-
isParallelExecutionEnabled = params.isParallelExecutionEnabled,
75-
)
82+
return if (testPlan.containsTests()) {
83+
AndroidJUnitPlatformTestTree(
84+
testPlan = testPlan,
85+
testClass = testClass,
86+
needLegacyFormat = isIsolatedMethodRun || isUsingOrchestrator,
87+
isParallelExecutionEnabled = params.isParallelExecutionEnabled,
88+
)
89+
} else {
90+
null
91+
}
7692
}
7793

78-
private fun createNotifier(nextNotifier: RunNotifier) =
79-
if (testTree.isParallelExecutionEnabled) {
94+
private fun AndroidJUnitPlatformTestTree.createNotifier(nextNotifier: RunNotifier) =
95+
if (isParallelExecutionEnabled) {
8096
// Wrap the default notifier with a special handler for parallel test execution
8197
ParallelRunNotifier(nextNotifier)
8298
} else {

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ import de.mannodermaus.junit5.internal.extensions.format
77
import de.mannodermaus.junit5.internal.extensions.isDynamicTest
88
import java.util.Optional
99
import java.util.function.Predicate
10-
import org.junit.platform.commons.util.AnnotationUtils
1110
import org.junit.platform.engine.UniqueId
1211
import org.junit.platform.engine.support.descriptor.ClassSource
1312
import org.junit.platform.engine.support.descriptor.MethodSource
1413
import org.junit.platform.launcher.TestIdentifier
1514
import org.junit.platform.launcher.TestPlan
16-
import org.junit.platform.suite.api.SuiteDisplayName
1715
import org.junit.runner.Description
1816

1917
/**
@@ -82,17 +80,10 @@ internal class AndroidJUnitPlatformTestTree(
8280
return descriptions.getValue(identifier)
8381
}
8482

85-
private fun generateSuiteDescription(testPlan: TestPlan, testClass: Class<*>): Description {
86-
return Description.createSuiteDescription(getSuiteDisplayName(testClass)).also {
83+
private fun generateSuiteDescription(testPlan: TestPlan, testClass: Class<*>): Description =
84+
Description.createSuiteDescription(testClass.name).also {
8785
buildDescriptionTree(it, testPlan)
8886
}
89-
}
90-
91-
private fun getSuiteDisplayName(testClass: Class<*>): String =
92-
AnnotationUtils.findAnnotation(testClass, SuiteDisplayName::class.java)
93-
.map(SuiteDisplayName::value)
94-
.filter(String::isNotBlank)
95-
.orElse(testClass.name)
9687

9788
private fun buildDescriptionTree(suiteDescription: Description, testPlan: TestPlan) {
9889
testPlan.roots.forEach { identifier ->

‎instrumentation/testutil/build.gradle.kts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,4 @@ dependencies {
2424
api(libs.junit.jupiter.api)
2525
api(libs.junit.jupiter.params)
2626
api(libs.junit.platform.launcher)
27-
api(libs.junit.platform.suiteapi)
2827
}

0 commit comments

Comments
 (0)