diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index ee4d8bfc09..8d53868b6e 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -249,6 +249,18 @@ public final class kotlinx/coroutines/DebugKt { public static final field DEBUG_PROPERTY_VALUE_ON Ljava/lang/String; } +public final class kotlinx/coroutines/DefaultExecutor : java/lang/Runnable { + public static final field INSTANCE Lkotlinx/coroutines/DefaultExecutor; + public static final field THREAD_NAME Ljava/lang/String; + public static synthetic fun decrementUseCount$default (Lkotlinx/coroutines/EventLoop;ZILjava/lang/Object;)V + public fun enqueue (Ljava/lang/Runnable;)V + public static synthetic fun incrementUseCount$default (Lkotlinx/coroutines/EventLoop;ZILjava/lang/Object;)V + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun run ()V + public fun shutdown ()V + public final fun shutdownForTests (J)V +} + public abstract interface class kotlinx/coroutines/Deferred : kotlinx/coroutines/Job { public abstract fun await (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getCompleted ()Ljava/lang/Object; diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt index da094e152d..e17833218f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt @@ -12,7 +12,6 @@ import kotlin.coroutines.* */ public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext -@PublishedApi @Suppress("PropertyName") internal expect val DefaultDelay: Delay diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index 95cb3c2964..7c4af9aad5 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -34,7 +34,7 @@ private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED && navigator.userAgent.match("\\bjsdom\\b") internal actual val DefaultDelay: Delay - get() = Dispatchers.Default as Delay + get() = Dispatchers.Main as? Delay ?: Dispatchers.Default as Delay public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { val combined = coroutineContext + context diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 3eec5408cc..1304c5a9e5 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -19,11 +19,6 @@ public actual object Dispatchers { internal fun injectMain(dispatcher: MainCoroutineDispatcher) { injectedMainDispatcher = dispatcher } - - @PublishedApi - internal fun resetInjectedMain() { - injectedMainDispatcher = null - } } private class JsMainDispatcher( diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index 7b0810c2a1..b679a99cb8 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -8,10 +8,10 @@ import kotlinx.coroutines.internal.* import java.util.concurrent.* import kotlin.coroutines.* -internal actual val DefaultDelay: Delay = initializeDefaultDelay() - private val defaultMainDelayOptIn = systemProp("kotlinx.coroutines.main.delay", true) +internal actual val DefaultDelay: Delay = initializeDefaultDelay() + private fun initializeDefaultDelay(): Delay { // Opt-out flag if (!defaultMainDelayOptIn) return DefaultExecutor @@ -21,10 +21,11 @@ private fun initializeDefaultDelay(): Delay { * no sense to create a separate thread with timer that cannot be controller * by the UI runtime. */ - return if (main.isMissing() || main !is Delay) DefaultExecutor else main + return if (main !is Delay) DefaultExecutor else main } @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +@PublishedApi internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { const val THREAD_NAME = "kotlinx.coroutines.DefaultExecutor" diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 2da633a6b6..bf7e0a104b 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -61,7 +61,12 @@ public fun MainDispatcherFactory.tryCreateDispatcher(factories: List) = + override fun dispatch(context: CoroutineContext, block: Runnable): Nothing = missing() private fun missing(): Nothing { @@ -124,6 +120,7 @@ private class MissingMainCoroutineDispatcher( * @suppress */ @InternalCoroutinesApi +@Deprecated("No longer used", level = DeprecationLevel.WARNING) // WARNING in 1.6, ERROR in 1.7, removed in 1.8 public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory { override val loadPriority: Int get() = -1 diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt index e1e29581a7..15f7b4c04c 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt @@ -41,7 +41,11 @@ internal actual object DefaultExecutor : CoroutineDispatcher(), Delay { internal expect fun createDefaultDispatcher(): CoroutineDispatcher @SharedImmutable -internal actual val DefaultDelay: Delay = if (multithreadingSupported) DefaultExecutor else OldDefaultExecutor +@PublishedApi +internal val NonMockedDefaultDelay: Delay = if (multithreadingSupported) DefaultExecutor else OldDefaultExecutor + +internal actual val DefaultDelay: Delay + get() = Dispatchers.Main as? Delay ?: NonMockedDefaultDelay public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { val combined = coroutineContext + context diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt index 6c51a03463..83c5304147 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.multithreadingSupported import kotlin.coroutines.* +import kotlin.native.concurrent.* public actual object Dispatchers { public actual val Default: CoroutineDispatcher = createDefaultDispatcherBasedOnMm() @@ -24,13 +25,15 @@ public actual object Dispatchers { } injectedMainDispatcher = dispatcher } - - @PublishedApi - internal fun resetInjectedMain() { - injectedMainDispatcher = null - } } +/** + * Does this platform define a Main dispatcher? + */ +@PublishedApi +@SharedImmutable +internal expect val mainDispatcherIsPresent: Boolean + internal expect fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher private fun createDefaultDispatcherBasedOnMm(): CoroutineDispatcher { diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt index ace20422f6..535451e60d 100644 --- a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt @@ -17,6 +17,9 @@ internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher = if (multithreadingSupported) DarwinMainDispatcher(false) else OldMainDispatcher(Dispatchers.Default) +@SharedImmutable +internal actual val mainDispatcherIsPresent: Boolean = true + internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DarwinGlobalQueueDispatcher private object DarwinGlobalQueueDispatcher : CoroutineDispatcher() { diff --git a/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt index 517190d0a3..2b42d25aea 100644 --- a/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt @@ -5,10 +5,14 @@ package kotlinx.coroutines import kotlin.coroutines.* +import kotlin.native.concurrent.* internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher = MissingMainDispatcher +@SharedImmutable +internal actual val mainDispatcherIsPresent: Boolean = false + internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DefaultDispatcher private object DefaultDispatcher : CoroutineDispatcher() { diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 91d2b07652..53aea58b1d 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -74,8 +74,12 @@ class SomeTest { Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. -If `Main` is overridden with a [TestDispatcher], then its [TestCoroutineScheduler] is used when new [TestDispatcher] or -[TestScope] instances are created without [TestCoroutineScheduler] being passed as an argument. +If `Main` is overridden with a [TestDispatcher], then +* its [TestCoroutineScheduler] is used when new [TestDispatcher] or [TestScope] instances are created without + [TestCoroutineScheduler] being passed as an argument. +* even the code running on other dispatchers, like `Dispatchers.IO` or `Dispatchers.Default`, + will run its delays using its [TestCoroutineScheduler]. + The only exception is the code running on dispatchers that implement `Delay`. ## runTest @@ -336,8 +340,9 @@ fun testFooWithTimeout() = runTest { ## Virtual time support with other dispatchers Calls to `withContext(Dispatchers.IO)`, `withContext(Dispatchers.Default)` ,and `withContext(Dispatchers.Main)` are -common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers -using the virtual time source, so delays will not be skipped in them. +common in coroutines-based code bases. +Unfortunately, just executing code in a test will not lead to these dispatchers using the virtual time source, so delays +will not be skipped in them. ```kotlin suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) { @@ -351,9 +356,13 @@ fun testExpensiveFunction() = runTest { } ``` -Tests should, when possible, replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the -function under test. For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using -either dependency injection, a service locator, or a default parameter, if it is to be used with virtual time. +To ensure that these delays are skipped, do one of the following: +* Call [Dispatchers.setMain] with a [TestDispatcher]. + Then, all delays that happen outside of dispatchers implementing `Delay` will use its [TestCoroutineScheduler]. +* Replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the + function under test. + For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using either dependency + injection, a service locator, or a default parameter, if it is to be used with virtual time. ### Status of the API diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt index e6d0c3970d..9b5d88a156 100644 --- a/kotlinx-coroutines-test/common/src/TestBuilders.kt +++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt @@ -8,6 +8,7 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlinx.coroutines.selects.* +import kotlinx.coroutines.test.internal.* import kotlin.coroutines.* import kotlin.jvm.* @@ -214,21 +215,24 @@ internal suspend fun > runTestCoroutine( completed = true continue } - select { - coroutine.onJoin { - completed = true - } - scheduler.onDispatchEvent { - // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout - } - onTimeout(dispatchTimeoutMs) { - try { - cleanup() - } catch (e: UncompletedCoroutinesError) { - // we expect these and will instead throw a more informative exception just below. - emptyList() - }.throwAll() - throw UncompletedCoroutinesError("The test coroutine was not completed after waiting for $dispatchTimeoutMs ms") + withContext(nonMockedDelay as ContinuationInterceptor) { + select { + coroutine.onJoin { + completed = true + } + scheduler.onDispatchEvent { + // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout + } + onTimeout(dispatchTimeoutMs) { + try { + cleanup() + } catch (e: UncompletedCoroutinesError) { + // we expect these and will instead throw a more informative exception just below. + emptyList() + }.throwAll() + throw UncompletedCoroutinesError( + "The test coroutine was not completed after waiting for $dispatchTimeoutMs ms") + } } } } diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt index 24e093be21..7b9cd79a71 100644 --- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt @@ -13,15 +13,15 @@ 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(delegate: CoroutineDispatcher?, mainInitException: Throwable? = null) : MainCoroutineDispatcher(), - Delay -{ - private val mainDispatcher = delegate + Delay { + private val mainDispatcher = delegate ?: UnsetMainDispatcher(mainInitException) + private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main") private val delay - get() = delegate.value as? Delay ?: defaultDelay + get() = delegate.value as? Delay ?: nonMockedDelay override val immediate: MainCoroutineDispatcher get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this @@ -30,7 +30,8 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher): override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block) + override fun dispatchYield(context: CoroutineContext, block: Runnable) = + delegate.value.dispatchYield(context, block) fun setDispatcher(dispatcher: CoroutineDispatcher) { delegate.value = dispatcher @@ -46,6 +47,8 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher): override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = delay.invokeOnTimeout(timeMillis, block, context) + override fun toString(): String = "TestMainDispatcher[delegate=${delegate.value}]" + companion object { internal val currentTestDispatcher get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher @@ -86,11 +89,25 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher): if (readers.value != 0) throw concurrentRW() } } + + private class UnsetMainDispatcher(private val mainInitException: Throwable?) : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher get() = this + override fun isDispatchNeeded(context: CoroutineContext): Boolean = missing() + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = missing() + override fun dispatch(context: CoroutineContext, block: Runnable) = missing() + + private fun missing(): Nothing { + val message = if (mainInitException == null) + "Dispatchers.Main is not available and was not provided for tests via Dispatchers.setMain." + else + "Dispatchers.Main failed to initialize and was not replaced for tests via Dispatchers.setMain." + throw IllegalStateException(message, mainInitException) + } + + override fun toString(): String = "missing" + } } -@Suppress("INVISIBLE_MEMBER") -private val defaultDelay - inline get() = DefaultDelay +internal expect val nonMockedDelay: Delay -@Suppress("INVISIBLE_MEMBER") -internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher +internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt index e063cdacf1..af6c3d1dcd 100644 --- a/kotlinx-coroutines-test/common/test/RunTestTest.kt +++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlinx.coroutines.selects.* import kotlin.coroutines.* import kotlin.test.* @@ -70,6 +71,7 @@ class RunTestTest { /** Tests that a dispatch timeout of `0` will fail the test if there are some dispatches outside the scheduler. */ @Test + @NoNative // the event loop was shut down fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn -> assertFailsWith { fn() } }) { @@ -105,6 +107,23 @@ class RunTestTest { } } + /** Tests that [onTimeout] executes quickly. */ + @Test + fun testOnTimeout() = runTest { + assertRunsFast { + val deferred = CompletableDeferred() + val result = select { + onTimeout(SLOW) { + true + } + deferred.onJoin { + fail("unreached") + } + } + assertTrue(result) + } + } + /** Tests uncaught exceptions taking priority over dispatch timeout in error reports. */ @Test @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native diff --git a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt index 66a6c24e8f..f07e501399 100644 --- a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt +++ b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt @@ -21,6 +21,52 @@ class TestDispatchersTest: OrderedExecutionTestBase() { Dispatchers.resetMain() } + /** Tests that replacing the dispatcher also replaces the default delay implementation. */ + @Test + @NoJs // AfterTest is asynchronous + fun testDefaultDelayReplacement() = runTest { + assertRunsFast { + delay(10_000) + withContext(Dispatchers.Default) { + delay(10_000) + } + delay(10_000) + } + } + + /** + * Tests that the dispatch timeout is not affected by the change to the delay implementation. + * + * In this case, it checks that there are no spurious notifications about dispatches. + */ + @Test + fun testRunTestWithMainAndSmallTimeout() = testResultMap({ fn -> + assertFailsWith { fn() } + }) { + runTest(dispatchTimeoutMs = 100) { + withContext(Dispatchers.Default) { + suspendCancellableCoroutine { + nonMockedDelay.scheduleResumeAfterDelay(5_000, it) + } + } + fail("shouldn't be reached") + } + } + + /** + * Tests that the dispatch timeout is not affected by the change to the delay implementation. + * + * In this case, it checks that the timeout does not happen immediately. + */ + @Test + fun testRunTestWithMainAndLangeTimeout() = runTest(dispatchTimeoutMs = 1_000) { + withContext(Dispatchers.Default) { + suspendCancellableCoroutine { + nonMockedDelay.scheduleResumeAfterDelay(50, it) + } + } + } + /** Tests that asynchronous execution of tests does not happen concurrently with [AfterTest]. */ @Test @NoJs diff --git a/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt index 4d865f83c0..11c9442083 100644 --- a/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/js/src/internal/TestMainDispatcher.kt @@ -5,6 +5,9 @@ package kotlinx.coroutines.test.internal import kotlinx.coroutines.* +@Suppress("INVISIBLE_MEMBER") +internal actual val nonMockedDelay: Delay = Dispatchers.Default as Delay + @Suppress("INVISIBLE_MEMBER") internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher = when (val mainDispatcher = Main) { diff --git a/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt index f86b08ea14..3333fd1b5a 100644 --- a/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt +++ b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt @@ -11,9 +11,12 @@ internal class TestMainDispatcherFactory : MainDispatcherFactory { override fun createDispatcher(allFactories: List): MainCoroutineDispatcher { val otherFactories = allFactories.filter { it !== this } - val secondBestFactory = otherFactories.maxByOrNull { it.loadPriority } ?: MissingMainCoroutineDispatcherFactory - val dispatcher = secondBestFactory.tryCreateDispatcher(otherFactories) - return TestMainDispatcher(dispatcher) + val secondBestFactory = otherFactories.maxByOrNull { it.loadPriority } + val main = secondBestFactory?.tryCreateDispatcher(otherFactories) + return when (val exception = main?.exceptionIfMissing()) { + null -> TestMainDispatcher(main) + else -> TestMainDispatcher(null, exception.cause) + } } /** @@ -24,6 +27,9 @@ internal class TestMainDispatcherFactory : MainDispatcherFactory { get() = Int.MAX_VALUE } +@Suppress("INVISIBLE_MEMBER") +internal actual val nonMockedDelay: Delay = DefaultExecutor + internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher { val mainDispatcher = Main require(mainDispatcher is TestMainDispatcher) { "TestMainDispatcher is not set as main dispatcher, have $mainDispatcher instead." } diff --git a/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt index 4d865f83c0..1b7cc0f6b5 100644 --- a/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt @@ -5,9 +5,12 @@ package kotlinx.coroutines.test.internal import kotlinx.coroutines.* +@Suppress("INVISIBLE_MEMBER") +internal actual val nonMockedDelay: Delay = NonMockedDefaultDelay + @Suppress("INVISIBLE_MEMBER") internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher = when (val mainDispatcher = Main) { is TestMainDispatcher -> mainDispatcher - else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) } + else -> TestMainDispatcher(if (mainDispatcherIsPresent) mainDispatcher else null).also { injectMain(it) } } diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstMockedMainTest.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstMockedMainTest.kt index c134ec5f89..337365636c 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstMockedMainTest.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/FirstMockedMainTest.kt @@ -38,7 +38,7 @@ open class FirstMockedMainTest : TestBase() { component.launchSomething() throw component.caughtException } catch (e: IllegalStateException) { - assertTrue(e.message!!.contains("Dispatchers.setMain from kotlinx-coroutines-test")) + assertTrue(e.message!!.contains("Dispatchers.setMain")) } } } diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt index 5128a74caf..3c40c04b1e 100644 --- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt @@ -161,4 +161,22 @@ class HandlerDispatcherTest : TestBase() { assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) } + + @Test + fun testDelayIsDelegatedToMain() = runTest { + val mainLooper = shadowOf(Looper.getMainLooper()) + mainLooper.pause() + val mainMessageQueue = shadowOf(Looper.getMainLooper().queue) + assertNull(mainMessageQueue.head) + val job = launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + expect(1) + delay(10_000_000) + expect(3) + } + expect(2) + assertNotNull(mainMessageQueue.head) + mainLooper.runOneTask() + job.join() + finish(4) + } }