I've been trying to nail down a particularly nasty crash during my tests that occur when setting up/using Dispatchers.Main
value. I see this issue about once every 10 or so runs.
Example stack trace:
java.lang.IllegalStateException: Dispatchers.Main is used concurrently with setting it
at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.concurrentRW(TestMainDispatcher.kt:71)
at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.setValue(TestMainDispatcher.kt:89)
at kotlinx.coroutines.test.internal.TestMainDispatcher.setDispatcher(TestMainDispatcher.kt:36)
at kotlinx.coroutines.test.TestDispatchers.setMain(TestDispatchers.kt:24)
at com.zao.testcommon.CoroutineTestRule.starting(CoroutineTestRule.kt:20)
at org.junit.rules.TestWatcher.startingQuietly(TestWatcher.java:113)
at org.junit.rules.TestWatcher.access$000(TestWatcher.java:52)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:59)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:67)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:58)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:446)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2205)
Caused by: java.lang.Throwable: reader location
at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.getValue(TestMainDispatcher.kt:75)
at kotlinx.coroutines.test.internal.TestMainDispatcher.dispatch(TestMainDispatcher.kt:29)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:320)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith$default(DispatchedContinuation.kt:276)
at kotlinx.coroutines.internal.ScopeCoroutine.afterCompletion(Scopes.kt:27)
at kotlinx.coroutines.JobSupport.continueCompleting(JobSupport.kt:939)
at kotlinx.coroutines.JobSupport.access$continueCompleting(JobSupport.kt:25)
at kotlinx.coroutines.JobSupport$ChildCompletion.invoke(JobSupport.kt:1158)
at kotlinx.coroutines.JobSupport.notifyCompletion(JobSupport.kt:1494)
at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:324)
at kotlinx.coroutines.JobSupport.finalizeFinishingState(JobSupport.kt:241)
at kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath(JobSupport.kt:909)
at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:866)
at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:831)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:100)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
I use a standard CoroutineTestRule
:
class CoroutineTestRule : TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
val testDispatcher = StandardTestDispatcher()
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
And each test sets this up via:
@get:Rule
val coroutineTestRule = CoroutineTestRule()
In the stacktrace when the crash occurs, I see that the CoroutineTestRule
is one of the two places that are accessing the Dispatchers.Main
. A test (not sure which?) is starting, and we need to setup the dispatchers... but I can't seem to figure out the second thread that is using the Dispatchers.Main
value... it seems related to a coroutine resuming (from BaseContinuationImpl.resumeWith
and other methods named here), but I can't understand why...
My instrumentation tests use a combination of activity/fragment scenarios, etc. (all standard practice stuff), but shouldn't these all "die down" by the time the next test is starting?
The solve for this was simple, but time consuming.
I have multiple modules in my app, and while tests running with a given module are synchronous, multiple modules can be running tests at the same time.
Dispatchers.setMain
/ Dispatchers.resetMain
works on a singleton... so when you are running multiple module tests at once, there's a chance the test dispatcher could be set while it's currently being used as the error points out.
To fix this, you simply cannot use the test dispatcher APIs like this. This means you cannot use viewModelScope
which is bound to Dispatchers.Main.immediate
, and instead need to create a custom coroutine scope for your viewmodel classes (and everything else hard coded against Dispatchers.Main
.
It's relatively easy to do this - but just time consuming and unfortunate that the test APIs are setup like this... :sigh:
edit 5/16/24: With the release of Lifecycle 2.8.0, you can use the built in viewModelScope
value in the view model classes, but you must pass in your own custom scope to the constructor of the view model. See https://developer.android.com/jetpack/androidx/releases/lifecycle#2.8.0 where it says:
ViewModel.viewModelScope is now an overridable constructor parameter, allowing you to inject your own dispatcher and SupervisorJob() or to override the default by using the backgroundScope available within runTest.
That way you can create your own scope, pass that in for tests/for implementation/etc as needed and still not need to rely on the Dispatchers.setMain
logic.