Skip to content

Commit 736cd68

Browse files
authored
Forcefully disable concurrent test execution for ActivityScenarioExtension tests, even if parallelism is otherwise enabled (#308)
1 parent 4ab6ba2 commit 736cd68

File tree

5 files changed

+69
-11
lines changed

5 files changed

+69
-11
lines changed

instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import android.annotation.TargetApi
44
import android.app.Activity
55
import android.content.Intent
66
import android.os.Build
7+
import android.util.Log
78
import androidx.test.core.app.ActivityScenario
89
import de.mannodermaus.junit5.ActivityScenarioExtension.Companion.launch
10+
import de.mannodermaus.junit5.internal.LOG_TAG
911
import org.junit.jupiter.api.extension.AfterEachCallback
1012
import org.junit.jupiter.api.extension.BeforeEachCallback
1113
import org.junit.jupiter.api.extension.ExtensionContext
@@ -14,6 +16,7 @@ import org.junit.jupiter.api.extension.ParameterResolver
1416
import org.junit.jupiter.api.extension.RegisterExtension
1517
import org.junit.jupiter.api.parallel.ExecutionMode
1618
import java.lang.reflect.ParameterizedType
19+
import java.util.concurrent.locks.ReentrantLock
1720

1821
/**
1922
* JUnit 5 Extension for the [ActivityScenario] API,
@@ -113,6 +116,11 @@ private constructor(private val scenarioSupplier: () -> ActivityScenario<A>) : B
113116
AfterEachCallback, ParameterResolver {
114117

115118
public companion object {
119+
private const val WARNING_KEY = "de.mannodermaus.junit5.LogConcurrentExecutionWarning"
120+
private const val LOCK_KEY = "de.mannodermaus.junit5.SharedResourceLock"
121+
122+
private val NAMESPACE =
123+
ExtensionContext.Namespace.create(ActivityScenarioExtension::class)
116124

117125
/**
118126
* Launches an activity of a given class and constructs an [ActivityScenario] for it.
@@ -151,23 +159,24 @@ private constructor(private val scenarioSupplier: () -> ActivityScenario<A>) : B
151159
public val scenario: ActivityScenario<A>
152160
get() = _scenario!!
153161

154-
/* Methods */
162+
/* BeforeEachCallback */
155163

156164
override fun beforeEach(context: ExtensionContext) {
157-
require(context.executionMode == ExecutionMode.SAME_THREAD) {
158-
"UI tests using ActivityScenarioExtension cannot be executed in ${context.executionMode} mode. " +
159-
"Please change it to ${ExecutionMode.SAME_THREAD}, e.g. via the @Execution annotation! " +
160-
"For more information, you can consult the JUnit 5 User Guide at " +
161-
"https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization."
162-
}
165+
context.acquireLock(true)
163166

164167
_scenario = scenarioSupplier()
165168
}
166169

170+
/* AfterEachCallback */
171+
167172
override fun afterEach(context: ExtensionContext) {
168173
scenario.close()
174+
175+
context.acquireLock(false)
169176
}
170177

178+
/* ParameterResolver */
179+
171180
override fun supportsParameter(
172181
parameterContext: ParameterContext,
173182
extensionContext: ExtensionContext
@@ -182,4 +191,50 @@ private constructor(private val scenarioSupplier: () -> ActivityScenario<A>) : B
182191
parameterContext: ParameterContext,
183192
extensionContext: ExtensionContext
184193
): Any = scenario
194+
195+
/* Private */
196+
197+
@Suppress("InconsistentCommentForJavaParameter")
198+
private fun ExtensionContext.acquireLock(state: Boolean) {
199+
// No need to do anything unless parallelism is enabled
200+
if (executionMode != ExecutionMode.CONCURRENT) {
201+
return
202+
}
203+
204+
val rootContext = this.root
205+
val store = rootContext.getStore(NAMESPACE)
206+
207+
logConcurrentExecutionWarningOnce(store)
208+
209+
// Create a global lock for restricting test execution to one-by-one;
210+
// this is necessary to ensure that only one ActivityScenario is ever active at a time,
211+
// preventing violations of Android's instrumentation and Espresso
212+
val lock = store.getOrComputeIfAbsent(
213+
/* key = */ LOCK_KEY,
214+
/* defaultCreator = */ { ReentrantLock() },
215+
/* requiredType = */ ReentrantLock::class.java,
216+
)
217+
218+
if (state) {
219+
lock.lock()
220+
} else {
221+
lock.unlock()
222+
}
223+
}
224+
225+
private fun logConcurrentExecutionWarningOnce(store: ExtensionContext.Store) {
226+
store.getOrComputeIfAbsent(WARNING_KEY) {
227+
setOf(
228+
" [WARNING!] UI tests using ActivityScenarioExtension should not be executed in CONCURRENT mode.",
229+
" We will try to disable parallelism for Espresso tests, but this may be error-prone",
230+
" (also, your execution times will look off). If you encounter issues, please consider",
231+
" annotating your Espresso test classes to use the SAME_THREAD mode via the @Execution annotation!",
232+
" --------------------------------------------------------------------",
233+
" For more information, feel free to consult the JUnit 5 User Guide at:",
234+
" https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization",
235+
).forEach { line ->
236+
Log.e(LOG_TAG, line)
237+
}
238+
}
239+
}
185240
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
package de.mannodermaus.junit5.internal
22

33
internal const val NOT_SET = -1
4+
internal const val LOG_TAG = "AndroidJUnit5"

instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/utils/BuildConfigValueUtils.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package de.mannodermaus.junit5.internal.utils
33
import android.annotation.SuppressLint
44
import android.util.Log
55
import androidx.test.platform.app.InstrumentationRegistry
6+
import de.mannodermaus.junit5.internal.LOG_TAG
67
import java.lang.reflect.Field
78

89
@SuppressLint("NewApi")
@@ -33,7 +34,7 @@ internal object BuildConfigValueUtils {
3334
try {
3435
Wrapper()
3536
} catch (t: Throwable) {
36-
Log.e("AndroidJUnit5", "Cannot initialize access to BuildConfig", t)
37+
Log.e(LOG_TAG, "Cannot initialize access to BuildConfig", t)
3738
null
3839
}
3940
}

instrumentation/sample/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ android {
2525
testInstrumentationRunnerArguments["configurationParameters"] = "junit.jupiter.execution.parallel.enabled=true,junit.jupiter.execution.parallel.mode.default=concurrent"
2626

2727
buildConfigField("boolean", "MY_VALUE", "true")
28+
29+
testOptions {
30+
animationsDisabled = true
31+
}
2832
}
2933

3034
// Add Kotlin source directory to all source sets

instrumentation/sample/src/androidTest/kotlin/de/mannodermaus/sample/ActivityOneTest.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@ import org.junit.jupiter.api.RepetitionInfo
1616
import org.junit.jupiter.api.Tag
1717
import org.junit.jupiter.api.Test
1818
import org.junit.jupiter.api.extension.RegisterExtension
19-
import org.junit.jupiter.api.parallel.Execution
20-
import org.junit.jupiter.api.parallel.ExecutionMode
2119
import org.junit.jupiter.params.ParameterizedTest
2220
import org.junit.jupiter.params.provider.ValueSource
2321

24-
@Execution(ExecutionMode.SAME_THREAD)
2522
class ActivityOneTest {
2623

2724
@JvmField

0 commit comments

Comments
 (0)