Skip to content

Commit f979638

Browse files
committed
Eagerly enter launch and async blocks with unconfined dispatcher (#3011)
Also, fix `Dispatchers.Main` not delegating `Delay` methods and discover that, on JS, `Dispatchers.Main` gets reset during the test if it is reset in `AfterTest`.
1 parent 665cc43 commit f979638

13 files changed

+228
-40
lines changed

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

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

15+
@PublishedApi
1516
@Suppress("PropertyName")
1617
internal expect val DefaultDelay: Delay
1718

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public fun runTest(
219219
return createTestResult {
220220
/** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
221221
* [TestCoroutineDispatcher], because the event loop is not started. */
222-
testScope.start(CoroutineStart.DEFAULT, testScope) {
222+
testScope.start(CoroutineStart.UNDISPATCHED, testScope) {
223223
testBody()
224224
}
225225
var completed = false

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

+43-12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package kotlinx.coroutines.test
77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.channels.*
99
import kotlinx.coroutines.flow.*
10+
import kotlinx.coroutines.test.internal.*
11+
import kotlinx.coroutines.test.internal.TestMainDispatcher
1012
import kotlin.coroutines.*
1113

1214
/**
@@ -15,10 +17,32 @@ import kotlin.coroutines.*
1517
* This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular
1618
* thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do.
1719
*
20+
* Like [Dispatchers.Unconfined], this one does not provide guarantees about the execution order when several coroutines
21+
* are queued in this dispatcher. However, we ensure that the [launch] and [async] blocks at the top level of [runTest]
22+
* are entered eagerly. This allows launching child coroutines and not calling [runCurrent] for them to start executing.
23+
*
24+
* ```
25+
* @Test
26+
* fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
27+
* var entered = false
28+
* val deferred = CompletableDeferred<Unit>()
29+
* var completed = false
30+
* launch {
31+
* entered = true
32+
* deferred.await()
33+
* completed = true
34+
* }
35+
* assertTrue(entered) // `entered = true` already executed.
36+
* assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
37+
* deferred.complete(Unit) // resume the coroutine.
38+
* assertTrue(completed) // now the child coroutine is immediately completed.
39+
* }
40+
* ```
41+
*
1842
* Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and
1943
* in which order the queued coroutines are executed.
20-
* The typical use case for this is launching child coroutines that are resumed immediately, without going through a
21-
* dispatch; this can be helpful for testing [Channel] and [StateFlow] usages.
44+
* Another typical use case for this dispatcher is launching child coroutines that are resumed immediately, without
45+
* going through a dispatch; this can be helpful for testing [Channel] and [StateFlow] usages.
2246
*
2347
* ```
2448
* @Test
@@ -40,14 +64,16 @@ import kotlin.coroutines.*
4064
* }
4165
* ```
4266
*
43-
* However, please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order
67+
* Please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order
4468
* guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing
4569
* functionality, not the specific order of actions.
4670
* See [Dispatchers.Unconfined] for a discussion of the execution order guarantees.
4771
*
4872
* In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control
49-
* the virtual time and can be shared among many test dispatchers. If no [scheduler] is passed as an argument, a new one
50-
* is created.
73+
* the virtual time and can be shared among many test dispatchers.
74+
* If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a
75+
* [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if
76+
* [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created.
5177
*
5278
* Additionally, [name] can be set to distinguish each dispatcher instance when debugging.
5379
*
@@ -56,14 +82,14 @@ import kotlin.coroutines.*
5682
@ExperimentalCoroutinesApi
5783
@Suppress("FunctionName")
5884
public fun UnconfinedTestDispatcher(
59-
scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
85+
scheduler: TestCoroutineScheduler? = null,
6086
name: String? = null
61-
): TestDispatcher = UnconfinedTestDispatcherImpl(scheduler, name)
87+
): TestDispatcher = UnconfinedTestDispatcherImpl(scheduler ?: mainTestScheduler ?: TestCoroutineScheduler(), name)
6288

6389
private class UnconfinedTestDispatcherImpl(
6490
override val scheduler: TestCoroutineScheduler,
6591
private val name: String? = null
66-
): TestDispatcher() {
92+
) : TestDispatcher() {
6793

6894
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
6995

@@ -103,22 +129,24 @@ private class UnconfinedTestDispatcherImpl(
103129
* run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when
104130
* inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines.
105131
*
106-
* If a [scheduler] is not passed as an argument, a new one is created.
132+
* If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a
133+
* [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if
134+
* [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created.
107135
*
108136
* One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging.
109137
*
110138
* @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread.
111139
*/
112140
@Suppress("FunctionName")
113141
public fun StandardTestDispatcher(
114-
scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
142+
scheduler: TestCoroutineScheduler? = null,
115143
name: String? = null
116-
): TestDispatcher = StandardTestDispatcherImpl(scheduler, name)
144+
): TestDispatcher = StandardTestDispatcherImpl(scheduler ?: mainTestScheduler ?: TestCoroutineScheduler(), name)
117145

118146
private class StandardTestDispatcherImpl(
119147
override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
120148
private val name: String? = null
121-
): TestDispatcher() {
149+
) : TestDispatcher() {
122150

123151
override fun dispatch(context: CoroutineContext, block: Runnable) {
124152
checkSchedulerInContext(scheduler, context)
@@ -127,3 +155,6 @@ private class StandardTestDispatcherImpl(
127155

128156
override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]"
129157
}
158+
159+
private val mainTestScheduler
160+
get() = ((Dispatchers.Main as? TestMainDispatcher)?.delegate as? TestDispatcher)?.scheduler

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

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

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.internal.*
9-
import kotlinx.coroutines.test.internal.*
10-
import kotlinx.coroutines.test.internal.TestMainDispatcher
119
import kotlin.coroutines.*
1210

1311
/**
@@ -172,12 +170,7 @@ public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineCo
172170
}
173171
dispatcher
174172
}
175-
null -> {
176-
val mainDispatcherScheduler =
177-
((Dispatchers.Main as? TestMainDispatcher)?.delegate as? TestDispatcher)?.scheduler
178-
scheduler = context[TestCoroutineScheduler] ?: mainDispatcherScheduler ?: TestCoroutineScheduler()
179-
StandardTestDispatcher(scheduler)
180-
}
173+
null -> StandardTestDispatcher(context[TestCoroutineScheduler]).also { scheduler = it.scheduler }
181174
else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
182175
}
183176
var scope: TestCoroutineScopeImpl? = null

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import kotlin.coroutines.*
1212
*/
1313
internal class TestMainDispatcher(var delegate: CoroutineDispatcher):
1414
MainCoroutineDispatcher(),
15-
Delay by (delegate as? Delay ?: defaultDelay)
15+
Delay
1616
{
1717
private val mainDispatcher = delegate // the initial value passed to the constructor
1818

19+
private val delay
20+
get() = delegate as? Delay ?: defaultDelay
21+
1922
override val immediate: MainCoroutineDispatcher
2023
get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this
2124

@@ -28,6 +31,12 @@ internal class TestMainDispatcher(var delegate: CoroutineDispatcher):
2831
fun resetDispatcher() {
2932
delegate = mainDispatcher
3033
}
34+
35+
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
36+
delay.scheduleResumeAfterDelay(timeMillis, continuation)
37+
38+
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
39+
delay.invokeOnTimeout(timeMillis, block, context)
3140
}
3241

3342
@Suppress("INVISIBLE_MEMBER")

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

+3
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,8 @@ open class OrderedExecutionTestBase {
6767

6868
internal fun <T> T.void() { }
6969

70+
@OptionalExpectation
71+
expect annotation class NoJs()
72+
7073
@OptionalExpectation
7174
expect annotation class NoNative()

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

+18-18
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class RunTestTest {
8484

8585
/** Tests that too low of a dispatch timeout causes crashes. */
8686
@Test
87-
@Ignore // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
87+
@NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
8888
fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
8989
assertFailsWith<UncompletedCoroutinesError> { fn() }
9090
}) {
@@ -107,7 +107,7 @@ class RunTestTest {
107107

108108
/** Tests uncaught exceptions taking priority over dispatch timeout in error reports. */
109109
@Test
110-
@Ignore // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
110+
@NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
111111
fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
112112
assertFailsWith<IllegalArgumentException> { fn() }
113113
}) {
@@ -174,12 +174,12 @@ class RunTestTest {
174174

175175
/** Tests that, once the test body has thrown, the child coroutines are cancelled. */
176176
@Test
177-
fun testChildrenCancellationOnTestBodyFailure() {
177+
fun testChildrenCancellationOnTestBodyFailure(): TestResult {
178178
var job: Job? = null
179-
testResultMap({
179+
return testResultMap({
180180
assertFailsWith<AssertionError> { it() }
181181
assertTrue(job!!.isCancelled)
182-
}, {
182+
}) {
183183
runTest {
184184
job = launch {
185185
while (true) {
@@ -188,34 +188,34 @@ class RunTestTest {
188188
}
189189
throw AssertionError()
190190
}
191-
})
191+
}
192192
}
193193

194194
/** Tests that [runTest] reports [TimeoutCancellationException]. */
195195
@Test
196196
fun testTimeout() = testResultMap({
197197
assertFailsWith<TimeoutCancellationException> { it() }
198-
}, {
198+
}) {
199199
runTest {
200200
withTimeout(50) {
201201
launch {
202202
delay(1000)
203203
}
204204
}
205205
}
206-
})
206+
}
207207

208208
/** Checks that [runTest] throws the root cause and not [JobCancellationException] when a child coroutine throws. */
209209
@Test
210210
fun testRunTestThrowsRootCause() = testResultMap({
211211
assertFailsWith<TestException> { it() }
212-
}, {
212+
}) {
213213
runTest {
214214
launch {
215215
throw TestException()
216216
}
217217
}
218-
})
218+
}
219219

220220
/** Tests that [runTest] completes its job. */
221221
@Test
@@ -224,13 +224,13 @@ class RunTestTest {
224224
return testResultMap({
225225
it()
226226
assertTrue(handlerCalled)
227-
}, {
227+
}) {
228228
runTest {
229229
coroutineContext.job.invokeOnCompletion {
230230
handlerCalled = true
231231
}
232232
}
233-
})
233+
}
234234
}
235235

236236
/** Tests that [runTest] doesn't complete the job that was passed to it as an argument. */
@@ -245,11 +245,11 @@ class RunTestTest {
245245
it()
246246
assertFalse(handlerCalled)
247247
assertEquals(0, job.children.filter { it.isActive }.count())
248-
}, {
248+
}) {
249249
runTest(job) {
250250
assertTrue(coroutineContext.job in job.children)
251251
}
252-
})
252+
}
253253
}
254254

255255
/** Tests that, when the test body fails, the reported exceptions are suppressed. */
@@ -267,14 +267,14 @@ class RunTestTest {
267267
assertEquals("y", suppressed[1].message)
268268
assertEquals("z", suppressed[2].message)
269269
}
270-
}, {
270+
}) {
271271
runTest {
272272
launch(SupervisorJob()) { throw TestException("x") }
273273
launch(SupervisorJob()) { throw TestException("y") }
274274
launch(SupervisorJob()) { throw TestException("z") }
275275
throw TestException("w")
276276
}
277-
})
277+
}
278278

279279
/** Tests that [TestCoroutineScope.runTest] does not inherit the exception handler and works. */
280280
@Test
@@ -287,10 +287,10 @@ class RunTestTest {
287287
} catch (e: TestException) {
288288
scope.cleanupTestCoroutines() // should not fail
289289
}
290-
}, {
290+
}) {
291291
scope.runTest {
292292
launch(SupervisorJob()) { throw TestException("x") }
293293
}
294-
})
294+
}
295295
}
296296
}

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

+13
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,17 @@ class StandardTestDispatcherTest: OrderedExecutionTestBase() {
5454
expect(5)
5555
}.void()
5656

57+
/** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */
58+
@Test
59+
fun testSchedulerReuse() {
60+
val dispatcher1 = StandardTestDispatcher()
61+
Dispatchers.setMain(dispatcher1)
62+
try {
63+
val dispatcher2 = StandardTestDispatcher()
64+
assertSame(dispatcher1.scheduler, dispatcher2.scheduler)
65+
} finally {
66+
Dispatchers.resetMain()
67+
}
68+
}
69+
5770
}

0 commit comments

Comments
 (0)