Skip to content

Commit 7ebd03d

Browse files
committed
WIP
1 parent a8e43d6 commit 7ebd03d

10 files changed

+353
-32
lines changed

kotlinx-coroutines-test/api/kotlinx-coroutines-test.api

+21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ public abstract interface class kotlinx/coroutines/test/DelayController {
99
public abstract fun runCurrent ()V
1010
}
1111

12+
public final class kotlinx/coroutines/test/StandardTestDispatcher : kotlinx/coroutines/test/TestDispatcher, kotlinx/coroutines/Delay {
13+
public fun <init> ()V
14+
public fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)V
15+
public synthetic fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
16+
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
17+
public fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
18+
public fun toString ()Ljava/lang/String;
19+
}
20+
1221
public final class kotlinx/coroutines/test/TestBuildersKt {
1322
public static final fun runBlockingTest (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
1423
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineDispatcher;Lkotlin/jvm/functions/Function2;)V
@@ -69,6 +78,8 @@ public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
6978
public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
7079
public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestCoroutineScope;J)V
7180
public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestCoroutineScope;)V
81+
public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
82+
public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
7283
public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J
7384
public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
7485
public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -94,3 +105,13 @@ public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor
94105
public abstract fun getUncaughtExceptions ()Ljava/util/List;
95106
}
96107

108+
public final class kotlinx/coroutines/test/UnconfinedTestDispatcher : kotlinx/coroutines/test/TestDispatcher, kotlinx/coroutines/Delay {
109+
public fun <init> ()V
110+
public fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)V
111+
public synthetic fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
112+
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
113+
public fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
114+
public fun isDispatchNeeded (Lkotlin/coroutines/CoroutineContext;)Z
115+
public fun toString ()Ljava/lang/String;
116+
}
117+

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

+4-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import kotlin.coroutines.*
4444
*/
4545
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
4646
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
47-
val scope = TestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
47+
val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
4848
val scheduler = scope.testScheduler
4949
val deferred = scope.async {
5050
scope.testBody()
@@ -167,10 +167,9 @@ public expect class TestResult
167167
*
168168
* [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
169169
* scope created for the test, [context] also can be used to change how the test is executed.
170-
* See the [TestCoroutineScope] constructor documentation for details.
170+
* See the [createTestCoroutineScope] documentation for details.
171171
*
172-
* @throws IllegalArgumentException if the [context] is invalid. See the [TestCoroutineScope] constructor docs for
173-
* details.
172+
* @throws IllegalArgumentException if the [context] is invalid. See the [createTestCoroutineScope] docs for details.
174173
*/
175174
@ExperimentalCoroutinesApi
176175
public fun runTest(
@@ -180,7 +179,7 @@ public fun runTest(
180179
): TestResult {
181180
if (context[RunningInRunTest] != null)
182181
throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
183-
val testScope = TestBodyCoroutine<Unit>(TestCoroutineScope(context + RunningInRunTest))
182+
val testScope = TestBodyCoroutine<Unit>(createTestCoroutineScope(context + RunningInRunTest))
184183
val scheduler = testScope.testScheduler
185184
return createTestResult {
186185
/** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.test
6+
7+
import kotlinx.coroutines.*
8+
import kotlin.coroutines.*
9+
10+
/**
11+
* An unconfined [TestDispatcher].
12+
*
13+
* This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular
14+
* thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do.
15+
*
16+
* Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and
17+
* in which order the queued coroutines are executed. However, please be aware that, like [Dispatchers.Unconfined], this
18+
* is a specific dispatcher with execution order guarantees that are unusual and not shared by most other dispatchers,
19+
* so it can only be used reliably for testing functionality, not the specific order of actions.
20+
* See [Dispatchers.Unconfined] for a discussion of the execution order guarantees.
21+
*
22+
* In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control
23+
* the virtual time and can be shared among many test dispatchers. If no [scheduler] is passed as an argument, a new one
24+
* is created.
25+
*
26+
* Additionally, [name] can be set to distinguish each dispatcher instance when debugging.
27+
*
28+
* @see StandardTestDispatcher for a more predictable [TestDispatcher] that, however, requires interacting with the
29+
* scheduler in order for the tasks to run.
30+
*/
31+
@ExperimentalCoroutinesApi
32+
public class UnconfinedTestDispatcher(
33+
public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
34+
private val name: String? = null
35+
): TestDispatcher(), Delay {
36+
37+
/** @suppress */
38+
override fun processEvent(time: Long, marker: Any) {
39+
check(marker is Runnable)
40+
marker.run()
41+
}
42+
43+
/** @suppress */
44+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
45+
46+
/** @suppress */
47+
@Suppress("INVISIBLE_MEMBER")
48+
override fun dispatch(context: CoroutineContext, block: Runnable) {
49+
checkSchedulerInContext(scheduler, context)
50+
scheduler.sendDispatchEvent()
51+
52+
/** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */
53+
/** It can only be called by the [yield] function. See also code of [yield] function. */
54+
val yieldContext = context[YieldContext]
55+
if (yieldContext !== null) {
56+
// report to "yield" that it is an unconfined dispatcher and don't call "block.run()"
57+
yieldContext.dispatcherWasUnconfined = true
58+
return
59+
}
60+
throw UnsupportedOperationException(
61+
"Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " +
62+
"the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
63+
"isDispatchNeeded and dispatch calls."
64+
)
65+
}
66+
67+
/** @suppress */
68+
override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]"
69+
70+
}
71+
72+
/**
73+
* A [TestDispatcher] instance whose tasks are run inside calls to the [scheduler].
74+
*
75+
* This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its
76+
* [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent],
77+
* [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these
78+
* tasks in a blocking manner.
79+
*
80+
* In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are
81+
* parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to
82+
* run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when
83+
* inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines.
84+
*
85+
* If a [scheduler] is not passed as an argument, a new one is created.
86+
*
87+
* One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging.
88+
*
89+
* @see UnconfinedTestDispatcher for a dispatcher that immediately enters [launch] and [async] blocks and is not
90+
* confined to any particular thread.
91+
*/
92+
@ExperimentalCoroutinesApi
93+
public class StandardTestDispatcher(
94+
public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
95+
private val name: String? = null
96+
): TestDispatcher(), Delay {
97+
98+
/** @suppress */
99+
override fun processEvent(time: Long, marker: Any) {
100+
check(marker is Runnable)
101+
marker.run()
102+
}
103+
104+
/** @suppress */
105+
@Suppress("INVISIBLE_MEMBER")
106+
override fun dispatch(context: CoroutineContext, block: Runnable) {
107+
checkSchedulerInContext(scheduler, context)
108+
scheduler.registerEvent(this, 0, block) { false }
109+
}
110+
111+
/** @suppress */
112+
override fun toString(): String = "${name ?: "ConfinedTestDispatcher"}[scheduler=$scheduler]"
113+
114+
}

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

+39-10
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,18 @@ private class TestCoroutineScopeImpl(
4545

4646
override fun cleanupTestCoroutines() {
4747
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
48-
coroutineContext.delayController?.cleanupTestCoroutines()
48+
val delayController = coroutineContext.delayController
49+
if (delayController != null) {
50+
delayController.cleanupTestCoroutines()
51+
} else {
52+
testScheduler.runCurrent()
53+
if (!testScheduler.isIdle()) {
54+
throw UncompletedCoroutinesError(
55+
"Unfinished coroutines during teardown. Ensure all coroutines are" +
56+
" completed or cancelled by your test."
57+
)
58+
}
59+
}
4960
val jobs = coroutineContext.activeJobs()
5061
if ((jobs - initialJobs).isNotEmpty())
5162
throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
@@ -56,13 +67,29 @@ private fun CoroutineContext.activeJobs(): Set<Job> {
5667
return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
5768
}
5869

70+
/**
71+
* A coroutine scope for launching test coroutines using [TestCoroutineDispatcher].
72+
*
73+
* [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher].
74+
*/
75+
@Deprecated("This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
76+
"Please use `createTestCoroutineScope` instead.",
77+
ReplaceWith("createTestCoroutineScope(TestCoroutineDispatcher() + context)",
78+
"kotlin.coroutines.EmptyCoroutineContext"),
79+
level = DeprecationLevel.WARNING
80+
)
81+
public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
82+
val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler()
83+
return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + context)
84+
}
85+
5986
/**
6087
* A coroutine scope for launching test coroutines.
6188
*
6289
* It ensures that all the test module machinery is properly initialized.
6390
* * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
6491
* a new one is created, unless a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used.
65-
* * If [context] doesn't have a [ContinuationInterceptor], a [TestCoroutineDispatcher] is created.
92+
* * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created.
6693
* * If [context] does not provide a [CoroutineExceptionHandler], [TestCoroutineExceptionHandler] is created
6794
* automatically.
6895
* * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
@@ -73,9 +100,8 @@ private fun CoroutineContext.activeJobs(): Set<Job> {
73100
* @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
74101
* [UncaughtExceptionCaptor].
75102
*/
76-
@Suppress("FunctionName")
77103
@ExperimentalCoroutinesApi
78-
public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
104+
public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
79105
val scheduler: TestCoroutineScheduler
80106
val dispatcher = when (val dispatcher = context[ContinuationInterceptor]) {
81107
is TestDispatcher -> {
@@ -91,7 +117,7 @@ public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext)
91117
}
92118
null -> {
93119
scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler()
94-
TestCoroutineDispatcher(scheduler)
120+
StandardTestDispatcher(scheduler)
95121
}
96122
else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
97123
}
@@ -159,7 +185,7 @@ public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit =
159185
* @see TestCoroutineScheduler.advanceUntilIdle
160186
*/
161187
@ExperimentalCoroutinesApi
162-
public fun TestCoroutineScope.advanceUntilIdle(): Unit {
188+
public fun TestCoroutineScope.advanceUntilIdle() {
163189
coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle()
164190
}
165191

@@ -170,13 +196,14 @@ public fun TestCoroutineScope.advanceUntilIdle(): Unit {
170196
* @see TestCoroutineScheduler.runCurrent
171197
*/
172198
@ExperimentalCoroutinesApi
173-
public fun TestCoroutineScope.runCurrent(): Unit {
199+
public fun TestCoroutineScope.runCurrent() {
174200
coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent()
175201
}
176202

177203
@ExperimentalCoroutinesApi
178204
@Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " +
179-
"Only `TestCoroutineDispatcher` supports pausing; pause it directly.",
205+
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
206+
"\"paused\", like `StandardTestDispatcher`.",
180207
ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
181208
"kotlin.coroutines.ContinuationInterceptor"),
182209
DeprecationLevel.WARNING)
@@ -186,7 +213,8 @@ public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit)
186213

187214
@ExperimentalCoroutinesApi
188215
@Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " +
189-
"Only `TestCoroutineDispatcher` supports pausing; pause it directly.",
216+
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
217+
"\"paused\", like `StandardTestDispatcher`.",
190218
ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
191219
"kotlin.coroutines.ContinuationInterceptor"),
192220
level = DeprecationLevel.WARNING)
@@ -196,7 +224,8 @@ public fun TestCoroutineScope.pauseDispatcher() {
196224

197225
@ExperimentalCoroutinesApi
198226
@Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " +
199-
"Only `TestCoroutineDispatcher` supports pausing; pause it directly.",
227+
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
228+
"\"paused\", like `StandardTestDispatcher`.",
200229
ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
201230
"kotlin.coroutines.ContinuationInterceptor"),
202231
level = DeprecationLevel.WARNING)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class RunTestTest {
156156

157157
@Test
158158
fun reproducer2405() = runTest {
159-
val dispatcher = TestCoroutineDispatcher(testScheduler)
159+
val dispatcher = StandardTestDispatcher(testScheduler)
160160
var collectedError = false
161161
withContext(dispatcher) {
162162
flow { emit(1) }

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class TestCoroutineSchedulerTest {
7272
@Test
7373
fun testAdvanceTimeBy() = assertRunsFast {
7474
val scheduler = TestCoroutineScheduler()
75-
val scope = TestCoroutineScope(scheduler)
75+
val scope = createTestCoroutineScope(scheduler)
7676
var stage = 1
7777
scope.launch {
7878
delay(1_000)
@@ -126,7 +126,7 @@ class TestCoroutineSchedulerTest {
126126
@Test
127127
fun testRunCurrentNotDrainingQueue() = assertRunsFast {
128128
val scheduler = TestCoroutineScheduler()
129-
val scope = TestCoroutineScope(scheduler)
129+
val scope = createTestCoroutineScope(scheduler)
130130
var stage = 1
131131
scope.launch {
132132
delay(SLOW)
@@ -149,7 +149,7 @@ class TestCoroutineSchedulerTest {
149149
@Test
150150
fun testNestedAdvanceUntilIdle() = assertRunsFast {
151151
val scheduler = TestCoroutineScheduler()
152-
val scope = TestCoroutineScope(scheduler)
152+
val scope = createTestCoroutineScope(scheduler)
153153
var executed = false
154154
scope.launch {
155155
launch {
@@ -165,7 +165,7 @@ class TestCoroutineSchedulerTest {
165165
/** Tests [yield] scheduling tasks for future execution and not executing immediately. */
166166
@Test
167167
fun testYield() {
168-
val scope = TestCoroutineScope()
168+
val scope = createTestCoroutineScope()
169169
var stage = 0
170170
scope.launch {
171171
yield()
@@ -206,7 +206,7 @@ class TestCoroutineSchedulerTest {
206206
/** Tests that timeouts get triggered. */
207207
@Test
208208
fun testSmallTimeouts() {
209-
val scope = TestCoroutineScope()
209+
val scope = createTestCoroutineScope(TestCoroutineDispatcher())
210210
scope.checkTimeout(true) {
211211
val half = SLOW / 2
212212
delay(half)
@@ -217,7 +217,7 @@ class TestCoroutineSchedulerTest {
217217
/** Tests that timeouts don't get triggered if the code finishes in time. */
218218
@Test
219219
fun testLargeTimeouts() {
220-
val scope = TestCoroutineScope()
220+
val scope = createTestCoroutineScope()
221221
scope.checkTimeout(false) {
222222
val half = SLOW / 2
223223
delay(half)
@@ -228,7 +228,7 @@ class TestCoroutineSchedulerTest {
228228
/** Tests that timeouts get triggered if the code fails to finish in time asynchronously. */
229229
@Test
230230
fun testSmallAsynchronousTimeouts() {
231-
val scope = TestCoroutineScope()
231+
val scope = createTestCoroutineScope()
232232
val deferred = CompletableDeferred<Unit>()
233233
scope.launch {
234234
val half = SLOW / 2
@@ -244,7 +244,7 @@ class TestCoroutineSchedulerTest {
244244
/** Tests that timeouts don't get triggered if the code finishes in time, even if it does so asynchronously. */
245245
@Test
246246
fun testLargeAsynchronousTimeouts() {
247-
val scope = TestCoroutineScope()
247+
val scope = createTestCoroutineScope()
248248
val deferred = CompletableDeferred<Unit>()
249249
scope.launch {
250250
val half = SLOW / 2

0 commit comments

Comments
 (0)