Skip to content

Commit

Permalink
Do not initialize non-mocked Dispatchers.Main unnecessarily
Browse files Browse the repository at this point in the history
Before this change, the following could happen on the JVM:
* `kotlinx.coroutines.test` accesses `Dispatchers.Main` before
  `setMain` is called.
* `Dispatchers.Main` attempts to initialize *some other* Main
  dispatcher in addition to the `kotlinx-coroutines-test` Main
  dispatcher, if there is one.
* If the `kotlinx-coroutines-android` artifact is present + its
  R8 rules are used to minify the tests, it's illegal to
  attempt to fail to create a `Main` dispatcher.
  `SUPPORT_MISSING = false` ensures that attempting to create a
  Main dispatcher fails immediately, in the call frame that
  attempts the creation, not waiting for `Dispatchers.Main` to
  be meaningfully used.

In total, `kotlinx-coroutines-test` + `kotlinx-coroutines-android`
+ R8 minification of tests leads to some patterns of
`kotlinx-coroutines-test` usage to crash.

Fixes #4297
  • Loading branch information
dkhalanskyjb committed Dec 16, 2024
1 parent 2cafea4 commit 1ac3943
Show file tree
Hide file tree
Showing 2 changed files with 14 additions and 10 deletions.
19 changes: 12 additions & 7 deletions kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,36 @@ import kotlin.coroutines.*
* The testable main dispatcher used by kotlinx-coroutines-test.
* It is a [MainCoroutineDispatcher] that delegates all actions to a settable delegate.
*/
internal class TestMainDispatcher(delegate: CoroutineDispatcher):
internal class TestMainDispatcher(createInnerMain: () -> CoroutineDispatcher):
MainCoroutineDispatcher(),
Delay
{
private val mainDispatcher = delegate
private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main")
internal constructor(delegate: CoroutineDispatcher): this({ delegate })

private val mainDispatcher by lazy(createInnerMain)
private var delegate = NonConcurrentlyModifiable<CoroutineDispatcher?>(null, "Dispatchers.Main")

private val delay
get() = delegate.value as? Delay ?: defaultDelay

override val immediate: MainCoroutineDispatcher
get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this

override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.value.dispatch(context, block)
override fun dispatch(context: CoroutineContext, block: Runnable) =
(delegate.value ?: mainDispatcher).dispatch(context, block)

override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context)
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
(delegate.value ?: mainDispatcher).isDispatchNeeded(context)

override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block)
override fun dispatchYield(context: CoroutineContext, block: Runnable) =
(delegate.value ?: mainDispatcher).dispatchYield(context, block)

fun setDispatcher(dispatcher: CoroutineDispatcher) {
delegate.value = dispatcher
}

fun resetDispatcher() {
delegate.value = mainDispatcher
delegate.value = null
}

override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ internal class TestMainDispatcherFactory : MainDispatcherFactory {
override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
val otherFactories = allFactories.filter { it !== this }
val secondBestFactory = otherFactories.maxByOrNull { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory
val dispatcher = secondBestFactory.tryCreateDispatcher(otherFactories)
return TestMainDispatcher(dispatcher)
return TestMainDispatcher({ secondBestFactory.tryCreateDispatcher(otherFactories) })
}

/**
Expand All @@ -24,4 +23,4 @@ internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher {
val mainDispatcher = Main
require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." }
return mainDispatcher
}
}

0 comments on commit 1ac3943

Please sign in to comment.