Skip to content

Commit 2e25bae

Browse files
Update kotlinx-coroutines-test (#2973)
This commit introduces the new version of the test module. Please see README.md and MIGRATION.md for a thorough discussion of the changes. Fixes #1203 Fixes #1609 Fixes #2379 Fixes #1749 Fixes #1204 Fixes #1390 Fixes #1222 Fixes #1395 Fixes #1881 Fixes #1910 Fixes #1772 Fixes #1626 Fixes #1742 Fixes #2082 Fixes #2102 Fixes #2405 Fixes #2462 Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
1 parent a4ae389 commit 2e25bae

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4367
-834
lines changed

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

+9
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,15 @@ public final class kotlinx/coroutines/TimeoutKt {
556556
public static final fun withTimeoutOrNull-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
557557
}
558558

559+
public final class kotlinx/coroutines/YieldContext : kotlin/coroutines/AbstractCoroutineContextElement {
560+
public static final field Key Lkotlinx/coroutines/YieldContext$Key;
561+
public field dispatcherWasUnconfined Z
562+
public fun <init> ()V
563+
}
564+
565+
public final class kotlinx/coroutines/YieldContext$Key : kotlin/coroutines/CoroutineContext$Key {
566+
}
567+
559568
public final class kotlinx/coroutines/YieldKt {
560569
public static final fun yield (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
561570
}

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-core/common/src/Unconfined.kt

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal object Unconfined : CoroutineDispatcher() {
3838
/**
3939
* Used to detect calls to [Unconfined.dispatch] from [yield] function.
4040
*/
41+
@PublishedApi
4142
internal class YieldContext : AbstractCoroutineContextElement(Key) {
4243
companion object Key : CoroutineContext.Key<YieldContext>
4344

kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt

-2
Original file line numberDiff line numberDiff line change
@@ -968,15 +968,13 @@ internal class CoroutineScheduler(
968968
* Checks if the thread is part of a thread pool that supports coroutines.
969969
* This function is needed for integration with BlockHound.
970970
*/
971-
@Suppress("UNUSED")
972971
@JvmName("isSchedulerWorker")
973972
internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker
974973

975974
/**
976975
* Checks if the thread is running a CPU-bound task.
977976
* This function is needed for integration with BlockHound.
978977
*/
979-
@Suppress("UNUSED")
980978
@JvmName("mayNotBlock")
981979
internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker &&
982980
thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED

kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import kotlinx.coroutines.scheduling.*
1010
import reactor.blockhound.*
1111
import reactor.blockhound.integration.*
1212

13-
@Suppress("UNUSED")
1413
public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
1514

1615
override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) {

kotlinx-coroutines-test/MIGRATION.md

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# Migration to the new kotlinx-coroutines-test API
2+
3+
In version 1.6.0, the API of the test module changed significantly.
4+
This is a guide for gradually adapting the existing test code to the new API.
5+
This guide is written step-by-step; the idea is to separate the migration into several sets of small changes.
6+
7+
## Remove custom `UncaughtExceptionCaptor`, `DelayController`, and `TestCoroutineScope` implementations
8+
9+
We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that
10+
you don't need to do anything for this section.
11+
12+
### `UncaughtExceptionCaptor`
13+
14+
If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler`
15+
was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure
16+
was called.
17+
18+
We currently don't provide a replacement for this.
19+
However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines
20+
are propagated structurally, which makes uncaught exception handlers less useful.
21+
22+
If you have a use case for this, please tell us about it at the issue tracker.
23+
Meanwhile, it should be possible to use a custom exception captor, which should only implement
24+
`CoroutineExceptionHandler` now, like this:
25+
26+
```kotlin
27+
@Test
28+
fun testFoo() = runTest {
29+
val customCaptor = MyUncaughtExceptionCaptor()
30+
launch(customCaptor) {
31+
// ...
32+
}
33+
advanceUntilIdle()
34+
customCaptor.cleanupTestCoroutines()
35+
}
36+
```
37+
38+
### `DelayController`
39+
40+
We don't provide a way to define custom dispatching strategies that support virtual time.
41+
That said, we significantly enhanced this mechanism:
42+
* Using multiple test dispatchers simultaneously is supported.
43+
For the dispatchers to have a shared knowledge of the virtual time, either the same `TestCoroutineScheduler` should be
44+
passed to each of them, or all of them should be constructed after `Dispatchers.setMain` is called with some test
45+
dispatcher.
46+
* Both a simple `StandardTestDispatcher` that is always paused, and unconfined `UnconfinedTestDispatcher` are provided.
47+
48+
If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue
49+
tracker.
50+
51+
### `TestCoroutineScope`
52+
53+
This scope couldn't be meaningfully used in tandem with `runBlockingTest`: according to the definition of
54+
`TestCoroutineScope.runBlockingTest`, only the scope's `coroutineContext` is used.
55+
So, there could be two reasons for defining a custom implementation:
56+
57+
* Avoiding the restrictions on placed `coroutineContext` in the `TestCoroutineScope` constructor function.
58+
These restrictions consisted of requirements for `CoroutineExceptionHandler` being an `UncaughtExceptionCaptor`, and
59+
`ContinuationInterceptor` being a `DelayController`, so it is also possible to fulfill these restrictions by defining
60+
conforming instances. In this case, follow the instructions about replacing them.
61+
* Using without `runBlockingTest`. In this case, you don't even need to implement `TestCoroutineScope`: nothing else
62+
accepts a `TestCoroutineScope` specifically as an argument.
63+
64+
## Remove usages of `TestCoroutineExceptionHandler` and `TestCoroutineScope.uncaughtExceptions`
65+
66+
It is already illegal to use a `TestCoroutineScope` without performing `cleanupTestCoroutines`, so the valid uses of
67+
`TestCoroutineExceptionHandler` include:
68+
69+
* Accessing `uncaughtExceptions` in the middle of the test to make sure that there weren't any uncaught exceptions
70+
*yet*.
71+
If there are any, they will be thrown by the cleanup procedure anyway.
72+
We don't support this use case, given how comparatively rare it is, but it can be handled in the same way as the
73+
following one.
74+
* Accessing `uncaughtExceptions` when the uncaught exceptions are actually expected.
75+
In this case, `cleanupTestCoroutines` will fail with an exception that is being caught later.
76+
It would be better in this case to use a custom `CoroutineExceptionHandler` so that actual problems that could be
77+
found by the cleanup procedure are not superseded by the exceptions that are expected.
78+
An example is shown below.
79+
80+
```kotlin
81+
val exceptions = mutableListOf<Throwable>()
82+
val customCaptor = CoroutineExceptionHandler { ctx, throwable ->
83+
exceptions.add(throwable) // add proper synchronization if the test is multithreaded
84+
}
85+
86+
@Test
87+
fun testFoo() = runTest {
88+
launch(customCaptor) {
89+
// ...
90+
}
91+
advanceUntilIdle()
92+
// check the list of the caught exceptions
93+
}
94+
```
95+
96+
## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope`
97+
98+
This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`.
99+
If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case,
100+
also pass this scheduler as the argument to the dispatcher.
101+
102+
## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher`
103+
104+
* In places where `pauseDispatcher` in its block form is called, replace it with a call to
105+
`withContext(StandardTestDispatcher(testScheduler))`
106+
(`testScheduler` is available as a field of `TestCoroutineScope`,
107+
or `scheduler` is available as a field of `TestCoroutineDispatcher`),
108+
followed by `advanceUntilIdle()`.
109+
This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused
110+
when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`.
111+
* Often, `pauseDispatcher()` in a non-block form is used at the start of the test.
112+
Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`,
113+
if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used,
114+
or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`.
115+
This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming,
116+
instead of the deprecated `TestCoroutineDispatcher`.
117+
* Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test.
118+
In this case, attempt to wrap everything until the next `resumeDispatcher()` in
119+
a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of
120+
`StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where
121+
execution happens).
122+
123+
## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()`
124+
125+
For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated.
126+
It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the
127+
tasks scheduled *at* `currentTime + n`.
128+
129+
There is an automatic replacement for this deprecation, which produces correct but inelegant code.
130+
131+
Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not
132+
encounter this edge case.
133+
134+
## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())`
135+
136+
This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with
137+
`TestScope`.
138+
139+
Significant differences of `runTest` from `runBlockingTest` are each given a section below.
140+
141+
### It works properly with other dispatchers and asynchronous completions.
142+
143+
No action on your part is required, other than replacing `runBlocking` with `runTest` as well.
144+
145+
### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`.
146+
147+
By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
148+
variant of `TestCoroutineDispatcher` should be used.
149+
This version of the dispatcher, which can be observed has the property of eagerly entering `launch` and `async` blocks:
150+
code until the first suspension is executed without dispatching.
151+
152+
We ensured sure that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
153+
blocks, but *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
154+
any guarantees about their dispatching order.
155+
156+
So, using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
157+
did, but in the possible case that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, it
158+
will need to be tweaked.
159+
If the test expects some code to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
160+
at this moment of time to run.
161+
162+
### The job hierarchy is completely different.
163+
164+
- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the
165+
created coroutine.
166+
- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`.
167+
- The job passed as an argument is used as a parent job.
168+
169+
Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a
170+
`SupervisorJob`; this should make the job hierarchy resemble what it used to be.
171+
172+
```kotlin
173+
@Test
174+
fun testFoo() = runTest {
175+
val deferred = async(SupervisorJob()) {
176+
// test code
177+
}
178+
advanceUntilIdle()
179+
deferred.getCompletionExceptionOrNull()?.let {
180+
throw it
181+
}
182+
}
183+
```
184+
185+
### Only a single call to `runTest` is permitted per test.
186+
187+
In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned
188+
immediately:
189+
190+
```kotlin
191+
@Test
192+
fun testFoo(): TestResult {
193+
// arbitrary code here
194+
return runTest {
195+
// ...
196+
}
197+
}
198+
```
199+
200+
When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported.
201+
Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue
202+
tracker.
203+
204+
### It uses `TestScope`, not `TestCoroutineScope`, by default.
205+
206+
There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating
207+
from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and
208+
`TestScope` will not suffice.
209+
210+
## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest`
211+
212+
Likely can be done together with the next step.
213+
214+
Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base.
215+
Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside
216+
the `runTest` block.
217+
218+
The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup.
219+
If a test must check that no other delays are remaining after it has finished, the following form may help:
220+
```kotlin
221+
runTest {
222+
testBody()
223+
val timeAfterTest = currentTime()
224+
advanceUntilIdle() // run the remaining tasks
225+
assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment
226+
}
227+
```
228+
Note that this will report time advancement even if the job scheduled at a later point was cancelled.
229+
230+
It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens
231+
outside the test itself.
232+
In this case, we propose that you write a wrapper of the form:
233+
234+
```kotlin
235+
fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest {
236+
try {
237+
body()
238+
} finally {
239+
// the usual cleanup procedures that used to happen before `cleanupTestCoroutines`
240+
}
241+
}
242+
```
243+
244+
## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope`
245+
246+
Also, replace `runTestWithLegacyScope` with just `runTest`.
247+
All of this can be done in parallel with replacing `runBlockingTest` with `runTest`.
248+
249+
This step should remove all uses of `TestCoroutineScope`, explicit or implicit.
250+
251+
Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be
252+
straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it.
253+
Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest`
254+
handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of
255+
`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them.
256+
257+
Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`,
258+
and its usages should have been removed during the previous step.
259+
260+
## Replace `runBlocking` with `runTest`
261+
262+
Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful.
263+
As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other
264+
threads, like `Dispatchers.IO` or `Dispatchers.Default`.
265+
266+
## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher`
267+
268+
`TestCoroutineDispatcher` is a dispatcher with two modes:
269+
* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks.
270+
* ("paused") Behaving like a `StandardTestDispatcher`.
271+
272+
In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the
273+
implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to
274+
`runTest`.
275+
276+
Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate.
277+
278+
## Simplify code by removing unneeded entities
279+
280+
Likely, now some code has the form
281+
282+
```kotlin
283+
val dispatcher = StandardTestDispatcher()
284+
val scope = TestScope(dispatcher)
285+
286+
@BeforeTest
287+
fun setUp() {
288+
Dispatchers.setMain(dispatcher)
289+
}
290+
291+
@AfterTest
292+
fun tearDown() {
293+
Dispatchers.resetMain()
294+
}
295+
296+
@Test
297+
fun testFoo() = scope.runTest {
298+
// ...
299+
}
300+
```
301+
302+
The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for
303+
`Dispatchers.Main`.
304+
305+
However, now this can be simplified to just
306+
307+
```kotlin
308+
@BeforeTest
309+
fun setUp() {
310+
Dispatchers.setMain(StandardTestDispatcher())
311+
}
312+
313+
@AfterTest
314+
fun tearDown() {
315+
Dispatchers.resetMain()
316+
}
317+
318+
@Test
319+
fun testFoo() = runTest {
320+
// ...
321+
}
322+
```
323+
324+
The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from
325+
the current `Dispatchers.Main`.

0 commit comments

Comments
 (0)