@@ -4,8 +4,10 @@ import android.annotation.TargetApi
44import android.app.Activity
55import android.content.Intent
66import android.os.Build
7+ import android.util.Log
78import androidx.test.core.app.ActivityScenario
89import de.mannodermaus.junit5.ActivityScenarioExtension.Companion.launch
10+ import de.mannodermaus.junit5.internal.LOG_TAG
911import org.junit.jupiter.api.extension.AfterEachCallback
1012import org.junit.jupiter.api.extension.BeforeEachCallback
1113import org.junit.jupiter.api.extension.ExtensionContext
@@ -14,6 +16,7 @@ import org.junit.jupiter.api.extension.ParameterResolver
1416import org.junit.jupiter.api.extension.RegisterExtension
1517import org.junit.jupiter.api.parallel.ExecutionMode
1618import 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}
0 commit comments