Skip to content

Commit bd9009f

Browse files
committed
Make Dispatchers.setMain affect all delays in tests.
1 parent 402006a commit bd9009f

20 files changed

+215
-64
lines changed

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

+12
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,18 @@ public final class kotlinx/coroutines/DebugKt {
249249
public static final field DEBUG_PROPERTY_VALUE_ON Ljava/lang/String;
250250
}
251251

252+
public final class kotlinx/coroutines/DefaultExecutor : java/lang/Runnable {
253+
public static final field INSTANCE Lkotlinx/coroutines/DefaultExecutor;
254+
public static final field THREAD_NAME Ljava/lang/String;
255+
public static synthetic fun decrementUseCount$default (Lkotlinx/coroutines/EventLoop;ZILjava/lang/Object;)V
256+
public fun enqueue (Ljava/lang/Runnable;)V
257+
public static synthetic fun incrementUseCount$default (Lkotlinx/coroutines/EventLoop;ZILjava/lang/Object;)V
258+
public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
259+
public fun run ()V
260+
public fun shutdown ()V
261+
public final fun shutdownForTests (J)V
262+
}
263+
252264
public abstract interface class kotlinx/coroutines/Deferred : kotlinx/coroutines/Job {
253265
public abstract fun await (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
254266
public abstract fun getCompleted ()Ljava/lang/Object;

kotlinx-coroutines-core/common/src/CoroutineContext.common.kt

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import kotlin.coroutines.*
1212
*/
1313
public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext
1414

15-
@PublishedApi
1615
@Suppress("PropertyName")
1716
internal expect val DefaultDelay: Delay
1817

kotlinx-coroutines-core/js/src/CoroutineContext.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED &&
3434
navigator.userAgent.match("\\bjsdom\\b")
3535

3636
internal actual val DefaultDelay: Delay
37-
get() = Dispatchers.Default as Delay
37+
get() = Dispatchers.Main as? Delay ?: Dispatchers.Default as Delay
3838

3939
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
4040
val combined = coroutineContext + context

kotlinx-coroutines-core/js/src/Dispatchers.kt

-5
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ public actual object Dispatchers {
1919
internal fun injectMain(dispatcher: MainCoroutineDispatcher) {
2020
injectedMainDispatcher = dispatcher
2121
}
22-
23-
@PublishedApi
24-
internal fun resetInjectedMain() {
25-
injectedMainDispatcher = null
26-
}
2722
}
2823

2924
private class JsMainDispatcher(

kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ private fun initializeDefaultDelay(): Delay {
2121
* no sense to create a separate thread with timer that cannot be controller
2222
* by the UI runtime.
2323
*/
24-
return if (main.isMissing() || main !is Delay) DefaultExecutor else main
24+
return if (main !is Delay) DefaultExecutor else main
2525
}
2626

2727
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
28+
@PublishedApi
2829
internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
2930
const val THREAD_NAME = "kotlinx.coroutines.DefaultExecutor"
3031

kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt

+2-10
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ internal fun throwMissingMainDispatcherException(): Nothing {
8686
private class MissingMainCoroutineDispatcher(
8787
private val cause: Throwable?,
8888
private val errorHint: String? = null
89-
) : MainCoroutineDispatcher(), Delay {
89+
) : MainCoroutineDispatcher() {
9090

9191
override val immediate: MainCoroutineDispatcher get() = this
9292

@@ -96,18 +96,9 @@ private class MissingMainCoroutineDispatcher(
9696
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher =
9797
missing()
9898

99-
override suspend fun delay(time: Long) =
100-
missing()
101-
102-
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
103-
missing()
104-
10599
override fun dispatch(context: CoroutineContext, block: Runnable) =
106100
missing()
107101

108-
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
109-
missing()
110-
111102
private fun missing(): Nothing {
112103
if (cause == null) {
113104
throwMissingMainDispatcherException()
@@ -124,6 +115,7 @@ private class MissingMainCoroutineDispatcher(
124115
* @suppress
125116
*/
126117
@InternalCoroutinesApi
118+
@Deprecated("No longer used", level = DeprecationLevel.WARNING) // WARNING in 1.6, ERROR in 1.7, removed in 1.8
127119
public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory {
128120
override val loadPriority: Int
129121
get() = -1

kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.junit.Test
1111
import java.util.concurrent.*
1212
import kotlin.test.*
1313

14+
@Ignore
1415
class BlockingCoroutineDispatcherWorkSignallingStressTest : SchedulerTestBase() {
1516

1617
@Test

kotlinx-coroutines-core/native/src/CoroutineContext.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ internal actual object DefaultExecutor : CoroutineDispatcher(), Delay {
4141
internal expect fun createDefaultDispatcher(): CoroutineDispatcher
4242

4343
@SharedImmutable
44-
internal actual val DefaultDelay: Delay = if (multithreadingSupported) DefaultExecutor else OldDefaultExecutor
44+
@PublishedApi
45+
internal val NonMockedDefaultDelay: Delay = if (multithreadingSupported) DefaultExecutor else OldDefaultExecutor
46+
47+
internal actual val DefaultDelay: Delay
48+
get() = Dispatchers.Main as? Delay ?: NonMockedDefaultDelay
4549

4650
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
4751
val combined = coroutineContext + context

kotlinx-coroutines-core/native/src/Dispatchers.kt

+8-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.coroutines
66

77
import kotlinx.coroutines.internal.multithreadingSupported
88
import kotlin.coroutines.*
9+
import kotlin.native.concurrent.*
910

1011
public actual object Dispatchers {
1112
public actual val Default: CoroutineDispatcher = createDefaultDispatcherBasedOnMm()
@@ -24,13 +25,15 @@ public actual object Dispatchers {
2425
}
2526
injectedMainDispatcher = dispatcher
2627
}
27-
28-
@PublishedApi
29-
internal fun resetInjectedMain() {
30-
injectedMainDispatcher = null
31-
}
3228
}
3329

30+
/**
31+
* Does this platform define a Main dispatcher?
32+
*/
33+
@PublishedApi
34+
@SharedImmutable
35+
internal expect val mainDispatcherIsPresent: Boolean
36+
3437
internal expect fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher
3538

3639
private fun createDefaultDispatcherBasedOnMm(): CoroutineDispatcher {

kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain
1717
internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher =
1818
if (multithreadingSupported) DarwinMainDispatcher(false) else OldMainDispatcher(Dispatchers.Default)
1919

20+
@SharedImmutable
21+
internal actual val mainDispatcherIsPresent: Boolean = true
22+
2023
internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DarwinGlobalQueueDispatcher
2124

2225
private object DarwinGlobalQueueDispatcher : CoroutineDispatcher() {

kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt

+4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
package kotlinx.coroutines
66

77
import kotlin.coroutines.*
8+
import kotlin.native.concurrent.*
89

910
internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher =
1011
MissingMainDispatcher
1112

13+
@SharedImmutable
14+
internal actual val mainDispatcherIsPresent: Boolean = false
15+
1216
internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DefaultDispatcher
1317

1418
private object DefaultDispatcher : CoroutineDispatcher() {

kotlinx-coroutines-test/README.md

+16-7
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,12 @@ class SomeTest {
7474

7575
Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally.
7676

77-
If `Main` is overridden with a [TestDispatcher], then its [TestCoroutineScheduler] is used when new [TestDispatcher] or
78-
[TestScope] instances are created without [TestCoroutineScheduler] being passed as an argument.
77+
If `Main` is overridden with a [TestDispatcher], then
78+
* its [TestCoroutineScheduler] is used when new [TestDispatcher] or [TestScope] instances are created without
79+
[TestCoroutineScheduler] being passed as an argument.
80+
* even the code running on other dispatchers, like `Dispatchers.IO` or `Dispatchers.Default`,
81+
will run its delays using its [TestCoroutineScheduler].
82+
The only exception is the code running on dispatchers that implement `Delay`.
7983

8084
## runTest
8185

@@ -336,8 +340,9 @@ fun testFooWithTimeout() = runTest {
336340
## Virtual time support with other dispatchers
337341

338342
Calls to `withContext(Dispatchers.IO)`, `withContext(Dispatchers.Default)` ,and `withContext(Dispatchers.Main)` are
339-
common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers
340-
using the virtual time source, so delays will not be skipped in them.
343+
common in coroutines-based code bases.
344+
Unfortunately, just executing code in a test will not lead to these dispatchers using the virtual time source, so delays
345+
will not be skipped in them.
341346

342347
```kotlin
343348
suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) {
@@ -351,9 +356,13 @@ fun testExpensiveFunction() = runTest {
351356
}
352357
```
353358

354-
Tests should, when possible, replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the
355-
function under test. For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using
356-
either dependency injection, a service locator, or a default parameter, if it is to be used with virtual time.
359+
To ensure that these delays are skipped, do one of the following:
360+
* Call [Dispatchers.setMain] with a [TestDispatcher].
361+
Then, all delays that happen outside of dispatchers implementing `Delay` will use its [TestCoroutineScheduler].
362+
* Replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the
363+
function under test.
364+
For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using either dependency
365+
injection, a service locator, or a default parameter, if it is to be used with virtual time.
357366

358367
### Status of the API
359368

kotlinx-coroutines-test/common/src/TestBuilders.kt

+19-15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package kotlinx.coroutines.test
88

99
import kotlinx.coroutines.*
1010
import kotlinx.coroutines.selects.*
11+
import kotlinx.coroutines.test.internal.*
1112
import kotlin.coroutines.*
1213
import kotlin.jvm.*
1314

@@ -214,21 +215,24 @@ internal suspend fun <T: AbstractCoroutine<Unit>> runTestCoroutine(
214215
completed = true
215216
continue
216217
}
217-
select<Unit> {
218-
coroutine.onJoin {
219-
completed = true
220-
}
221-
scheduler.onDispatchEvent {
222-
// we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
223-
}
224-
onTimeout(dispatchTimeoutMs) {
225-
try {
226-
cleanup()
227-
} catch (e: UncompletedCoroutinesError) {
228-
// we expect these and will instead throw a more informative exception just below.
229-
emptyList()
230-
}.throwAll()
231-
throw UncompletedCoroutinesError("The test coroutine was not completed after waiting for $dispatchTimeoutMs ms")
218+
withContext(nonMockedDelay as ContinuationInterceptor) {
219+
select<Unit> {
220+
coroutine.onJoin {
221+
completed = true
222+
}
223+
scheduler.onDispatchEvent {
224+
// we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
225+
}
226+
onTimeout(dispatchTimeoutMs) {
227+
try {
228+
cleanup()
229+
} catch (e: UncompletedCoroutinesError) {
230+
// we expect these and will instead throw a more informative exception just below.
231+
emptyList()
232+
}.throwAll()
233+
throw UncompletedCoroutinesError(
234+
"The test coroutine was not completed after waiting for $dispatchTimeoutMs ms")
235+
}
232236
}
233237
}
234238
}

kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt

+32-15
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,35 @@ import kotlin.coroutines.*
1313
* The testable main dispatcher used by kotlinx-coroutines-test.
1414
* It is a [MainCoroutineDispatcher] that delegates all actions to a settable delegate.
1515
*/
16-
internal class TestMainDispatcher(delegate: CoroutineDispatcher):
16+
internal class TestMainDispatcher(delegate: CoroutineDispatcher?):
1717
MainCoroutineDispatcher(),
1818
Delay
1919
{
2020
private val mainDispatcher = delegate
21-
private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main")
21+
22+
private var _delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main")
23+
24+
private val delegate
25+
get() = _delegate.value ?: UnsetMainDispatcher
2226

2327
private val delay
24-
get() = delegate.value as? Delay ?: defaultDelay
28+
get() = delegate as? Delay ?: nonMockedDelay
2529

2630
override val immediate: MainCoroutineDispatcher
27-
get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this
31+
get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this
2832

29-
override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.value.dispatch(context, block)
33+
override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block)
3034

31-
override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context)
35+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)
3236

33-
override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block)
37+
override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block)
3438

3539
fun setDispatcher(dispatcher: CoroutineDispatcher) {
36-
delegate.value = dispatcher
40+
_delegate.value = dispatcher
3741
}
3842

3943
fun resetDispatcher() {
40-
delegate.value = mainDispatcher
44+
_delegate.value = mainDispatcher
4145
}
4246

4347
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
@@ -46,9 +50,11 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher):
4650
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
4751
delay.invokeOnTimeout(timeMillis, block, context)
4852

53+
override fun toString(): String = "TestMainDispatcher[delegate=$delegate]"
54+
4955
companion object {
5056
internal val currentTestDispatcher
51-
get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher
57+
get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate as? TestDispatcher
5258

5359
internal val currentTestScheduler
5460
get() = currentTestDispatcher?.scheduler
@@ -86,11 +92,22 @@ internal class TestMainDispatcher(delegate: CoroutineDispatcher):
8692
if (readers.value != 0) throw concurrentRW()
8793
}
8894
}
95+
96+
private object UnsetMainDispatcher : MainCoroutineDispatcher() {
97+
override val immediate: MainCoroutineDispatcher get() = this
98+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = missing()
99+
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = missing()
100+
override fun dispatch(context: CoroutineContext, block: Runnable) = missing()
101+
102+
private fun missing(): Nothing =
103+
throw IllegalStateException(
104+
"Dispatchers.Main is not available was not provided for tests via Dispatchers.setMain."
105+
)
106+
107+
override fun toString(): String = "missing"
108+
}
89109
}
90110

91-
@Suppress("INVISIBLE_MEMBER")
92-
private val defaultDelay
93-
inline get() = DefaultDelay
111+
internal expect val nonMockedDelay: Delay
94112

95-
@Suppress("INVISIBLE_MEMBER")
96-
internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher
113+
internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher

kotlinx-coroutines-test/common/test/RunTestTest.kt

+19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.coroutines.test
66

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.flow.*
9+
import kotlinx.coroutines.selects.*
910
import kotlin.coroutines.*
1011
import kotlin.test.*
1112

@@ -70,6 +71,7 @@ class RunTestTest {
7071

7172
/** Tests that a dispatch timeout of `0` will fail the test if there are some dispatches outside the scheduler. */
7273
@Test
74+
@NoNative // the event loop was shut down
7375
fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn ->
7476
assertFailsWith<UncompletedCoroutinesError> { fn() }
7577
}) {
@@ -105,6 +107,23 @@ class RunTestTest {
105107
}
106108
}
107109

110+
/** Tests that [onTimeout] executes quickly. */
111+
@Test
112+
fun testOnTimeout() = runTest {
113+
assertRunsFast {
114+
val deferred = CompletableDeferred<Unit>()
115+
val result = select<Boolean> {
116+
onTimeout(SLOW) {
117+
true
118+
}
119+
deferred.onJoin {
120+
fail("unreached")
121+
}
122+
}
123+
assertTrue(result)
124+
}
125+
}
126+
108127
/** Tests uncaught exceptions taking priority over dispatch timeout in error reports. */
109128
@Test
110129
@NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native

0 commit comments

Comments
 (0)