From 402006a082a8f66d4b4847d8a0f0c277f9f6ca24 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 24 Nov 2021 15:54:40 +0300 Subject: [PATCH 1/7] Fix reading an uninitialized value of defaultMainDelayOptIn due to initialization cycle and add a test that ensures this behaviour in the future Fixes #3044 --- .../jvm/src/DefaultExecutor.kt | 4 ++-- .../test/HandlerDispatcherTest.kt | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index 7b0810c2a1..1b14748041 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 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) + } } From 7d1cd381f84f5084d85b18ec026faac8a03297fb Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 25 Nov 2021 11:28:38 +0300 Subject: [PATCH 2/7] Make Dispatchers.setMain affect all delays in tests. --- .../api/kotlinx-coroutines-core.api | 12 +++++ .../common/src/CoroutineContext.common.kt | 1 - .../js/src/CoroutineContext.kt | 2 +- kotlinx-coroutines-core/js/src/Dispatchers.kt | 5 -- .../jvm/src/DefaultExecutor.kt | 3 +- .../jvm/src/internal/MainDispatchers.kt | 12 +---- ...utineDispatcherWorkSignallingStressTest.kt | 1 + .../native/src/CoroutineContext.kt | 6 ++- .../native/src/Dispatchers.kt | 13 +++-- .../nativeDarwin/src/Dispatchers.kt | 3 ++ .../nativeOther/src/Dispatchers.kt | 4 ++ kotlinx-coroutines-test/README.md | 23 ++++++--- .../common/src/TestBuilders.kt | 34 ++++++++------ .../common/src/internal/TestMainDispatcher.kt | 47 +++++++++++++------ .../common/test/RunTestTest.kt | 19 ++++++++ .../common/test/TestDispatchersTest.kt | 46 ++++++++++++++++++ .../js/src/internal/TestMainDispatcher.kt | 3 ++ .../jvm/src/internal/TestMainDispatcherJvm.kt | 11 ++++- .../native/src/internal/TestMainDispatcher.kt | 5 +- 19 files changed, 186 insertions(+), 64 deletions(-) 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 1b14748041..b679a99cb8 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -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..6575b3e171 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -86,7 +86,7 @@ internal fun throwMissingMainDispatcherException(): Nothing { private class MissingMainCoroutineDispatcher( private val cause: Throwable?, private val errorHint: String? = null -) : MainCoroutineDispatcher(), Delay { +) : MainCoroutineDispatcher() { override val immediate: MainCoroutineDispatcher get() = this @@ -96,18 +96,9 @@ private class MissingMainCoroutineDispatcher( override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = missing() - override suspend fun delay(time: Long) = - missing() - - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = - missing() - override fun dispatch(context: CoroutineContext, block: Runnable) = missing() - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = - missing() - private fun missing(): Nothing { if (cause == null) { throwMissingMainDispatcherException() @@ -124,6 +115,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/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt index 3b3e085047..6fa7443415 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt @@ -11,6 +11,7 @@ import org.junit.Test import java.util.concurrent.* import kotlin.test.* +@Ignore class BlockingCoroutineDispatcherWorkSignallingStressTest : SchedulerTestBase() { @Test 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..d8bf62ff87 100644 --- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt @@ -13,31 +13,35 @@ 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?): MainCoroutineDispatcher(), Delay { private val mainDispatcher = delegate - private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main") + + private var _delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main") + + private val delegate + get() = _delegate.value ?: UnsetMainDispatcher private val delay - get() = delegate.value as? Delay ?: defaultDelay + get() = delegate as? Delay ?: nonMockedDelay override val immediate: MainCoroutineDispatcher - get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this + get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this - override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.value.dispatch(context, block) + override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context) + override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block) + override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) fun setDispatcher(dispatcher: CoroutineDispatcher) { - delegate.value = dispatcher + _delegate.value = dispatcher } fun resetDispatcher() { - delegate.value = mainDispatcher + _delegate.value = mainDispatcher } override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = @@ -46,9 +50,11 @@ 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]" + companion object { internal val currentTestDispatcher - get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher + get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate as? TestDispatcher internal val currentTestScheduler get() = currentTestDispatcher?.scheduler @@ -86,11 +92,22 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher): if (readers.value != 0) throw concurrentRW() } } + + private object UnsetMainDispatcher : 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 = + throw IllegalStateException( + "Dispatchers.Main is not available was not provided for tests via Dispatchers.setMain." + ) + + 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..4d4abe0185 100644 --- a/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt +++ b/kotlinx-coroutines-test/jvm/src/internal/TestMainDispatcherJvm.kt @@ -11,8 +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) + val secondBestFactory = otherFactories.maxByOrNull { it.loadPriority } + val main = secondBestFactory?.tryCreateDispatcher(otherFactories) + val dispatcher = when { + main?.isMissing() ?: true -> null + else -> main + } return TestMainDispatcher(dispatcher) } @@ -24,6 +28,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) } } From e116c71e6e76fcb92dac1e684c3b6a3deb3ab54b Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 25 Nov 2021 11:55:10 +0300 Subject: [PATCH 3/7] Preserve the exception with which Main failed to init --- .../jvm/src/internal/MainDispatchers.kt | 13 ++++--- .../common/src/internal/TestMainDispatcher.kt | 36 +++++++++---------- .../jvm/src/internal/TestMainDispatcherJvm.kt | 7 ++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 6575b3e171..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 null - else -> main + return when (val exception = main?.exceptionIfMissing()) { + null -> TestMainDispatcher(main) + else -> TestMainDispatcher(null, exception.cause) } - return TestMainDispatcher(dispatcher) } /** From 849ff5a37f080fa56aa13a28000aee720fc836d3 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 25 Nov 2021 11:56:08 +0300 Subject: [PATCH 4/7] Fixup --- .../common/src/internal/TestMainDispatcher.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt index 62e51165b4..f39e1afbae 100644 --- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt @@ -18,27 +18,27 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher?, mainInitExcept Delay { private val mainDispatcher = delegate ?: UnsetMainDispatcher(mainInitException) - private var _delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main") + private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main") private val delay - get() = _delegate.value as? Delay ?: nonMockedDelay + get() = delegate.value as? Delay ?: nonMockedDelay override val immediate: MainCoroutineDispatcher - get() = (_delegate.value as? MainCoroutineDispatcher)?.immediate ?: this + 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.dispatch(context, block) - override fun isDispatchNeeded(context: CoroutineContext): Boolean = _delegate.value.isDispatchNeeded(context) + override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context) override fun dispatchYield(context: CoroutineContext, block: Runnable) = - _delegate.value.dispatchYield(context, block) + delegate.value.dispatchYield(context, block) fun setDispatcher(dispatcher: CoroutineDispatcher) { - _delegate.value = dispatcher + delegate.value = dispatcher } fun resetDispatcher() { - _delegate.value = mainDispatcher + delegate.value = mainDispatcher } override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = @@ -47,11 +47,11 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher?, mainInitExcept override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = delay.invokeOnTimeout(timeMillis, block, context) - override fun toString(): String = "TestMainDispatcher[delegate=${_delegate.value}]" + override fun toString(): String = "TestMainDispatcher[delegate=${delegate.value}]" companion object { internal val currentTestDispatcher - get() = (Dispatchers.Main as? TestMainDispatcher)?._delegate?.value as? TestDispatcher + get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher internal val currentTestScheduler get() = currentTestDispatcher?.scheduler From 361462b7c53a76861532036d970d0aca9828deae Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 25 Nov 2021 12:02:45 +0300 Subject: [PATCH 5/7] Fix a test --- .../common/src/internal/TestMainDispatcher.kt | 2 +- .../test/ordered/tests/FirstMockedMainTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt index f39e1afbae..be97123d6c 100644 --- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt @@ -13,7 +13,7 @@ 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?, mainInitException: Throwable? = null): +internal class TestMainDispatcher(delegate: CoroutineDispatcher?, mainInitException: Throwable? = null) : MainCoroutineDispatcher(), Delay { private val mainDispatcher = delegate ?: UnsetMainDispatcher(mainInitException) 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")) } } } From 00a47dd9c0353d349d77b101393b3d527935917b Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 25 Nov 2021 12:09:28 +0300 Subject: [PATCH 6/7] Remove extraneous @Ignore --- .../BlockingCoroutineDispatcherWorkSignallingStressTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt index 6fa7443415..3b3e085047 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt @@ -11,7 +11,6 @@ import org.junit.Test import java.util.concurrent.* import kotlin.test.* -@Ignore class BlockingCoroutineDispatcherWorkSignallingStressTest : SchedulerTestBase() { @Test From dc09c3735c137624941bcb7bd0874a1bd6db5a4d Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 25 Nov 2021 12:11:19 +0300 Subject: [PATCH 7/7] Better exception messages --- .../common/src/internal/TestMainDispatcher.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt index be97123d6c..7b9cd79a71 100644 --- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt @@ -98,9 +98,9 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher?, mainInitExcept private fun missing(): Nothing { val message = if (mainInitException == null) - "Dispatchers.Main is not available was not provided for tests via Dispatchers.setMain." + "Dispatchers.Main is not available and was not provided for tests via Dispatchers.setMain." else - "Dispatchers.Main failed to initialize and was not replaced via Dispatchers.setMain." + "Dispatchers.Main failed to initialize and was not replaced for tests via Dispatchers.setMain." throw IllegalStateException(message, mainInitException) }