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]>
diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
index 36a516e..ee4d8bf 100644
--- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
+++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
@@ -556,6 +556,15 @@
public static final fun withTimeoutOrNull-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
+public final class kotlinx/coroutines/YieldContext : kotlin/coroutines/AbstractCoroutineContextElement {
+ public static final field Key Lkotlinx/coroutines/YieldContext$Key;
+ public field dispatcherWasUnconfined Z
+ public fun <init> ()V
+}
+
+public final class kotlinx/coroutines/YieldContext$Key : kotlin/coroutines/CoroutineContext$Key {
+}
+
public final class kotlinx/coroutines/YieldKt {
public static final fun yield (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
index e178332..da094e1 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
@@ -12,6 +12,7 @@
*/
public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext
+@PublishedApi
@Suppress("PropertyName")
internal expect val DefaultDelay: Delay
diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt
index 24a401f..5837ae8 100644
--- a/kotlinx-coroutines-core/common/src/Unconfined.kt
+++ b/kotlinx-coroutines-core/common/src/Unconfined.kt
@@ -38,6 +38,7 @@
/**
* Used to detect calls to [Unconfined.dispatch] from [yield] function.
*/
+@PublishedApi
internal class YieldContext : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<YieldContext>
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
index 84d9d9f..41f759c 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
@@ -968,7 +968,6 @@
* Checks if the thread is part of a thread pool that supports coroutines.
* This function is needed for integration with BlockHound.
*/
-@Suppress("UNUSED")
@JvmName("isSchedulerWorker")
internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker
@@ -976,7 +975,6 @@
* Checks if the thread is running a CPU-bound task.
* This function is needed for integration with BlockHound.
*/
-@Suppress("UNUSED")
@JvmName("mayNotBlock")
internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker &&
thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED
diff --git a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
index e7fe1e6..9cafffb 100644
--- a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
+++ b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
@@ -10,7 +10,6 @@
import reactor.blockhound.*
import reactor.blockhound.integration.*
-@Suppress("UNUSED")
public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) {
diff --git a/kotlinx-coroutines-test/MIGRATION.md b/kotlinx-coroutines-test/MIGRATION.md
new file mode 100644
index 0000000..5124864
--- /dev/null
+++ b/kotlinx-coroutines-test/MIGRATION.md
@@ -0,0 +1,325 @@
+# Migration to the new kotlinx-coroutines-test API
+
+In version 1.6.0, the API of the test module changed significantly.
+This is a guide for gradually adapting the existing test code to the new API.
+This guide is written step-by-step; the idea is to separate the migration into several sets of small changes.
+
+## Remove custom `UncaughtExceptionCaptor`, `DelayController`, and `TestCoroutineScope` implementations
+
+We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that
+you don't need to do anything for this section.
+
+### `UncaughtExceptionCaptor`
+
+If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler`
+was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure
+was called.
+
+We currently don't provide a replacement for this.
+However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines
+are propagated structurally, which makes uncaught exception handlers less useful.
+
+If you have a use case for this, please tell us about it at the issue tracker.
+Meanwhile, it should be possible to use a custom exception captor, which should only implement
+`CoroutineExceptionHandler` now, like this:
+
+```kotlin
+@Test
+fun testFoo() = runTest {
+ val customCaptor = MyUncaughtExceptionCaptor()
+ launch(customCaptor) {
+ // ...
+ }
+ advanceUntilIdle()
+ customCaptor.cleanupTestCoroutines()
+}
+```
+
+### `DelayController`
+
+We don't provide a way to define custom dispatching strategies that support virtual time.
+That said, we significantly enhanced this mechanism:
+* Using multiple test dispatchers simultaneously is supported.
+ For the dispatchers to have a shared knowledge of the virtual time, either the same `TestCoroutineScheduler` should be
+ passed to each of them, or all of them should be constructed after `Dispatchers.setMain` is called with some test
+ dispatcher.
+* Both a simple `StandardTestDispatcher` that is always paused, and unconfined `UnconfinedTestDispatcher` are provided.
+
+If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue
+tracker.
+
+### `TestCoroutineScope`
+
+This scope couldn't be meaningfully used in tandem with `runBlockingTest`: according to the definition of
+`TestCoroutineScope.runBlockingTest`, only the scope's `coroutineContext` is used.
+So, there could be two reasons for defining a custom implementation:
+
+* Avoiding the restrictions on placed `coroutineContext` in the `TestCoroutineScope` constructor function.
+ These restrictions consisted of requirements for `CoroutineExceptionHandler` being an `UncaughtExceptionCaptor`, and
+ `ContinuationInterceptor` being a `DelayController`, so it is also possible to fulfill these restrictions by defining
+ conforming instances. In this case, follow the instructions about replacing them.
+* Using without `runBlockingTest`. In this case, you don't even need to implement `TestCoroutineScope`: nothing else
+ accepts a `TestCoroutineScope` specifically as an argument.
+
+## Remove usages of `TestCoroutineExceptionHandler` and `TestCoroutineScope.uncaughtExceptions`
+
+It is already illegal to use a `TestCoroutineScope` without performing `cleanupTestCoroutines`, so the valid uses of
+`TestCoroutineExceptionHandler` include:
+
+* Accessing `uncaughtExceptions` in the middle of the test to make sure that there weren't any uncaught exceptions
+ *yet*.
+ If there are any, they will be thrown by the cleanup procedure anyway.
+ We don't support this use case, given how comparatively rare it is, but it can be handled in the same way as the
+ following one.
+* Accessing `uncaughtExceptions` when the uncaught exceptions are actually expected.
+ In this case, `cleanupTestCoroutines` will fail with an exception that is being caught later.
+ It would be better in this case to use a custom `CoroutineExceptionHandler` so that actual problems that could be
+ found by the cleanup procedure are not superseded by the exceptions that are expected.
+ An example is shown below.
+
+```kotlin
+val exceptions = mutableListOf<Throwable>()
+val customCaptor = CoroutineExceptionHandler { ctx, throwable ->
+ exceptions.add(throwable) // add proper synchronization if the test is multithreaded
+}
+
+@Test
+fun testFoo() = runTest {
+ launch(customCaptor) {
+ // ...
+ }
+ advanceUntilIdle()
+ // check the list of the caught exceptions
+}
+```
+
+## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope`
+
+This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`.
+If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case,
+also pass this scheduler as the argument to the dispatcher.
+
+## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher`
+
+* In places where `pauseDispatcher` in its block form is called, replace it with a call to
+ `withContext(StandardTestDispatcher(testScheduler))`
+ (`testScheduler` is available as a field of `TestCoroutineScope`,
+ or `scheduler` is available as a field of `TestCoroutineDispatcher`),
+ followed by `advanceUntilIdle()`.
+ This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused
+ when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`.
+* Often, `pauseDispatcher()` in a non-block form is used at the start of the test.
+ Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`,
+ if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used,
+ or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`.
+ This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming,
+ instead of the deprecated `TestCoroutineDispatcher`.
+* Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test.
+ In this case, attempt to wrap everything until the next `resumeDispatcher()` in
+ a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of
+ `StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where
+ execution happens).
+
+## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()`
+
+For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated.
+It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the
+tasks scheduled *at* `currentTime + n`.
+
+There is an automatic replacement for this deprecation, which produces correct but inelegant code.
+
+Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not
+encounter this edge case.
+
+## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())`
+
+This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with
+`TestScope`.
+
+Significant differences of `runTest` from `runBlockingTest` are each given a section below.
+
+### It works properly with other dispatchers and asynchronous completions.
+
+No action on your part is required, other than replacing `runBlocking` with `runTest` as well.
+
+### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`.
+
+By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
+variant of `TestCoroutineDispatcher` should be used.
+This version of the dispatcher, which can be observed has the property of eagerly entering `launch` and `async` blocks:
+code until the first suspension is executed without dispatching.
+
+We ensured sure that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
+blocks, but *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
+any guarantees about their dispatching order.
+
+So, using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
+did, but in the possible case that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, it
+will need to be tweaked.
+If the test expects some code to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
+at this moment of time to run.
+
+### The job hierarchy is completely different.
+
+- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the
+ created coroutine.
+- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`.
+- The job passed as an argument is used as a parent job.
+
+Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a
+`SupervisorJob`; this should make the job hierarchy resemble what it used to be.
+
+```kotlin
+@Test
+fun testFoo() = runTest {
+ val deferred = async(SupervisorJob()) {
+ // test code
+ }
+ advanceUntilIdle()
+ deferred.getCompletionExceptionOrNull()?.let {
+ throw it
+ }
+}
+```
+
+### Only a single call to `runTest` is permitted per test.
+
+In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned
+immediately:
+
+```kotlin
+@Test
+fun testFoo(): TestResult {
+ // arbitrary code here
+ return runTest {
+ // ...
+ }
+}
+```
+
+When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported.
+Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue
+tracker.
+
+### It uses `TestScope`, not `TestCoroutineScope`, by default.
+
+There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating
+from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and
+`TestScope` will not suffice.
+
+## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest`
+
+Likely can be done together with the next step.
+
+Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base.
+Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside
+the `runTest` block.
+
+The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup.
+If a test must check that no other delays are remaining after it has finished, the following form may help:
+```kotlin
+runTest {
+ testBody()
+ val timeAfterTest = currentTime()
+ advanceUntilIdle() // run the remaining tasks
+ assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment
+}
+```
+Note that this will report time advancement even if the job scheduled at a later point was cancelled.
+
+It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens
+outside the test itself.
+In this case, we propose that you write a wrapper of the form:
+
+```kotlin
+fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest {
+ try {
+ body()
+ } finally {
+ // the usual cleanup procedures that used to happen before `cleanupTestCoroutines`
+ }
+}
+```
+
+## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope`
+
+Also, replace `runTestWithLegacyScope` with just `runTest`.
+All of this can be done in parallel with replacing `runBlockingTest` with `runTest`.
+
+This step should remove all uses of `TestCoroutineScope`, explicit or implicit.
+
+Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be
+straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it.
+Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest`
+handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of
+`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them.
+
+Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`,
+and its usages should have been removed during the previous step.
+
+## Replace `runBlocking` with `runTest`
+
+Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful.
+As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other
+threads, like `Dispatchers.IO` or `Dispatchers.Default`.
+
+## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher`
+
+`TestCoroutineDispatcher` is a dispatcher with two modes:
+* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks.
+* ("paused") Behaving like a `StandardTestDispatcher`.
+
+In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the
+implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to
+`runTest`.
+
+Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate.
+
+## Simplify code by removing unneeded entities
+
+Likely, now some code has the form
+
+```kotlin
+val dispatcher = StandardTestDispatcher()
+val scope = TestScope(dispatcher)
+
+@BeforeTest
+fun setUp() {
+ Dispatchers.setMain(dispatcher)
+}
+
+@AfterTest
+fun tearDown() {
+ Dispatchers.resetMain()
+}
+
+@Test
+fun testFoo() = scope.runTest {
+ // ...
+}
+```
+
+The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for
+`Dispatchers.Main`.
+
+However, now this can be simplified to just
+
+```kotlin
+@BeforeTest
+fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+}
+
+@AfterTest
+fun tearDown() {
+ Dispatchers.resetMain()
+}
+
+@Test
+fun testFoo() = runTest {
+ // ...
+}
+```
+
+The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from
+the current `Dispatchers.Main`.
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md
index 43ae18f..54450b1 100644
--- a/kotlinx-coroutines-test/README.md
+++ b/kotlinx-coroutines-test/README.md
@@ -2,7 +2,24 @@
Test utilities for `kotlinx.coroutines`.
-This package provides testing utilities for effectively testing coroutines.
+## Overview
+
+This package provides utilities for efficiently testing coroutines.
+
+| Name | Description |
+| ---- | ----------- |
+| [runTest] | Runs the test code, automatically skipping delays and handling uncaught exceptions. |
+| [TestCoroutineScheduler] | The shared source of virtual time, used for controlling execution order and skipping delays. |
+| [TestScope] | A [CoroutineScope] that integrates with [runTest], providing access to [TestCoroutineScheduler]. |
+| [TestDispatcher] | A [CoroutineDispatcher] that whose delays are controlled by a [TestCoroutineScheduler]. |
+| [Dispatchers.setMain] | Mocks the main dispatcher using the provided one. If mocked with a [TestDispatcher], its [TestCoroutineScheduler] is used everywhere by default. |
+
+Provided [TestDispatcher] implementations:
+
+| Name | Description |
+| ---- | ----------- |
+| [StandardTestDispatcher] | A simple dispatcher with no special behavior other than being linked to a [TestCoroutineScheduler]. |
+| [UnconfinedTestDispatcher] | A dispatcher that behaves like [Dispatchers.Unconfined]. |
## Using in your project
@@ -13,24 +30,26 @@
}
```
-**Do not** depend on this project in your main sources, all utilities are intended and designed to be used only from tests.
+**Do not** depend on this project in your main sources, all utilities here are intended and designed to be used only from tests.
## Dispatchers.Main Delegation
-`Dispatchers.setMain` will override the `Main` dispatcher in test situations. This is helpful when you want to execute a
-test in situations where the platform `Main` dispatcher is not available, or you wish to replace `Dispatchers.Main` with a
-testing dispatcher.
+`Dispatchers.setMain` will override the `Main` dispatcher in test scenarios.
+This is helpful when one wants to execute a test in situations where the platform `Main` dispatcher is not available,
+or to replace `Dispatchers.Main` with a testing dispatcher.
-Once you have this dependency in the runtime,
-[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism will overwrite
-[Dispatchers.Main] with a testable implementation.
+On the JVM,
+the [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism is responsible
+for overwriting [Dispatchers.Main] with a testable implementation, which by default will delegate its calls to the real
+`Main` dispatcher, if any.
-You can override the `Main` implementation using [setMain][setMain] method with any [CoroutineDispatcher] implementation, e.g.:
+The `Main` implementation can be overridden using [Dispatchers.setMain][setMain] method with any [CoroutineDispatcher]
+implementation, e.g.:
```kotlin
class SomeTest {
-
+
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
@Before
@@ -40,10 +59,10 @@
@After
fun tearDown() {
- Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
+ Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
-
+
@Test
fun testSomeUI() = runBlocking {
launch(Dispatchers.Main) { // Will be launched in the mainThreadSurrogate dispatcher
@@ -52,372 +71,289 @@
}
}
```
-Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. The testable version of
-`Dispatchers.Main` installed by the `ServiceLoader` will delegate to the dispatcher provided by `setMain`.
-## runBlockingTest
+Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally.
-To test regular suspend functions or coroutines started with `launch` or `async` use the [runBlockingTest] coroutine
-builder that provides extra test control to coroutines.
+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.
-1. Auto-advancing of time for regular suspend functions
-2. Explicit time control for testing multiple coroutines
-3. Eager execution of `launch` or `async` code blocks
-4. Pause, manually advance, and restart the execution of coroutines in a test
-5. Report uncaught exceptions as test failures
+## runTest
-### Testing regular suspend functions
+[runTest] is the way to test code that involves coroutines. `suspend` functions can be called inside it.
-To test regular suspend functions, which may have a delay, you can use the [runBlockingTest] builder to start a testing
-coroutine. Any calls to `delay` will automatically advance virtual time by the amount delayed.
+**IMPORTANT: in order to work with on Kotlin/JS, the result of `runTest` must be immediately `return`-ed from each test.**
+The typical invocation of [runTest] thus looks like this:
```kotlin
@Test
-fun testFoo() = runBlockingTest { // a coroutine with an extra test control
- val actual = foo()
+fun testFoo() = runTest {
+ // code under test
+}
+```
+
+In more advanced scenarios, it's possible instead to use the following form:
+```kotlin
+@Test
+fun testFoo(): TestResult {
+ // initialize some test state
+ return runTest {
+ // code under test
+ }
+}
+```
+
+[runTest] is similar to running the code with `runBlocking` on Kotlin/JVM and Kotlin/Native, or launching a new promise
+on Kotlin/JS. The main differences are the following:
+
+* **The calls to `delay` are automatically skipped**, preserving the relative execution order of the tasks. This way,
+ it's possible to make tests finish more-or-less immediately.
+* **Controlling the virtual time**: in case just skipping delays is not sufficient, it's possible to more carefully
+ guide the execution, advancing the virtual time by a duration, draining the queue of the awaiting tasks, or running
+ the tasks scheduled at the present moment.
+* **Handling uncaught exceptions** spawned in the child coroutines by throwing them at the end of the test.
+* **Waiting for asynchronous callbacks**.
+ Sometimes, especially when working with third-party code, it's impossible to mock all the dispatchers in use.
+ [runTest] will handle the situations where some code runs in dispatchers not integrated with the test module.
+
+## Delay-skipping
+
+To test regular suspend functions, which may have a delay, just run them inside the [runTest] block.
+
+```kotlin
+@Test
+fun testFoo() = runTest { // a coroutine with an extra test control
+ val actual = foo()
// ...
}
suspend fun foo() {
- delay(1_000) // auto-advances virtual time by 1_000ms due to runBlockingTest
+ delay(1_000) // when run in `runTest`, will finish immediately instead of delaying
// ...
}
```
-`runBlockingTest` returns `Unit` so it may be used in a single expression with common testing libraries.
+## `launch` and `async`
-### Testing `launch` or `async`
+The coroutine dispatcher used for tests is single-threaded, meaning that the child coroutines of the [runTest] block
+will run on the thread that started the test, and will never run in parallel.
-Inside of [runBlockingTest], both [launch] and [async] will start a new coroutine that may run concurrently with the
-test case.
-
-To make common testing situations easier, by default the body of the coroutine is executed *eagerly* until
-the first call to [delay] or [yield].
+If several coroutines are waiting to be executed next, the one scheduled after the smallest delay will be chosen.
+The virtual time will automatically advance to the point of its resumption.
```kotlin
@Test
-fun testFooWithLaunch() = runBlockingTest {
- foo()
- // the coroutine launched by foo() is completed before foo() returns
- // ...
-}
-
-fun CoroutineScope.foo() {
- // This coroutines `Job` is not shared with the test code
- launch {
- bar() // executes eagerly when foo() is called due to runBlockingTest
- println(1) // executes eagerly when foo() is called due to runBlockingTest
- }
-}
-
-suspend fun bar() {}
-```
-
-`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines
-are not able to complete, an [UncompletedCoroutinesError] will be thrown.
-
-*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters.
-
-### Testing `launch` or `async` with `delay`
-
-If the coroutine created by `launch` or `async` calls `delay` then the [runBlockingTest] will not auto-progress time
-right away. This allows tests to observe the interaction of multiple coroutines with different delays.
-
-To control time in the test you can use the [DelayController] interface. The block passed to
-[runBlockingTest] can call any method on the `DelayController` interface.
-
-```kotlin
-@Test
-fun testFooWithLaunchAndDelay() = runBlockingTest {
- foo()
- // the coroutine launched by foo has not completed here, it is suspended waiting for delay(1_000)
- advanceTimeBy(1_000) // progress time, this will cause the delay to resume
- // the coroutine launched by foo has completed here
- // ...
-}
-
-suspend fun CoroutineScope.foo() {
+fun testWithMultipleDelays() = runTest {
launch {
- println(1) // executes eagerly when foo() is called due to runBlockingTest
- delay(1_000) // suspends until time is advanced by at least 1_000
- println(2) // executes after advanceTimeBy(1_000)
+ delay(1_000)
+ println("1. $currentTime") // 1000
+ delay(200)
+ println("2. $currentTime") // 1200
+ delay(2_000)
+ println("4. $currentTime") // 3200
}
+ val deferred = async {
+ delay(3_000)
+ println("3. $currentTime") // 3000
+ delay(500)
+ println("5. $currentTime") // 3500
+ }
+ deferred.await()
}
```
-*Note:* `runBlockingTest` will always attempt to auto-progress time until all coroutines are completed just before
-exiting. This is a convenience to avoid having to call [advanceUntilIdle][DelayController.advanceUntilIdle]
-as the last line of many common test cases.
-If any coroutines cannot complete by advancing time, an [UncompletedCoroutinesError] is thrown.
+## Controlling the virtual time
-### Testing `withTimeout` using `runBlockingTest`
-
-Time control can be used to test timeout code. To do so, ensure that the function under test is suspended inside a
-`withTimeout` block and advance time until the timeout is triggered.
-
-Depending on the code, causing the code to suspend may need to use different mocking or fake techniques. For this
-example an uncompleted `Deferred<Foo>` is provided to the function under test via parameter injection.
+Inside [runTest], the following operations are supported:
+* `currentTime` gets the current virtual time.
+* `runCurrent()` runs the tasks that are scheduled at this point of virtual time.
+* `advanceUntilIdle()` runs all enqueued tasks until there are no more.
+* `advanceTimeBy(timeDelta)` runs the enqueued tasks until the current virtual time advances by `timeDelta`.
```kotlin
-@Test(expected = TimeoutCancellationException::class)
-fun testFooWithTimeout() = runBlockingTest {
- val uncompleted = CompletableDeferred<Foo>() // this Deferred<Foo> will never complete
- foo(uncompleted)
- advanceTimeBy(1_000) // advance time, which will cause the timeout to throw an exception
- // ...
+@Test
+fun testFoo() = runTest {
+ launch {
+ println(1) // executes during runCurrent()
+ delay(1_000) // suspends until time is advanced by at least 1_000
+ println(2) // executes during advanceTimeBy(2_000)
+ delay(500) // suspends until the time is advanced by another 500 ms
+ println(3) // also executes during advanceTimeBy(2_000)
+ delay(5_000) // will suspend by another 4_500 ms
+ println(4) // executes during advanceUntilIdle()
+ }
+ // the child coroutine has not run yet
+ runCurrent()
+ // the child coroutine has called println(1), and is suspended on delay(1_000)
+ advanceTimeBy(2_000) // progress time, this will cause two calls to `delay` to resume
+ // the child coroutine has called println(2) and println(3) and suspends for another 4_500 virtual milliseconds
+ advanceUntilIdle() // will run the child coroutine to completion
+ assertEquals(6500, currentTime) // the child coroutine finished at virtual time of 6_500 milliseconds
+}
+```
+
+## Using multiple test dispatchers
+
+The virtual time is controlled by an entity called the [TestCoroutineScheduler], which behaves as the shared source of
+virtual time.
+
+Several dispatchers can be created that use the same [TestCoroutineScheduler], in which case they will share their
+knowledge of the virtual time.
+
+To access the scheduler used for this test, use the [TestScope.testScheduler] property.
+
+```kotlin
+@Test
+fun testWithMultipleDispatchers() = runTest {
+ val scheduler = testScheduler // the scheduler used for this test
+ val dispatcher1 = StandardTestDispatcher(scheduler, name = "IO dispatcher")
+ val dispatcher2 = StandardTestDispatcher(scheduler, name = "Background dispatcher")
+ launch(dispatcher1) {
+ delay(1_000)
+ println("1. $currentTime") // 1000
+ delay(200)
+ println("2. $currentTime") // 1200
+ delay(2_000)
+ println("4. $currentTime") // 3200
+ }
+ val deferred = async(dispatcher2) {
+ delay(3_000)
+ println("3. $currentTime") // 3000
+ delay(500)
+ println("5. $currentTime") // 3500
+ }
+ deferred.await()
+ }
+```
+
+**Note: if [Dispatchers.Main] is replaced by a [TestDispatcher], [runTest] will automatically use its scheduler.
+This is done so that there is no need to go through the ceremony of passing the correct scheduler to [runTest].**
+
+## Accessing the test coroutine scope
+
+Structured concurrency ties coroutines to scopes in which they are launched.
+[TestScope] is a special coroutine scope designed for testing coroutines, and a new one is automatically created
+for [runTest] and used as the receiver for the test body.
+
+However, it can be convenient to access a `CoroutineScope` before the test has started, for example, to perform mocking
+of some
+parts of the system in `@BeforeTest` via dependency injection.
+In these cases, it is possible to manually create [TestScope], the scope for the test coroutines, in advance,
+before the test begins.
+
+[TestScope] on its own does not automatically run the code launched in it.
+In addition, it is stateful in order to keep track of executing coroutines and uncaught exceptions.
+Therefore, it is important to ensure that [TestScope.runTest] is called eventually.
+
+```kotlin
+val scope = TestScope()
+
+@BeforeTest
+fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler))
+ TestSubject.setScope(scope)
}
-fun CoroutineScope.foo(resultDeferred: Deferred<Foo>) {
+@AfterTest
+fun tearDown() {
+ Dispatchers.resetMain()
+ TestSubject.resetScope()
+}
+
+@Test
+fun testSubject() = scope.runTest {
+ // the receiver here is `testScope`
+}
+```
+
+## Eagerly entering `launch` and `async` blocks
+
+Some tests only test functionality and don't particularly care about the precise order in which coroutines are
+dispatched.
+In these cases, it can be cumbersome to always call [runCurrent] or [yield] to observe the effects of the coroutines
+after they are launched.
+
+If [runTest] executes with an [UnconfinedTestDispatcher], the child coroutines launched at the top level are entered
+*eagerly*, that is, they don't go through a dispatch until the first suspension.
+
+```kotlin
+@Test
+fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ var entered = false
+ val deferred = CompletableDeferred<Unit>()
+ var completed = false
launch {
+ entered = true
+ deferred.await()
+ completed = true
+ }
+ assertTrue(entered) // `entered = true` already executed.
+ assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
+ deferred.complete(Unit) // resume the coroutine.
+ assertTrue(completed) // now the child coroutine is immediately completed.
+}
+```
+
+If this behavior is desirable, but some parts of the test still require accurate dispatching, for example, to ensure
+that the code executes on the correct thread, then simply `launch` a new coroutine with the [StandardTestDispatcher].
+
+```kotlin
+@Test
+fun testEagerlyEnteringSomeChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ var entered1 = false
+ launch {
+ entered1 = true
+ }
+ assertTrue(entered1) // `entered1 = true` already executed
+
+ var entered2 = false
+ launch(StandardTestDispatcher(testScheduler)) {
+ // this block and every coroutine launched inside it will explicitly go through the needed dispatches
+ entered2 = true
+ }
+ assertFalse(entered2)
+ runCurrent() // need to explicitly run the dispatched continuation
+ assertTrue(entered2)
+}
+```
+
+### Using `withTimeout` inside `runTest`
+
+Timeouts are also susceptible to time control, so the code below will immediately finish.
+
+```kotlin
+@Test
+fun testFooWithTimeout() = runTest {
+ assertFailsWith<TimeoutCancellationException> {
withTimeout(1_000) {
- resultDeferred.await() // await() will suspend forever waiting for uncompleted
- // ...
+ delay(999)
+ delay(2)
+ println("this won't be reached")
}
}
}
```
-*Note:* Testing timeouts is simpler with a second coroutine that can be suspended (as in this example). If the
-call to `withTimeout` is in a regular suspend function, consider calling `launch` or `async` inside your test body to
-create a second coroutine.
+## Virtual time support with other dispatchers
-### Using `pauseDispatcher` for explicit execution of `runBlockingTest`
-
-The eager execution of `launch` and `async` bodies makes many tests easier, but some tests need more fine grained
-control of coroutine execution.
-
-To disable eager execution, you can call [pauseDispatcher][DelayController.pauseDispatcher]
-to pause the [TestCoroutineDispatcher] that [runBlockingTest] uses.
-
-When the dispatcher is paused, all coroutines will be added to a queue instead running. In addition, time will never
-auto-progress due to `delay` on a paused dispatcher.
+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.
```kotlin
-@Test
-fun testFooWithPauseDispatcher() = runBlockingTest {
- pauseDispatcher {
- foo()
- // the coroutine started by foo has not run yet
- runCurrent() // the coroutine started by foo advances to delay(1_000)
- // the coroutine started by foo has called println(1), and is suspended on delay(1_000)
- advanceTimeBy(1_000) // progress time, this will cause the delay to resume
- // the coroutine started by foo has called println(2) and has completed here
- }
- // ...
-}
-
-fun CoroutineScope.foo() {
- launch {
- println(1) // executes after runCurrent() is called
- delay(1_000) // suspends until time is advanced by at least 1_000
- println(2) // executes after advanceTimeBy(1_000)
- }
-}
-```
-
-Using `pauseDispatcher` gives tests explicit control over the progress of time as well as the ability to enqueue all
-coroutines. As a best practice consider adding two tests, one paused and one eager, to test coroutines that have
-non-trivial external dependencies and side effects in their launch body.
-
-*Important:* When passed a lambda block, `pauseDispatcher` will resume eager execution immediately after the block.
-This will cause time to auto-progress if there are any outstanding `delay` calls that were not resolved before the
-`pauseDispatcher` block returned. In advanced situations tests can call [pauseDispatcher][DelayController.pauseDispatcher]
-without a lambda block and then explicitly resume the dispatcher with [resumeDispatcher][DelayController.resumeDispatcher].
-
-## Integrating tests with structured concurrency
-
-Code that uses structured concurrency needs a [CoroutineScope] in order to launch a coroutine. In order to integrate
-[runBlockingTest] with code that uses common structured concurrency patterns tests can provide one (or both) of these
-classes to application code.
-
- | Name | Description |
- | ---- | ----------- |
- | [TestCoroutineScope] | A [CoroutineScope] which provides detailed control over the execution of coroutines for tests and integrates with [runBlockingTest]. |
- | [TestCoroutineDispatcher] | A [CoroutineDispatcher] which can be used for tests and integrates with [runBlockingTest]. |
-
- Both classes are provided to allow for various testing needs. Depending on the code that's being
- tested, it may be easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain] will accept
- a [TestCoroutineDispatcher] but not a [TestCoroutineScope].
-
- [TestCoroutineScope] will always use a [TestCoroutineDispatcher] to execute coroutines. It
- also uses [TestCoroutineExceptionHandler] to convert uncaught exceptions into test failures.
-
-By providing [TestCoroutineScope] a test case is able to control execution of coroutines, as well as ensure that
-uncaught exceptions thrown by coroutines are converted into test failures.
-
-### Providing `TestCoroutineScope` from `runBlockingTest`
-
-In simple cases, tests can use the [TestCoroutineScope] created by [runBlockingTest] directly.
-
-```kotlin
-@Test
-fun testFoo() = runBlockingTest {
- foo() // runBlockingTest passed in a TestCoroutineScope as this
-}
-
-fun CoroutineScope.foo() {
- launch { // CoroutineScope for launch is the TestCoroutineScope provided by runBlockingTest
- // ...
- }
-}
-```
-
-This style is preferred when the `CoroutineScope` is passed through an extension function style.
-
-### Providing an explicit `TestCoroutineScope`
-
-In many cases, the direct style is not preferred because [CoroutineScope] may need to be provided through another means
-such as dependency injection or service locators.
-
-Tests can declare a [TestCoroutineScope] explicitly in the class to support these use cases.
-
-Since [TestCoroutineScope] is stateful in order to keep track of executing coroutines and uncaught exceptions, it is
-important to ensure that [cleanupTestCoroutines][TestCoroutineScope.cleanupTestCoroutines] is called after every test case.
-
-```kotlin
-class TestClass {
- private val testScope = TestCoroutineScope()
- private lateinit var subject: Subject
-
- @Before
- fun setup() {
- // provide the scope explicitly, in this example using a constructor parameter
- subject = Subject(testScope)
- }
-
- @After
- fun cleanUp() {
- testScope.cleanupTestCoroutines()
- }
-
- @Test
- fun testFoo() = testScope.runBlockingTest {
- // TestCoroutineScope.runBlockingTest uses the Dispatcher and exception handler provided by `testScope`
- subject.foo()
- }
-}
-
-class Subject(val scope: CoroutineScope) {
- fun foo() {
- scope.launch {
- // launch uses the testScope injected in setup
- }
- }
-}
-```
-
-*Note:* [TestCoroutineScope], [TestCoroutineDispatcher], and [TestCoroutineExceptionHandler] are interfaces to enable
-test libraries to provide library specific integrations. For example, a JUnit4 `@Rule` may call
-[Dispatchers.setMain][setMain] then expose [TestCoroutineScope] for use in tests.
-
-### Providing an explicit `TestCoroutineDispatcher`
-
-While providing a [TestCoroutineScope] is slightly preferred due to the improved uncaught exception handling, there are
-many situations where it is easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain]
-does not accept a [TestCoroutineScope] and requires a [TestCoroutineDispatcher] to control coroutine execution in
-tests.
-
-The main difference between `TestCoroutineScope` and `TestCoroutineDispatcher` is how uncaught exceptions are handled.
-When using `TestCoroutineDispatcher` uncaught exceptions thrown in coroutines will use regular
-[coroutine exception handling](https://kotlinlang.org/docs/reference/coroutines/exception-handling.html).
-`TestCoroutineScope` will always use `TestCoroutineDispatcher` as it's dispatcher.
-
-A test can use a `TestCoroutineDispatcher` without declaring an explicit `TestCoroutineScope`. This is preferred
-when the class under test allows a test to provide a [CoroutineDispatcher] but does not allow the test to provide a
-[CoroutineScope].
-
-Since [TestCoroutineDispatcher] is stateful in order to keep track of executing coroutines, it is
-important to ensure that [cleanupTestCoroutines][DelayController.cleanupTestCoroutines] is called after every test case.
-
-```kotlin
-class TestClass {
- private val testDispatcher = TestCoroutineDispatcher()
-
- @Before
- fun setup() {
- // provide the scope explicitly, in this example using a constructor parameter
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- fun cleanUp() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
- @Test
- fun testFoo() = testDispatcher.runBlockingTest {
- // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines
- foo()
- }
-}
-
-fun foo() {
- MainScope().launch {
- // launch will use the testDispatcher provided by setMain
- }
-}
-```
-
-*Note:* Prefer to provide `TestCoroutineScope` when it does not complicate code since it will also elevate exceptions
-to test failures. However, exposing a `CoroutineScope` to callers of a function may lead to complicated code, in which
-case this is the preferred pattern.
-
-### Using `TestCoroutineScope` and `TestCoroutineDispatcher` without `runBlockingTest`
-
-It is supported to use both [TestCoroutineScope] and [TestCoroutineDispatcher] without using the [runBlockingTest]
-builder. Tests may need to do this in situations such as introducing multiple dispatchers and library writers may do
-this to provide alternatives to `runBlockingTest`.
-
-```kotlin
-@Test
-fun testFooWithAutoProgress() {
- val scope = TestCoroutineScope()
- scope.foo()
- // foo is suspended waiting for time to progress
- scope.advanceUntilIdle()
- // foo's coroutine will be completed before here
-}
-
-fun CoroutineScope.foo() {
- launch {
- println(1) // executes eagerly when foo() is called due to TestCoroutineScope
- delay(1_000) // suspends until time is advanced by at least 1_000
- println(2) // executes after advanceTimeUntilIdle
- }
-}
-```
-
-## Using time control with `withContext`
-
-Calls to `withContext(Dispatchers.IO)` or `withContext(Dispatchers.Default)` are common in coroutines based codebases.
-Both dispatchers are not designed to interact with `TestCoroutineDispatcher`.
-
-Tests should provide a `TestCoroutineDispatcher` to replace these dispatchers if the `withContext` calls `delay` in the
-function under test. For example, a test that calls `veryExpensiveOne` should provide a `TestCoroutineDispatcher` using
-either dependency injection, a service locator, or a default parameter.
-
-```kotlin
-suspend fun veryExpensiveOne() = withContext(Dispatchers.Default) {
+suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) {
delay(1_000)
- 1 // for very expensive values of 1
+ 1
+}
+
+fun testExpensiveFunction() = runTest {
+ val result = veryExpensiveFunction() // will take a whole real-time second to execute
+ // the virtual time at this point is still 0
}
```
-In situations where the code inside the `withContext` is very simple, it is not as important to provide a test
-dispatcher. The function `veryExpensiveTwo` will behave identically in a `TestCoroutineDispatcher` and
-`Dispatchers.Default` after the thread switch for `Dispatchers.Default`. Because `withContext` always returns a value by
-directly, there is no need to inject a `TestCoroutineDispatcher` into this function.
-
-```kotlin
-suspend fun veryExpensiveTwo() = withContext(Dispatchers.Default) {
- 2 // for very expensive values of 2
-}
-```
-
-Tests should provide a `TestCoroutineDispatcher` to code that calls `withContext` to provide time control for
-delays, or when execution control is needed to test complex logic.
-
+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.
### Status of the API
@@ -426,36 +362,32 @@
Changes during experimental may have deprecation applied when possible, but it is not
advised to use the API in stable code before it leaves experimental due to possible breaking changes.
-If you have any suggestions for improvements to this experimental API please share them them on the
+If you have any suggestions for improvements to this experimental API please share them them on the
[issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
-[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
-[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
-[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
-[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
-[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
-[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
-[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
+[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html
+[Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html
+[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
+[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
[ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
<!--- MODULE kotlinx-coroutines-test -->
<!--- INDEX kotlinx.coroutines.test -->
+[runTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html
+[TestCoroutineScheduler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/index.html
+[TestScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/index.html
+[TestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-dispatcher/index.html
+[Dispatchers.setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
+[StandardTestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-standard-test-dispatcher.html
+[UnconfinedTestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-unconfined-test-dispatcher.html
[setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html
-[runBlockingTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html
-[UncompletedCoroutinesError]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-uncompleted-coroutines-error/index.html
-[DelayController]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/index.html
-[DelayController.advanceUntilIdle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/advance-until-idle.html
-[DelayController.pauseDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/pause-dispatcher.html
-[TestCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/index.html
-[DelayController.resumeDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/resume-dispatcher.html
-[TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html
-[TestCoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-exception-handler/index.html
-[TestCoroutineScope.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/cleanup-test-coroutines.html
-[DelayController.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/cleanup-test-coroutines.html
+[TestScope.testScheduler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/test-scheduler.html
+[TestScope.runTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html
+[runCurrent]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-current.html
<!--- END -->
diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
index 707ee43..d90a319 100644
--- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
+++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
@@ -13,41 +13,89 @@
public static final fun runBlockingTest (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineDispatcher;Lkotlin/jvm/functions/Function2;)V
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function2;)V
+ public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestScope;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun runBlockingTest$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static final fun runBlockingTestOnTestScope (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
+ public static synthetic fun runBlockingTestOnTestScope$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static final fun runTest (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
+ public static final fun runTest (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;)V
+ public static final fun runTest (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V
+ public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public static final fun runTestWithLegacyScope (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
+ public static synthetic fun runTestWithLegacyScope$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
}
-public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/DelayController {
+public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/test/TestDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/SchedulerAsDelayController {
public fun <init> ()V
+ public fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;)V
+ public synthetic fun <init> (Lkotlinx/coroutines/test/TestCoroutineScheduler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun advanceTimeBy (J)J
public fun advanceUntilIdle ()J
public fun cleanupTestCoroutines ()V
- public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun getCurrentTime ()J
- public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
+ public fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
public fun pauseDispatcher ()V
public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resumeDispatcher ()V
public fun runCurrent ()V
- public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V
public fun toString ()Ljava/lang/String;
}
+public final class kotlinx/coroutines/test/TestCoroutineDispatchersKt {
+ public static final fun StandardTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher;
+ public static synthetic fun StandardTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher;
+ public static final fun UnconfinedTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher;
+ public static synthetic fun UnconfinedTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher;
+}
+
public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor {
public fun <init> ()V
- public fun cleanupTestCoroutinesCaptor ()V
+ public fun cleanupTestCoroutines ()V
public fun getUncaughtExceptions ()Ljava/util/List;
public fun handleException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V
}
-public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope, kotlinx/coroutines/test/DelayController, kotlinx/coroutines/test/UncaughtExceptionCaptor {
+public final class kotlinx/coroutines/test/TestCoroutineScheduler : kotlin/coroutines/AbstractCoroutineContextElement, kotlin/coroutines/CoroutineContext$Element {
+ public static final field Key Lkotlinx/coroutines/test/TestCoroutineScheduler$Key;
+ public fun <init> ()V
+ public final fun advanceTimeBy (J)V
+ public final fun advanceUntilIdle ()V
+ public final fun getCurrentTime ()J
+ public final fun runCurrent ()V
+}
+
+public final class kotlinx/coroutines/test/TestCoroutineScheduler$Key : kotlin/coroutines/CoroutineContext$Key {
+}
+
+public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope {
public abstract fun cleanupTestCoroutines ()V
+ public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
}
public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
public static final fun TestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
+ public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestCoroutineScope;J)V
+ public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+ public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
+ public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
+ public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J
+ public static final fun getUncaughtExceptions (Lkotlinx/coroutines/test/TestCoroutineScope;)Ljava/util/List;
+ public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+ public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static final fun resumeDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+ public static final fun runCurrent (Lkotlinx/coroutines/test/TestCoroutineScope;)V
+}
+
+public abstract class kotlinx/coroutines/test/TestDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay {
+ public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
+ public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
+ public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V
}
public final class kotlinx/coroutines/test/TestDispatchers {
@@ -55,12 +103,21 @@
public static final fun setMain (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;)V
}
-public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
- public abstract fun cleanupTestCoroutinesCaptor ()V
- public abstract fun getUncaughtExceptions ()Ljava/util/List;
+public abstract interface class kotlinx/coroutines/test/TestScope : kotlinx/coroutines/CoroutineScope {
+ public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
}
-public final class kotlinx/coroutines/test/UncompletedCoroutinesError : java/lang/AssertionError {
- public fun <init> (Ljava/lang/String;)V
+public final class kotlinx/coroutines/test/TestScopeKt {
+ public static final fun TestScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestScope;
+ public static synthetic fun TestScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestScope;
+ public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestScope;J)V
+ public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestScope;)V
+ public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestScope;)J
+ public static final fun runCurrent (Lkotlinx/coroutines/test/TestScope;)V
+}
+
+public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
+ public abstract fun cleanupTestCoroutines ()V
+ public abstract fun getUncaughtExceptions ()Ljava/util/List;
}
diff --git a/kotlinx-coroutines-test/common/src/DelayController.kt b/kotlinx-coroutines-test/common/src/DelayController.kt
deleted file mode 100644
index a4ab8c4..0000000
--- a/kotlinx-coroutines-test/common/src/DelayController.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-/**
- * Control the virtual clock time of a [CoroutineDispatcher].
- *
- * Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher].
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public interface DelayController {
- /**
- * Returns the current virtual clock-time as it is known to this Dispatcher.
- *
- * @return The virtual clock-time
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public val currentTime: Long
-
- /**
- * Moves the Dispatcher's virtual clock forward by a specified amount of time.
- *
- * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses
- * blocking coroutines.
- *
- * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested
- * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different
- * calls to delay.
- *
- * ```
- * @Test
- * fun advanceTimeTest() = runBlockingTest {
- * foo()
- * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays
- * // virtual time is 2_000, next resume is at 2_001
- * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced)
- * // virtual time is 2_0002
- * }
- *
- * fun CoroutineScope.foo() {
- * launch {
- * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000)
- * // virtual time is 1_000
- * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500)
- * // virtual time is 1_500
- * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001)
- * // virtual time is 2_001
- * }
- * }
- * ```
- *
- * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
- * @return The amount of delay-time that this Dispatcher's clock has been forwarded.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun advanceTimeBy(delayTimeMillis: Long): Long
-
- /**
- * Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
- *
- * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
- * returns.
- *
- * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun advanceUntilIdle(): Long
-
- /**
- * Run any tasks that are pending at or before the current virtual clock-time.
- *
- * Calling this function will never advance the clock.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun runCurrent()
-
- /**
- * Call after test code completes to ensure that the dispatcher is properly cleaned up.
- *
- * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
- * coroutines.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- @Throws(UncompletedCoroutinesError::class)
- public fun cleanupTestCoroutines()
-
- /**
- * Run a block of code in a paused dispatcher.
- *
- * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
- * will resume auto-advancing.
- *
- * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
- * setup may be done between the time the coroutine is created and started.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public suspend fun pauseDispatcher(block: suspend () -> Unit)
-
- /**
- * Pause the dispatcher.
- *
- * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
- * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun pauseDispatcher()
-
- /**
- * Resume the dispatcher from a paused state.
- *
- * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
- * time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
- * or [advanceUntilIdle].
- */
- @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
- public fun resumeDispatcher()
-}
-
-/**
- * Thrown when a test has completed and there are tasks that are not completed or cancelled.
- */
-// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public class UncompletedCoroutinesError(message: String): AssertionError(message)
diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt
index dde9ac7..e6d0c39 100644
--- a/kotlinx-coroutines-test/common/src/TestBuilders.kt
+++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt
@@ -1,22 +1,45 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:JvmName("TestBuildersKt")
+@file:JvmMultifileClass
package kotlinx.coroutines.test
import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.*
import kotlin.coroutines.*
+import kotlin.jvm.*
/**
- * Executes a [testBody] inside an immediate execution dispatcher.
+ * A test result.
*
- * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
- * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
- * extra time.
+ * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
+ * platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
+ * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
+ * finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
+ *
+ * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
+ * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
+ * test finishes.
+ * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
+ * with a [TestResult] is to immediately `return` it from a test.
+ * * Don't nest functions returning a [TestResult].
+ */
+@Suppress("NO_ACTUAL_FOR_EXPECT")
+@ExperimentalCoroutinesApi
+public expect class TestResult
+
+/**
+ * Executes [testBody] as a test in a new coroutine, returning [TestResult].
+ *
+ * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
+ * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
+ * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
*
* ```
* @Test
- * fun exampleTest() = runBlockingTest {
+ * fun exampleTest() = runTest {
* val deferred = async {
* delay(1_000)
* async {
@@ -26,70 +49,204 @@
*
* deferred.await() // result available immediately
* }
- *
* ```
*
- * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
- * conditions.
+ * The platform difference entails that, in order to use this function correctly in common code, one must always
+ * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
+ * [TestResult] for details on this.
*
- * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
+ * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
+ * Because of this, child coroutines are not executed in parallel to the test body.
+ * In order to for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
+ * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
*
- * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
- * (including coroutines suspended on join/await).
+ * ```
+ * @Test
+ * fun exampleWaitingForAsyncTasks1() = runTest {
+ * // 1
+ * val job = launch {
+ * // 3
+ * }
+ * // 2
+ * job.join() // the main test coroutine suspends here, so the child is executed
+ * // 4
+ * }
*
- * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
- * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
- * @param testBody The code of the unit-test.
+ * @Test
+ * fun exampleWaitingForAsyncTasks2() = runTest {
+ * // 1
+ * launch {
+ * // 3
+ * }
+ * // 2
+ * advanceUntilIdle() // runs the tasks until their queue is empty
+ * // 4
+ * }
+ * ```
+ *
+ * ### Task scheduling
+ *
+ * Delay-skipping is achieved by using virtual time.
+ * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
+ * then its [TestCoroutineScheduler] is used;
+ * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
+ * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
+ * Some convenience methods are available on [TestScope] to control the scheduler.
+ *
+ * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
+ * ```
+ * @Test
+ * fun exampleTest() = runTest {
+ * val elapsed = TimeSource.Monotonic.measureTime {
+ * val deferred = async {
+ * delay(1_000) // will be skipped
+ * withContext(Dispatchers.Default) {
+ * delay(5_000) // Dispatchers.Default doesn't know about TestCoroutineScheduler
+ * }
+ * }
+ * deferred.await()
+ * }
+ * println(elapsed) // about five seconds
+ * }
+ * ```
+ *
+ * ### Failures
+ *
+ * #### Test body failures
+ *
+ * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
+ *
+ * #### Reported exceptions
+ *
+ * Unhandled exceptions will be thrown at the end of the test.
+ * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
+ * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
+ *
+ * #### Uncompleted coroutines
+ *
+ * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
+ * [testBody] also complete, or are cancelled.
+ * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
+ * [AssertionError], whereas on JS, the `Promise` will fail with it).
+ *
+ * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
+ * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
+ * for [dispatchTimeoutMs] milliseconds (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes
+ * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
+ * task during that time, the timer gets reset.
+ *
+ * ### Configuration
+ *
+ * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
+ * scope created for the test, [context] also can be used to change how the test is executed.
+ * See the [TestScope] constructor function documentation for details.
+ *
+ * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
*/
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
- val (safeContext, dispatcher) = context.checkArguments()
- val startingJobs = safeContext.activeJobs()
- val scope = TestCoroutineScope(safeContext)
- val deferred = scope.async {
- scope.testBody()
- }
- dispatcher.advanceUntilIdle()
- deferred.getCompletionExceptionOrNull()?.let {
- throw it
- }
- scope.cleanupTestCoroutines()
- val endingJobs = safeContext.activeJobs()
- if ((endingJobs - startingJobs).isNotEmpty()) {
- throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs")
- }
-}
-
-private fun CoroutineContext.activeJobs(): Set<Job> {
- return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
+@ExperimentalCoroutinesApi
+public fun runTest(
+ context: CoroutineContext = EmptyCoroutineContext,
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ testBody: suspend TestScope.() -> Unit
+): TestResult {
+ if (context[RunningInRunTest] != null)
+ throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
+ return TestScope(context + RunningInRunTest).runTest(dispatchTimeoutMs, testBody)
}
/**
- * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
+ * Performs [runTest] on an existing [TestScope].
*/
-// todo: need documentation on how this extension is supposed to be used
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
- runBlockingTest(coroutineContext, block)
+@ExperimentalCoroutinesApi
+public fun TestScope.runTest(
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ testBody: suspend TestScope.() -> Unit
+): TestResult = asSpecificImplementation().let {
+ it.enter()
+ createTestResult {
+ runTestCoroutine(it, dispatchTimeoutMs, testBody) { it.leave() }
+ }
+}
/**
- * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
+ * Runs [testProcedure], creating a [TestResult].
*/
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
- runBlockingTest(this, block)
+@Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult`
+internal expect fun createTestResult(testProcedure: suspend () -> Unit): TestResult
-private fun CoroutineContext.checkArguments(): Pair<CoroutineContext, DelayController> {
- val dispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
- is DelayController -> dispatcher
- null -> TestCoroutineDispatcher()
- else -> throw IllegalArgumentException("Dispatcher must implement DelayController: $dispatcher")
+/** A coroutine context element indicating that the coroutine is running inside `runTest`. */
+internal object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
+ override val key: CoroutineContext.Key<*>
+ get() = this
+
+ override fun toString(): String = "RunningInRunTest"
+}
+
+/** The default timeout to use when waiting for asynchronous completions of the coroutines managed by
+ * a [TestCoroutineScheduler]. */
+internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
+
+/**
+ * Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
+ * [dispatchTimeoutMs] milliseconds, and performing the [cleanup] procedure at the end.
+ *
+ * The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or
+ * return a list of uncaught exceptions that should be reported at the end of the test.
+ */
+internal suspend fun <T: AbstractCoroutine<Unit>> runTestCoroutine(
+ coroutine: T,
+ dispatchTimeoutMs: Long,
+ testBody: suspend T.() -> Unit,
+ cleanup: () -> List<Throwable>,
+) {
+ val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!!
+ /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
+ * [TestCoroutineDispatcher], because the event loop is not started. */
+ coroutine.start(CoroutineStart.UNDISPATCHED, coroutine) {
+ testBody()
}
- val exceptionHandler = when (val handler = get(CoroutineExceptionHandler)) {
- is UncaughtExceptionCaptor -> handler
- null -> TestCoroutineExceptionHandler()
- else -> throw IllegalArgumentException("coroutineExceptionHandler must implement UncaughtExceptionCaptor: $handler")
+ var completed = false
+ while (!completed) {
+ scheduler.advanceUntilIdle()
+ if (coroutine.isCompleted) {
+ /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
+ non-trivial dispatches. */
+ completed = true
+ continue
+ }
+ select<Unit> {
+ 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")
+ }
+ }
}
- val job = get(Job) ?: SupervisorJob()
- return Pair(this + dispatcher + exceptionHandler + job, dispatcher)
+ coroutine.getCompletionExceptionOrNull()?.let { exception ->
+ val exceptions = try {
+ cleanup()
+ } catch (e: UncompletedCoroutinesError) {
+ // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
+ emptyList()
+ }
+ (listOf(exception) + exceptions).throwAll()
+ }
+ cleanup().throwAll()
+}
+
+internal fun List<Throwable>.throwAll() {
+ firstOrNull()?.apply {
+ drop(1).forEach { addSuppressed(it) }
+ throw this
+ }
}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt
deleted file mode 100644
index 55b92cd..0000000
--- a/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.atomicfu.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.internal.*
-import kotlin.coroutines.*
-import kotlin.jvm.*
-import kotlin.math.*
-
-/**
- * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests
- * and implements [DelayController] to control its virtual clock.
- *
- * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are
- * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the
- * methods on [DelayController].
- *
- * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will
- * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the
- * methods on [DelayController].
- *
- * @see DelayController
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayController {
- private var dispatchImmediately = true
- set(value) {
- field = value
- if (value) {
- // there may already be tasks from setup code we need to run
- advanceUntilIdle()
- }
- }
-
- // The ordered queue for the runnable tasks.
- private val queue = ThreadSafeHeap<TimedRunnable>()
-
- // The per-scheduler global order counter.
- private val _counter = atomic(0L)
-
- // Storing time in nanoseconds internally.
- private val _time = atomic(0L)
-
- /** @suppress */
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- if (dispatchImmediately) {
- block.run()
- } else {
- post(block)
- }
- }
-
- /** @suppress */
- @InternalCoroutinesApi
- override fun dispatchYield(context: CoroutineContext, block: Runnable) {
- post(block)
- }
-
- /** @suppress */
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- postDelayed(CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) }, timeMillis)
- }
-
- /** @suppress */
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- val node = postDelayed(block, timeMillis)
- return DisposableHandle { queue.remove(node) }
- }
-
- /** @suppress */
- override fun toString(): String {
- return "TestCoroutineDispatcher[currentTime=${currentTime}ms, queued=${queue.size}]"
- }
-
- private fun post(block: Runnable) =
- queue.addLast(TimedRunnable(block, _counter.getAndIncrement()))
-
- private fun postDelayed(block: Runnable, delayTime: Long) =
- TimedRunnable(block, _counter.getAndIncrement(), safePlus(currentTime, delayTime))
- .also {
- queue.addLast(it)
- }
-
- private fun safePlus(currentTime: Long, delayTime: Long): Long {
- check(delayTime >= 0)
- val result = currentTime + delayTime
- if (result < currentTime) return Long.MAX_VALUE // clam on overflow
- return result
- }
-
- private fun doActionsUntil(targetTime: Long) {
- while (true) {
- val current = queue.removeFirstIf { it.time <= targetTime } ?: break
- // If the scheduled time is 0 (immediate) use current virtual time
- if (current.time != 0L) _time.value = current.time
- current.run()
- }
- }
-
- /** @suppress */
- override val currentTime: Long get() = _time.value
-
- /** @suppress */
- override fun advanceTimeBy(delayTimeMillis: Long): Long {
- val oldTime = currentTime
- advanceUntilTime(oldTime + delayTimeMillis)
- return currentTime - oldTime
- }
-
- /**
- * Moves the CoroutineContext's clock-time to a particular moment in time.
- *
- * @param targetTime The point in time to which to move the CoroutineContext's clock (milliseconds).
- */
- private fun advanceUntilTime(targetTime: Long) {
- doActionsUntil(targetTime)
- _time.update { currentValue -> max(currentValue, targetTime) }
- }
-
- /** @suppress */
- override fun advanceUntilIdle(): Long {
- val oldTime = currentTime
- while(!queue.isEmpty) {
- runCurrent()
- val next = queue.peek() ?: break
- advanceUntilTime(next.time)
- }
- return currentTime - oldTime
- }
-
- /** @suppress */
- override fun runCurrent(): Unit = doActionsUntil(currentTime)
-
- /** @suppress */
- override suspend fun pauseDispatcher(block: suspend () -> Unit) {
- val previous = dispatchImmediately
- dispatchImmediately = false
- try {
- block()
- } finally {
- dispatchImmediately = previous
- }
- }
-
- /** @suppress */
- override fun pauseDispatcher() {
- dispatchImmediately = false
- }
-
- /** @suppress */
- override fun resumeDispatcher() {
- dispatchImmediately = true
- }
-
- /** @suppress */
- override fun cleanupTestCoroutines() {
- // process any pending cancellations or completions, but don't advance time
- doActionsUntil(currentTime)
-
- // run through all pending tasks, ignore any submitted coroutines that are not active
- val pendingTasks = mutableListOf<TimedRunnable>()
- while (true) {
- pendingTasks += queue.removeFirstOrNull() ?: break
- }
- val activeDelays = pendingTasks
- .mapNotNull { it.runnable as? CancellableContinuationRunnable<*> }
- .filter { it.continuation.isActive }
-
- val activeTimeouts = pendingTasks.filter { it.runnable !is CancellableContinuationRunnable<*> }
- if (activeDelays.isNotEmpty() || activeTimeouts.isNotEmpty()) {
- throw UncompletedCoroutinesError(
- "Unfinished coroutines during teardown. Ensure all coroutines are" +
- " completed or cancelled by your test."
- )
- }
- }
-}
-
-/**
- * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled
- * in the future.
- */
-private class CancellableContinuationRunnable<T>(
- @JvmField val continuation: CancellableContinuation<T>,
- private val block: CancellableContinuation<T>.() -> Unit
-) : Runnable {
- override fun run() = continuation.block()
-}
-
-/**
- * A Runnable for our event loop that represents a task to perform at a time.
- */
-private class TimedRunnable(
- @JvmField val runnable: Runnable,
- private val count: Long = 0,
- @JvmField val time: Long = 0
-) : Comparable<TimedRunnable>, Runnable by runnable, ThreadSafeHeapNode {
- override var heap: ThreadSafeHeap<*>? = null
- override var index: Int = 0
-
- override fun compareTo(other: TimedRunnable) = if (time == other.time) {
- count.compareTo(other.count)
- } else {
- time.compareTo(other.time)
- }
-
- override fun toString() = "TimedRunnable(time=$time, run=$runnable)"
-}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
new file mode 100644
index 0000000..4cc48f4
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.test.internal.TestMainDispatcher
+import kotlin.coroutines.*
+
+/**
+ * Creates an instance of an unconfined [TestDispatcher].
+ *
+ * This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular
+ * thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do.
+ *
+ * Like [Dispatchers.Unconfined], this one does not provide guarantees about the execution order when several coroutines
+ * are queued in this dispatcher. However, we ensure that the [launch] and [async] blocks at the top level of [runTest]
+ * are entered eagerly. This allows launching child coroutines and not calling [runCurrent] for them to start executing.
+ *
+ * ```
+ * @Test
+ * fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ * var entered = false
+ * val deferred = CompletableDeferred<Unit>()
+ * var completed = false
+ * launch {
+ * entered = true
+ * deferred.await()
+ * completed = true
+ * }
+ * assertTrue(entered) // `entered = true` already executed.
+ * assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
+ * deferred.complete(Unit) // resume the coroutine.
+ * assertTrue(completed) // now the child coroutine is immediately completed.
+ * }
+ * ```
+ *
+ * Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and
+ * in which order the queued coroutines are executed.
+ * Another typical use case for this dispatcher is launching child coroutines that are resumed immediately, without
+ * going through a dispatch; this can be helpful for testing [Channel] and [StateFlow] usages.
+ *
+ * ```
+ * @Test
+ * fun testUnconfinedDispatcher() = runTest {
+ * val values = mutableListOf<Int>()
+ * val stateFlow = MutableStateFlow(0)
+ * val job = launch(UnconfinedTestDispatcher(testScheduler)) {
+ * stateFlow.collect {
+ * values.add(it)
+ * }
+ * }
+ * stateFlow.value = 1
+ * stateFlow.value = 2
+ * stateFlow.value = 3
+ * job.cancel()
+ * // each assignment will immediately resume the collecting child coroutine,
+ * // so no values will be skipped.
+ * assertEquals(listOf(0, 1, 2, 3), values)
+ * }
+ * ```
+ *
+ * Please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order
+ * guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing
+ * functionality, not the specific order of actions.
+ * See [Dispatchers.Unconfined] for a discussion of the execution order guarantees.
+ *
+ * In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control
+ * the virtual time and can be shared among many test dispatchers.
+ * If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a
+ * [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if
+ * [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created.
+ *
+ * Additionally, [name] can be set to distinguish each dispatcher instance when debugging.
+ *
+ * @see StandardTestDispatcher for a more predictable [TestDispatcher].
+ */
+@ExperimentalCoroutinesApi
+@Suppress("FunctionName")
+public fun UnconfinedTestDispatcher(
+ scheduler: TestCoroutineScheduler? = null,
+ name: String? = null
+): TestDispatcher = UnconfinedTestDispatcherImpl(
+ scheduler ?: TestMainDispatcher.currentTestScheduler ?: TestCoroutineScheduler(), name)
+
+private class UnconfinedTestDispatcherImpl(
+ override val scheduler: TestCoroutineScheduler,
+ private val name: String? = null
+) : TestDispatcher() {
+
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
+
+ @Suppress("INVISIBLE_MEMBER")
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ scheduler.sendDispatchEvent()
+
+ /** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */
+ /** It can only be called by the [yield] function. See also code of [yield] function. */
+ val yieldContext = context[YieldContext]
+ if (yieldContext !== null) {
+ // report to "yield" that it is an unconfined dispatcher and don't call "block.run()"
+ yieldContext.dispatcherWasUnconfined = true
+ return
+ }
+ throw UnsupportedOperationException(
+ "Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " +
+ "the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
+ "isDispatchNeeded and dispatch calls."
+ )
+ }
+
+ override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]"
+}
+
+/**
+ * Creates an instance of a [TestDispatcher] whose tasks are run inside calls to the [scheduler].
+ *
+ * This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its
+ * [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent],
+ * [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these
+ * tasks in a blocking manner.
+ *
+ * In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are
+ * parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to
+ * run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when
+ * inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines.
+ *
+ * If no [scheduler] is passed as an argument, [Dispatchers.Main] is checked, and if it was mocked with a
+ * [TestDispatcher] via [Dispatchers.setMain], the [TestDispatcher.scheduler] of the mock dispatcher is used; if
+ * [Dispatchers.Main] is not mocked with a [TestDispatcher], a new [TestCoroutineScheduler] is created.
+ *
+ * One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging.
+ *
+ * @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread.
+ */
+@ExperimentalCoroutinesApi
+@Suppress("FunctionName")
+public fun StandardTestDispatcher(
+ scheduler: TestCoroutineScheduler? = null,
+ name: String? = null
+): TestDispatcher = StandardTestDispatcherImpl(
+ scheduler ?: TestMainDispatcher.currentTestScheduler ?: TestCoroutineScheduler(), name)
+
+private class StandardTestDispatcherImpl(
+ override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
+ private val name: String? = null
+) : TestDispatcher() {
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ scheduler.registerEvent(this, 0, block) { false }
+ }
+
+ override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]"
+}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/common/src/TestCoroutineExceptionHandler.kt
deleted file mode 100644
index b1296df..0000000
--- a/kotlinx-coroutines-test/common/src/TestCoroutineExceptionHandler.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.internal.*
-import kotlin.coroutines.*
-
-/**
- * Access uncaught coroutine exceptions captured during test execution.
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public interface UncaughtExceptionCaptor {
- /**
- * List of uncaught coroutine exceptions.
- *
- * The returned list is a copy of the currently caught exceptions.
- * During [cleanupTestCoroutinesCaptor] the first element of this list is rethrown if it is not empty.
- */
- public val uncaughtExceptions: List<Throwable>
-
- /**
- * Call after the test completes to ensure that there were no uncaught exceptions.
- *
- * The first exception in uncaughtExceptions is rethrown. All other exceptions are
- * printed using [Throwable.printStackTrace].
- *
- * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
- */
- public fun cleanupTestCoroutinesCaptor()
-}
-
-/**
- * An exception handler that captures uncaught exceptions in tests.
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public class TestCoroutineExceptionHandler :
- AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
-{
- private val _exceptions = mutableListOf<Throwable>()
- private val _lock = SynchronizedObject()
-
- /** @suppress **/
- override fun handleException(context: CoroutineContext, exception: Throwable) {
- synchronized(_lock) {
- _exceptions += exception
- }
- }
-
- /** @suppress **/
- override val uncaughtExceptions: List<Throwable>
- get() = synchronized(_lock) { _exceptions.toList() }
-
- /** @suppress **/
- override fun cleanupTestCoroutinesCaptor() {
- synchronized(_lock) {
- val exception = _exceptions.firstOrNull() ?: return
- // log the rest
- _exceptions.drop(1).forEach { it.printStackTrace() }
- throw exception
- }
- }
-}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt
new file mode 100644
index 0000000..d256f27
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.atomicfu.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
+import kotlinx.coroutines.internal.*
+import kotlinx.coroutines.selects.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * This is a scheduler for coroutines used in tests, providing the delay-skipping behavior.
+ *
+ * [Test dispatchers][TestDispatcher] are parameterized with a scheduler. Several dispatchers can share the
+ * same scheduler, in which case their knowledge about the virtual time will be synchronized. When the dispatchers
+ * require scheduling an event at a later point in time, they notify the scheduler, which will establish the order of
+ * the tasks.
+ *
+ * The scheduler can be queried to advance the time (via [advanceTimeBy]), run all the scheduled tasks advancing the
+ * virtual time as needed (via [advanceUntilIdle]), or run the tasks that are scheduled to run as soon as possible but
+ * haven't yet been dispatched (via [runCurrent]).
+ */
+@ExperimentalCoroutinesApi
+// TODO: maybe make this a `TimeSource`?
+public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCoroutineScheduler),
+ CoroutineContext.Element {
+
+ /** @suppress */
+ public companion object Key : CoroutineContext.Key<TestCoroutineScheduler>
+
+ /** This heap stores the knowledge about which dispatchers are interested in which moments of virtual time. */
+ // TODO: all the synchronization is done via a separate lock, so a non-thread-safe priority queue can be used.
+ private val events = ThreadSafeHeap<TestDispatchEvent<Any>>()
+
+ /** Establishes that [currentTime] can't exceed the time of the earliest event in [events]. */
+ private val lock = SynchronizedObject()
+
+ /** This counter establishes some order on the events that happen at the same virtual time. */
+ private val count = atomic(0L)
+
+ /** The current virtual time. */
+ @ExperimentalCoroutinesApi
+ public var currentTime: Long = 0
+ get() = synchronized(lock) { field }
+ private set
+
+ /** A channel for notifying about the fact that a dispatch recently happened. */
+ private val dispatchEvents: Channel<Unit> = Channel(CONFLATED)
+
+ /**
+ * Registers a request for the scheduler to notify [dispatcher] at a virtual moment [timeDeltaMillis] milliseconds
+ * later via [TestDispatcher.processEvent], which will be called with the provided [marker] object.
+ *
+ * Returns the handler which can be used to cancel the registration.
+ */
+ internal fun <T : Any> registerEvent(
+ dispatcher: TestDispatcher,
+ timeDeltaMillis: Long,
+ marker: T,
+ isCancelled: (T) -> Boolean
+ ): DisposableHandle {
+ require(timeDeltaMillis >= 0) { "Attempted scheduling an event earlier in time (with the time delta $timeDeltaMillis)" }
+ val count = count.getAndIncrement()
+ return synchronized(lock) {
+ val time = addClamping(currentTime, timeDeltaMillis)
+ val event = TestDispatchEvent(dispatcher, count, time, marker as Any) { isCancelled(marker) }
+ events.addLast(event)
+ /** can't be moved above: otherwise, [onDispatchEvent] could consume the token sent here before there's
+ * actually anything in the event queue. */
+ sendDispatchEvent()
+ DisposableHandle {
+ synchronized(lock) {
+ events.remove(event)
+ }
+ }
+ }
+ }
+
+ /**
+ * Runs the next enqueued task, advancing the virtual time to the time of its scheduled awakening.
+ */
+ private fun tryRunNextTask(): Boolean {
+ val event = synchronized(lock) {
+ val event = events.removeFirstOrNull() ?: return false
+ if (currentTime > event.time)
+ currentTimeAheadOfEvents()
+ currentTime = event.time
+ event
+ }
+ event.dispatcher.processEvent(event.time, event.marker)
+ return true
+ }
+
+ /**
+ * Runs the enqueued tasks in the specified order, advancing the virtual time as needed until there are no more
+ * tasks associated with the dispatchers linked to this scheduler.
+ *
+ * A breaking change from [TestCoroutineDispatcher.advanceTimeBy] is that it no longer returns the total number of
+ * milliseconds by which the execution of this method has advanced the virtual time. If you want to recreate that
+ * functionality, query [currentTime] before and after the execution to achieve the same result.
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceUntilIdle() {
+ while (!synchronized(lock) { events.isEmpty }) {
+ tryRunNextTask()
+ }
+ }
+
+ /**
+ * Runs the tasks that are scheduled to execute at this moment of virtual time.
+ */
+ @ExperimentalCoroutinesApi
+ public fun runCurrent() {
+ val timeMark = synchronized(lock) { currentTime }
+ while (true) {
+ val event = synchronized(lock) {
+ events.removeFirstIf { it.time <= timeMark } ?: return
+ }
+ event.dispatcher.processEvent(event.time, event.marker)
+ }
+ }
+
+ /**
+ * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the
+ * scheduled tasks in the meantime.
+ *
+ * Breaking changes from [TestCoroutineDispatcher.advanceTimeBy]:
+ * * Intentionally doesn't return a `Long` value, as its use cases are unclear. We may restore it in the future;
+ * please describe your use cases at [the issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues/).
+ * For now, it's possible to query [currentTime] before and after execution of this method, to the same effect.
+ * * It doesn't run the tasks that are scheduled at exactly [currentTime] + [delayTimeMillis]. For example,
+ * advancing the time by one millisecond used to run the tasks at the current millisecond *and* the next
+ * millisecond, but now will stop just before executing any task starting at the next millisecond.
+ * * Overflowing the target time used to lead to nothing being done, but will now run the tasks scheduled at up to
+ * (but not including) [Long.MAX_VALUE].
+ *
+ * @throws IllegalStateException if passed a negative [delay][delayTimeMillis].
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceTimeBy(delayTimeMillis: Long) {
+ require(delayTimeMillis >= 0) { "Can not advance time by a negative delay: $delayTimeMillis" }
+ val startingTime = currentTime
+ val targetTime = addClamping(startingTime, delayTimeMillis)
+ while (true) {
+ val event = synchronized(lock) {
+ val timeMark = currentTime
+ val event = events.removeFirstIf { targetTime > it.time }
+ when {
+ event == null -> {
+ currentTime = targetTime
+ return
+ }
+ timeMark > event.time -> currentTimeAheadOfEvents()
+ else -> {
+ currentTime = event.time
+ event
+ }
+ }
+ }
+ event.dispatcher.processEvent(event.time, event.marker)
+ }
+ }
+
+ /**
+ * Checks that the only tasks remaining in the scheduler are cancelled.
+ */
+ internal fun isIdle(strict: Boolean = true): Boolean {
+ synchronized(lock) {
+ if (strict)
+ return events.isEmpty
+ // TODO: also completely empties the queue, as there's no nondestructive way to iterate over [ThreadSafeHeap]
+ val presentEvents = mutableListOf<TestDispatchEvent<*>>()
+ while (true) {
+ presentEvents += events.removeFirstOrNull() ?: break
+ }
+ return presentEvents.all { it.isCancelled() }
+ }
+ }
+
+ /**
+ * Notifies this scheduler about a dispatch event.
+ */
+ internal fun sendDispatchEvent() {
+ dispatchEvents.trySend(Unit)
+ }
+
+ /**
+ * Consumes the knowledge that a dispatch event happened recently.
+ */
+ internal val onDispatchEvent: SelectClause1<Unit> get() = dispatchEvents.onReceive
+}
+
+// Some error-throwing functions for pretty stack traces
+private fun currentTimeAheadOfEvents(): Nothing = invalidSchedulerState()
+
+private fun invalidSchedulerState(): Nothing =
+ throw IllegalStateException("The test scheduler entered an invalid state. Please report this at https://github.com/Kotlin/kotlinx.coroutines/issues.")
+
+/** [ThreadSafeHeap] node representing a scheduled task, ordered by the planned execution time. */
+private class TestDispatchEvent<T>(
+ @JvmField val dispatcher: TestDispatcher,
+ private val count: Long,
+ @JvmField val time: Long,
+ @JvmField val marker: T,
+ @JvmField val isCancelled: () -> Boolean
+) : Comparable<TestDispatchEvent<*>>, ThreadSafeHeapNode {
+ override var heap: ThreadSafeHeap<*>? = null
+ override var index: Int = 0
+
+ override fun compareTo(other: TestDispatchEvent<*>) =
+ compareValuesBy(this, other, TestDispatchEvent<*>::time, TestDispatchEvent<*>::count)
+
+ override fun toString() = "TestDispatchEvent(time=$time, dispatcher=$dispatcher)"
+}
+
+// works with positive `a`, `b`
+private fun addClamping(a: Long, b: Long): Long = (a + b).let { if (it >= 0) it else Long.MAX_VALUE }
+
+internal fun checkSchedulerInContext(scheduler: TestCoroutineScheduler, context: CoroutineContext) {
+ context[TestCoroutineScheduler]?.let {
+ check(it === scheduler) {
+ "Detected use of different schedulers. If you need to use several test coroutine dispatchers, " +
+ "create one `TestCoroutineScheduler` and pass it to each of them."
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt b/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt
deleted file mode 100644
index da29cd2..0000000
--- a/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlin.coroutines.*
-
-/**
- * A scope which provides detailed control over the execution of coroutines for tests.
- */
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public interface TestCoroutineScope: CoroutineScope, UncaughtExceptionCaptor, DelayController {
- /**
- * Call after the test completes.
- * Calls [UncaughtExceptionCaptor.cleanupTestCoroutinesCaptor] and [DelayController.cleanupTestCoroutines].
- *
- * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
- * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
- * coroutines.
- */
- public override fun cleanupTestCoroutines()
-}
-
-private class TestCoroutineScopeImpl (
- override val coroutineContext: CoroutineContext
-):
- TestCoroutineScope,
- UncaughtExceptionCaptor by coroutineContext.uncaughtExceptionCaptor,
- DelayController by coroutineContext.delayController
-{
- override fun cleanupTestCoroutines() {
- coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
- coroutineContext.delayController.cleanupTestCoroutines()
- }
-}
-
-/**
- * A scope which provides detailed control over the execution of coroutines for tests.
- *
- * If the provided context does not provide a [ContinuationInterceptor] (Dispatcher) or [CoroutineExceptionHandler], the
- * scope adds [TestCoroutineDispatcher] and [TestCoroutineExceptionHandler] automatically.
- *
- * @param context an optional context that MAY provide [UncaughtExceptionCaptor] and/or [DelayController]
- */
-@Suppress("FunctionName")
-@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
-public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
- var safeContext = context
- if (context[ContinuationInterceptor] == null) safeContext += TestCoroutineDispatcher()
- if (context[CoroutineExceptionHandler] == null) safeContext += TestCoroutineExceptionHandler()
- return TestCoroutineScopeImpl(safeContext)
-}
-
-private inline val CoroutineContext.uncaughtExceptionCaptor: UncaughtExceptionCaptor
- get() {
- val handler = this[CoroutineExceptionHandler]
- return handler as? UncaughtExceptionCaptor ?: throw IllegalArgumentException(
- "TestCoroutineScope requires a UncaughtExceptionCaptor such as " +
- "TestCoroutineExceptionHandler as the CoroutineExceptionHandler"
- )
- }
-
-private inline val CoroutineContext.delayController: DelayController
- get() {
- val handler = this[ContinuationInterceptor]
- return handler as? DelayController ?: throw IllegalArgumentException(
- "TestCoroutineScope requires a DelayController such as TestCoroutineDispatcher as " +
- "the ContinuationInterceptor (Dispatcher)"
- )
- }
diff --git a/kotlinx-coroutines-test/common/src/TestDispatcher.kt b/kotlinx-coroutines-test/common/src/TestDispatcher.kt
new file mode 100644
index 0000000..3b756b1
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestDispatcher.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * A test dispatcher that can interface with a [TestCoroutineScheduler].
+ */
+@ExperimentalCoroutinesApi
+public abstract class TestDispatcher internal constructor(): CoroutineDispatcher(), Delay {
+ /** The scheduler that this dispatcher is linked to. */
+ @ExperimentalCoroutinesApi
+ public abstract val scheduler: TestCoroutineScheduler
+
+ /** Notifies the dispatcher that it should process a single event marked with [marker] happening at time [time]. */
+ internal fun processEvent(time: Long, marker: Any) {
+ check(marker is Runnable)
+ marker.run()
+ }
+
+ /** @suppress */
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
+ checkSchedulerInContext(scheduler, continuation.context)
+ val timedRunnable = CancellableContinuationRunnable(continuation, this)
+ scheduler.registerEvent(this, timeMillis, timedRunnable, ::cancellableRunnableIsCancelled)
+ }
+
+ /** @suppress */
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
+ checkSchedulerInContext(scheduler, context)
+ return scheduler.registerEvent(this, timeMillis, block) { false }
+ }
+}
+
+/**
+ * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled
+ * in the future.
+ */
+private class CancellableContinuationRunnable(
+ @JvmField val continuation: CancellableContinuation<Unit>,
+ private val dispatcher: CoroutineDispatcher
+) : Runnable {
+ override fun run() = with(dispatcher) { with(continuation) { resumeUndispatched(Unit) } }
+}
+
+private fun cancellableRunnableIsCancelled(runnable: CancellableContinuationRunnable): Boolean =
+ !runnable.continuation.isActive
diff --git a/kotlinx-coroutines-test/common/src/TestDispatchers.kt b/kotlinx-coroutines-test/common/src/TestDispatchers.kt
index f8896d7..4454597 100644
--- a/kotlinx-coroutines-test/common/src/TestDispatchers.kt
+++ b/kotlinx-coroutines-test/common/src/TestDispatchers.kt
@@ -11,7 +11,10 @@
/**
* Sets the given [dispatcher] as an underlying dispatcher of [Dispatchers.Main].
- * All subsequent usages of [Dispatchers.Main] will use given [dispatcher] under the hood.
+ * All subsequent usages of [Dispatchers.Main] will use the given [dispatcher] under the hood.
+ *
+ * Using [TestDispatcher] as an argument has special behavior: subsequently-called [runTest], as well as
+ * [TestScope] and test dispatcher constructors, will use the [TestCoroutineScheduler] of the provided dispatcher.
*
* It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist.
*/
@@ -23,8 +26,9 @@
/**
* Resets state of the [Dispatchers.Main] to the original main dispatcher.
- * For example, in Android Main thread dispatcher will be set as [Dispatchers.Main].
- * Used to clean up all possible dependencies, should be used in tear down (`@After`) methods.
+ *
+ * For example, in Android, the Main thread dispatcher will be set as [Dispatchers.Main].
+ * This method undoes a dependency injection performed for tests, and so should be used in tear down (`@After`) methods.
*
* It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist.
*/
diff --git a/kotlinx-coroutines-test/common/src/TestScope.kt b/kotlinx-coroutines-test/common/src/TestScope.kt
new file mode 100644
index 0000000..ffd5c01
--- /dev/null
+++ b/kotlinx-coroutines-test/common/src/TestScope.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import kotlin.coroutines.*
+
+/**
+ * A coroutine scope that for launching test coroutines.
+ *
+ * The scope provides the following functionality:
+ * * The [coroutineContext] includes a [coroutine dispatcher][TestDispatcher] that supports delay-skipping, using
+ * a [TestCoroutineScheduler] for orchestrating the virtual time.
+ * This scheduler is also available via the [testScheduler] property, and some helper extension
+ * methods are defined to more conveniently interact with it: see [TestScope.currentTime], [TestScope.runCurrent],
+ * [TestScope.advanceTimeBy], and [TestScope.advanceUntilIdle].
+ * * When inside [runTest], uncaught exceptions from the child coroutines of this scope will be reported at the end of
+ * the test.
+ * It is invalid for child coroutines to throw uncaught exceptions when outside the call to [TestScope.runTest]:
+ * the only guarantee in this case is the best effort to deliver the exception.
+ *
+ * The usual way to access a [TestScope] is to call [runTest], but it can also be constructed manually, in order to
+ * use it to initialize the components that participate in the test.
+ *
+ * #### Differences from the deprecated [TestCoroutineScope]
+ *
+ * * This doesn't provide an equivalent of [TestCoroutineScope.cleanupTestCoroutines], and so can't be used as a
+ * standalone mechanism for writing tests: it does require that [runTest] is eventually called.
+ * The reason for this is that a proper cleanup procedure that supports using non-test dispatchers and arbitrary
+ * coroutine suspensions would be equivalent to [runTest], but would also be more error-prone, due to the potential
+ * for forgetting to perform the cleanup.
+ * * [TestCoroutineScope.advanceTimeBy] also calls [TestCoroutineScheduler.runCurrent] after advancing the virtual time.
+ * * No support for dispatcher pausing, like [DelayController] allows. [TestCoroutineDispatcher], which supported
+ * pausing, is deprecated; now, instead of pausing a dispatcher, one can use [withContext] to run a dispatcher that's
+ * paused by default, like [StandardTestDispatcher].
+ * * No access to the list of unhandled exceptions.
+ */
+@ExperimentalCoroutinesApi
+public sealed interface TestScope : CoroutineScope {
+ /**
+ * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
+ */
+ @ExperimentalCoroutinesApi
+ public val testScheduler: TestCoroutineScheduler
+}
+
+/**
+ * The current virtual time on [testScheduler][TestScope.testScheduler].
+ * @see TestCoroutineScheduler.currentTime
+ */
+@ExperimentalCoroutinesApi
+public val TestScope.currentTime: Long
+ get() = testScheduler.currentTime
+
+/**
+ * Advances the [testScheduler][TestScope.testScheduler] to the point where there are no tasks remaining.
+ * @see TestCoroutineScheduler.advanceUntilIdle
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.advanceUntilIdle(): Unit = testScheduler.advanceUntilIdle()
+
+/**
+ * Run any tasks that are pending at the current virtual time, according to
+ * the [testScheduler][TestScope.testScheduler].
+ *
+ * @see TestCoroutineScheduler.runCurrent
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.runCurrent(): Unit = testScheduler.runCurrent()
+
+/**
+ * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the
+ * scheduled tasks in the meantime.
+ *
+ * In contrast with [TestScope.advanceTimeBy], this function does not run the tasks scheduled at the moment
+ * [currentTime] + [delayTimeMillis].
+ *
+ * @throws IllegalStateException if passed a negative [delay][delayTimeMillis].
+ * @see TestCoroutineScheduler.advanceTimeBy
+ */
+@ExperimentalCoroutinesApi
+public fun TestScope.advanceTimeBy(delayTimeMillis: Long): Unit = testScheduler.advanceTimeBy(delayTimeMillis)
+
+/**
+ * Creates a [TestScope].
+ *
+ * It ensures that all the test module machinery is properly initialized.
+ * * If [context] doesn't provide a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
+ * a new one is created, unless either
+ * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
+ * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
+ * its [TestCoroutineScheduler] is used.
+ * * If [context] doesn't have a [TestDispatcher], a [StandardTestDispatcher] is created.
+ * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
+ * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
+ * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
+ * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
+ * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
+ * [TestCoroutineScope] and share your use case at
+ * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
+ * * If [context] provides a [Job], that job is used as a parent for the new scope.
+ *
+ * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
+ * different scheduler.
+ * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
+ * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
+ * [UncaughtExceptionCaptor].
+ */
+@ExperimentalCoroutinesApi
+@Suppress("FunctionName")
+public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope {
+ val ctxWithDispatcher = context.withDelaySkipping()
+ var scope: TestScopeImpl? = null
+ val exceptionHandler = when (ctxWithDispatcher[CoroutineExceptionHandler]) {
+ null -> CoroutineExceptionHandler { _, exception ->
+ scope!!.reportException(exception)
+ }
+ else -> throw IllegalArgumentException(
+ "A CoroutineExceptionHandler was passed to TestScope. " +
+ "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
+ "if uncaught exceptions require special treatment."
+ )
+ }
+ return TestScopeImpl(ctxWithDispatcher + exceptionHandler).also { scope = it }
+}
+
+/**
+ * Adds a [TestDispatcher] and a [TestCoroutineScheduler] to the context if there aren't any already.
+ *
+ * @throws IllegalArgumentException if both a [TestCoroutineScheduler] and a [TestDispatcher] are passed.
+ * @throws IllegalArgumentException if a [ContinuationInterceptor] is passed that is not a [TestDispatcher].
+ */
+internal fun CoroutineContext.withDelaySkipping(): CoroutineContext {
+ val dispatcher: TestDispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
+ is TestDispatcher -> {
+ val ctxScheduler = get(TestCoroutineScheduler)
+ if (ctxScheduler != null) {
+ require(dispatcher.scheduler === ctxScheduler) {
+ "Both a TestCoroutineScheduler $ctxScheduler and TestDispatcher $dispatcher linked to " +
+ "another scheduler were passed."
+ }
+ }
+ dispatcher
+ }
+ null -> StandardTestDispatcher(get(TestCoroutineScheduler))
+ else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
+ }
+ return this + dispatcher + dispatcher.scheduler
+}
+
+internal class TestScopeImpl(context: CoroutineContext) :
+ AbstractCoroutine<Unit>(context, initParentJob = true, active = true), TestScope {
+
+ override val testScheduler get() = context[TestCoroutineScheduler]!!
+
+ private var entered = false
+ private var finished = false
+ private val uncaughtExceptions = mutableListOf<Throwable>()
+ private val lock = SynchronizedObject()
+
+ /** Called upon entry to [runTest]. Will throw if called more than once. */
+ fun enter() {
+ val exceptions = synchronized(lock) {
+ if (entered)
+ throw IllegalStateException("Only a single call to `runTest` can be performed during one test.")
+ entered = true
+ check(!finished)
+ uncaughtExceptions
+ }
+ if (exceptions.isNotEmpty()) {
+ throw UncaughtExceptionsBeforeTest().apply {
+ for (e in exceptions)
+ addSuppressed(e)
+ }
+ }
+ }
+
+ /** Called at the end of the test. May only be called once. */
+ fun leave(): List<Throwable> {
+ val exceptions = synchronized(lock) {
+ if(!entered || finished)
+ throw IllegalStateException("An internal error. Please report to the Kotlinx Coroutines issue tracker")
+ finished = true
+ uncaughtExceptions
+ }
+ val activeJobs = children.filter { it.isActive }.toList() // only non-empty if used with `runBlockingTest`
+ if (exceptions.isEmpty()) {
+ if (activeJobs.isNotEmpty())
+ throw UncompletedCoroutinesError(
+ "Active jobs found during the tear-down. " +
+ "Ensure that all coroutines are completed or cancelled by your test. " +
+ "The active jobs: $activeJobs"
+ )
+ if (!testScheduler.isIdle())
+ throw UncompletedCoroutinesError(
+ "Unfinished coroutines found during the tear-down. " +
+ "Ensure that all coroutines are completed or cancelled by your test."
+ )
+ }
+ return exceptions
+ }
+
+ /** Stores an exception to report after [runTest], or rethrows it if not inside [runTest]. */
+ fun reportException(throwable: Throwable) {
+ synchronized(lock) {
+ if (finished) {
+ throw throwable
+ } else {
+ uncaughtExceptions.add(throwable)
+ if (!entered)
+ throw UncaughtExceptionsBeforeTest().apply { addSuppressed(throwable) }
+ }
+ }
+ }
+
+ override fun toString(): String =
+ "TestScope[" + (if (finished) "test ended" else if (entered) "test started" else "test not started") + "]"
+}
+
+/** Use the knowledge that any [TestScope] that we receive is necessarily a [TestScopeImpl]. */
+internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) {
+ is TestScopeImpl -> this
+}
+
+internal class UncaughtExceptionsBeforeTest : IllegalStateException(
+ "There were uncaught exceptions in coroutines launched from TestScope before the test started. Please avoid this," +
+ " as such exceptions are also reported in a platform-dependent manner so that they are not lost."
+)
+
+/**
+ * Thrown when a test has completed and there are tasks that are not completed or cancelled.
+ */
+@ExperimentalCoroutinesApi
+internal class UncompletedCoroutinesError(message: String) : AssertionError(message)
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
index f2e5b7a..24e093b 100644
--- a/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
+++ b/kotlinx-coroutines-test/common/src/internal/TestMainDispatcher.kt
@@ -3,34 +3,88 @@
*/
package kotlinx.coroutines.test.internal
+
+import kotlinx.atomicfu.*
import kotlinx.coroutines.*
+import kotlinx.coroutines.test.*
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(private var delegate: CoroutineDispatcher):
+internal class TestMainDispatcher(delegate: CoroutineDispatcher):
MainCoroutineDispatcher(),
- Delay by (delegate as? Delay ?: defaultDelay)
+ Delay
{
- private val mainDispatcher = delegate // the initial value passed to the constructor
+ private val mainDispatcher = delegate
+ private var delegate = NonConcurrentlyModifiable(mainDispatcher, "Dispatchers.Main")
+
+ private val delay
+ get() = delegate.value as? Delay ?: defaultDelay
override val immediate: MainCoroutineDispatcher
- get() = (delegate as? MainCoroutineDispatcher)?.immediate ?: this
+ get() = (delegate.value as? MainCoroutineDispatcher)?.immediate ?: this
- override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block)
+ override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.value.dispatch(context, block)
- override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.value.isDispatchNeeded(context)
- override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block)
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.value.dispatchYield(context, block)
fun setDispatcher(dispatcher: CoroutineDispatcher) {
- delegate = dispatcher
+ delegate.value = dispatcher
}
fun resetDispatcher() {
- delegate = mainDispatcher
+ delegate.value = mainDispatcher
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) =
+ delay.scheduleResumeAfterDelay(timeMillis, continuation)
+
+ override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
+ delay.invokeOnTimeout(timeMillis, block, context)
+
+ companion object {
+ internal val currentTestDispatcher
+ get() = (Dispatchers.Main as? TestMainDispatcher)?.delegate?.value as? TestDispatcher
+
+ internal val currentTestScheduler
+ get() = currentTestDispatcher?.scheduler
+ }
+
+ /**
+ * A wrapper around a value that attempts to throw when writing happens concurrently with reading.
+ *
+ * The read operations never throw. Instead, the failures detected inside them will be remembered and thrown on the
+ * next modification.
+ */
+ private class NonConcurrentlyModifiable<T>(initialValue: T, private val name: String) {
+ private val readers = atomic(0) // number of concurrent readers
+ private val isWriting = atomic(false) // a modification is happening currently
+ private val exceptionWhenReading: AtomicRef<Throwable?> = atomic(null) // exception from reading
+ private val _value = atomic(initialValue) // the backing field for the value
+
+ private fun concurrentWW() = IllegalStateException("$name is modified concurrently")
+ private fun concurrentRW() = IllegalStateException("$name is used concurrently with setting it")
+
+ var value: T
+ get() {
+ readers.incrementAndGet()
+ if (isWriting.value) exceptionWhenReading.value = concurrentRW()
+ val result = _value.value
+ readers.decrementAndGet()
+ return result
+ }
+ set(value) {
+ exceptionWhenReading.getAndSet(null)?.let { throw it }
+ if (readers.value != 0) throw concurrentRW()
+ if (!isWriting.compareAndSet(expect = false, update = true)) throw concurrentWW()
+ _value.value = value
+ isWriting.value = false
+ if (readers.value != 0) throw concurrentRW()
+ }
}
}
@@ -39,4 +93,4 @@
inline get() = DefaultDelay
@Suppress("INVISIBLE_MEMBER")
-internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher
\ No newline at end of file
+internal expect fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher
diff --git a/kotlinx-coroutines-test/common/test/Helpers.kt b/kotlinx-coroutines-test/common/test/Helpers.kt
index f0a462e..a63311b 100644
--- a/kotlinx-coroutines-test/common/test/Helpers.kt
+++ b/kotlinx-coroutines-test/common/test/Helpers.kt
@@ -4,5 +4,71 @@
package kotlinx.coroutines.test
+import kotlinx.atomicfu.*
+import kotlin.test.*
+import kotlin.time.*
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * The number of milliseconds that is sure not to pass [assertRunsFast].
+ */
+const val SLOW = 100_000L
+
+/**
+ * Asserts that a block completed within [timeout].
+ */
+@OptIn(ExperimentalTime::class)
+inline fun <T> assertRunsFast(timeout: Duration, block: () -> T): T {
+ val result: T
+ val elapsed = TimeSource.Monotonic.measureTime { result = block() }
+ assertTrue("Should complete in $timeout, but took $elapsed") { elapsed < timeout }
+ return result
+}
+
+/**
+ * Asserts that a block completed within two seconds.
+ */
+@OptIn(ExperimentalTime::class)
+inline fun <T> assertRunsFast(block: () -> T): T = assertRunsFast(2.seconds, block)
+
+/**
+ * Passes [test] as an argument to [block], but as a function returning not a [TestResult] but [Unit].
+*/
+expect fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult
+
+class TestException(message: String? = null): Exception(message)
+
+/**
+ * A class inheriting from which allows to check the execution order inside tests.
+ *
+ * @see TestBase
+ */
+open class OrderedExecutionTestBase {
+ private val actionIndex = atomic(0)
+ private val finished = atomic(false)
+
+ /** Expect the next action to be [index] in order. */
+ protected fun expect(index: Int) {
+ val wasIndex = actionIndex.incrementAndGet()
+ check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
+ }
+
+ /** Expect this action to be final, with the given [index]. */
+ protected fun finish(index: Int) {
+ expect(index)
+ check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
+ }
+
+ @AfterTest
+ fun ensureFinishCalls() {
+ assertTrue(finished.value || actionIndex.value == 0, "Expected `finish` to be called")
+ }
+}
+
+internal fun <T> T.void() { }
+
+@OptionalExpectation
+expect annotation class NoJs()
+
@OptionalExpectation
expect annotation class NoNative()
diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt
new file mode 100644
index 0000000..e063cda
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class RunTestTest {
+
+ /** Tests that [withContext] that sends work to other threads works in [runTest]. */
+ @Test
+ fun testWithContextDispatching() = runTest {
+ var counter = 0
+ withContext(Dispatchers.Default) {
+ counter += 1
+ }
+ assertEquals(counter, 1)
+ }
+
+ /** Tests that joining [GlobalScope.launch] works in [runTest]. */
+ @Test
+ fun testJoiningForkedJob() = runTest {
+ var counter = 0
+ val job = GlobalScope.launch {
+ counter += 1
+ }
+ job.join()
+ assertEquals(counter, 1)
+ }
+
+ /** Tests [suspendCoroutine] not failing [runTest]. */
+ @Test
+ fun testSuspendCoroutine() = runTest {
+ val answer = suspendCoroutine<Int> {
+ it.resume(42)
+ }
+ assertEquals(42, answer)
+ }
+
+ /** Tests that [runTest] attempts to detect it being run inside another [runTest] and failing in such scenarios. */
+ @Test
+ fun testNestedRunTestForbidden() = runTest {
+ assertFailsWith<IllegalStateException> {
+ runTest { }
+ }
+ }
+
+ /** Tests that even the dispatch timeout of `0` is fine if all the dispatches go through the same scheduler. */
+ @Test
+ fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTest(dispatchTimeoutMs = 0) {
+ // below is some arbitrary concurrent code where all dispatches go through the same scheduler.
+ launch {
+ delay(2000)
+ }
+ val deferred = async {
+ val job = launch(StandardTestDispatcher(testScheduler)) {
+ launch {
+ delay(500)
+ }
+ delay(1000)
+ }
+ job.join()
+ }
+ deferred.await()
+ }
+
+ /** Tests that a dispatch timeout of `0` will fail the test if there are some dispatches outside the scheduler. */
+ @Test
+ fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 0) {
+ withContext(Dispatchers.Default) {
+ delay(10)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ /** Tests that too low of a dispatch timeout causes crashes. */
+ @Test
+ @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
+ fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 100) {
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ /** Tests that too low of a dispatch timeout causes crashes. */
+ @Test
+ fun testRunTestWithLargeTimeout() = runTest(dispatchTimeoutMs = 5000) {
+ withContext(Dispatchers.Default) {
+ delay(50)
+ }
+ }
+
+ /** 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
+ fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
+ assertFailsWith<IllegalArgumentException> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 1) {
+ coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, IllegalArgumentException())
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ /** Tests that passing invalid contexts to [runTest] causes it to fail (on JS, without forking). */
+ @Test
+ fun testRunTestWithIllegalContext() {
+ for (ctx in TestScopeTest.invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ runTest(ctx) { }
+ }
+ }
+ }
+
+ /** Tests that throwing exceptions in [runTest] fails the test with them. */
+ @Test
+ fun testThrowingInRunTestBody() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTest {
+ throw RuntimeException()
+ }
+ }
+
+ /** Tests that throwing exceptions in pending tasks [runTest] fails the test with them. */
+ @Test
+ fun testThrowingInRunTestPendingTask() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTest {
+ launch {
+ delay(SLOW)
+ throw RuntimeException()
+ }
+ }
+ }
+
+ @Test
+ fun reproducer2405() = runTest {
+ val dispatcher = StandardTestDispatcher(testScheduler)
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ /** Tests that, once the test body has thrown, the child coroutines are cancelled. */
+ @Test
+ fun testChildrenCancellationOnTestBodyFailure(): TestResult {
+ var job: Job? = null
+ return testResultMap({
+ assertFailsWith<AssertionError> { it() }
+ assertTrue(job!!.isCancelled)
+ }) {
+ runTest {
+ job = launch {
+ while (true) {
+ delay(1000)
+ }
+ }
+ throw AssertionError()
+ }
+ }
+ }
+
+ /** Tests that [runTest] reports [TimeoutCancellationException]. */
+ @Test
+ fun testTimeout() = testResultMap({
+ assertFailsWith<TimeoutCancellationException> { it() }
+ }) {
+ runTest {
+ withTimeout(50) {
+ launch {
+ delay(1000)
+ }
+ }
+ }
+ }
+
+ /** Checks that [runTest] throws the root cause and not [JobCancellationException] when a child coroutine throws. */
+ @Test
+ fun testRunTestThrowsRootCause() = testResultMap({
+ assertFailsWith<TestException> { it() }
+ }) {
+ runTest {
+ launch {
+ throw TestException()
+ }
+ }
+ }
+
+ /** Tests that [runTest] completes its job. */
+ @Test
+ fun testCompletesOwnJob(): TestResult {
+ var handlerCalled = false
+ return testResultMap({
+ it()
+ assertTrue(handlerCalled)
+ }) {
+ runTest {
+ coroutineContext.job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ }
+ }
+ }
+
+ /** Tests that [runTest] doesn't complete the job that was passed to it as an argument. */
+ @Test
+ fun testDoesNotCompleteGivenJob(): TestResult {
+ var handlerCalled = false
+ val job = Job()
+ job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ return testResultMap({
+ it()
+ assertFalse(handlerCalled)
+ assertEquals(0, job.children.filter { it.isActive }.count())
+ }) {
+ runTest(job) {
+ assertTrue(coroutineContext.job in job.children)
+ }
+ }
+ }
+
+ /** Tests that, when the test body fails, the reported exceptions are suppressed. */
+ @Test
+ fun testSuppressedExceptions() = testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("w", e.message)
+ val suppressed = e.suppressedExceptions +
+ (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
+ assertEquals(3, suppressed.size)
+ assertEquals("x", suppressed[0].message)
+ assertEquals("y", suppressed[1].message)
+ assertEquals("z", suppressed[2].message)
+ }
+ }) {
+ runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ throw TestException("w")
+ }
+ }
+
+ /** Tests that [TestCoroutineScope.runTest] does not inherit the exception handler and works. */
+ @Test
+ fun testScopeRunTestExceptionHandler(): TestResult {
+ val scope = TestScope()
+ return testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt
new file mode 100644
index 0000000..d66be9b
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+class StandardTestDispatcherTest: OrderedExecutionTestBase() {
+
+ private val scope = TestScope(StandardTestDispatcher())
+
+ @BeforeTest
+ fun init() {
+ scope.asSpecificImplementation().enter()
+ }
+
+ @AfterTest
+ fun cleanup() {
+ scope.runCurrent()
+ assertEquals(listOf(), scope.asSpecificImplementation().leave())
+ }
+
+ /** Tests that the [StandardTestDispatcher] follows an execution order similar to `runBlocking`. */
+ @Test
+ fun testFlowsNotSkippingValues() = scope.launch {
+ // https://github.com/Kotlin/kotlinx.coroutines/issues/1626#issuecomment-554632852
+ val list = flowOf(1).onStart { emit(0) }
+ .combine(flowOf("A")) { int, str -> "$str$int" }
+ .toList()
+ assertEquals(list, listOf("A0", "A1"))
+ }.void()
+
+ /** Tests that each [launch] gets dispatched. */
+ @Test
+ fun testLaunchDispatched() = scope.launch {
+ expect(1)
+ launch {
+ expect(3)
+ }
+ finish(2)
+ }.void()
+
+ /** Tests that dispatching is done in a predictable order and [yield] puts this task at the end of the queue. */
+ @Test
+ fun testYield() = scope.launch {
+ expect(1)
+ scope.launch {
+ expect(3)
+ yield()
+ expect(6)
+ }
+ scope.launch {
+ expect(4)
+ yield()
+ finish(7)
+ }
+ expect(2)
+ yield()
+ expect(5)
+ }.void()
+
+ /** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */
+ @Test
+ @NoNative
+ fun testSchedulerReuse() {
+ val dispatcher1 = StandardTestDispatcher()
+ Dispatchers.setMain(dispatcher1)
+ try {
+ val dispatcher2 = StandardTestDispatcher()
+ assertSame(dispatcher1.scheduler, dispatcher2.scheduler)
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt
new file mode 100644
index 0000000..203ddc4
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+class TestCoroutineSchedulerTest {
+ /** Tests that `TestCoroutineScheduler` attempts to detect if there are several instances of it. */
+ @Test
+ fun testContextElement() = runTest {
+ assertFailsWith<IllegalStateException> {
+ withContext(StandardTestDispatcher()) {
+ }
+ }
+ }
+
+ /** Tests that, as opposed to [DelayController.advanceTimeBy] or [TestCoroutineScope.advanceTimeBy],
+ * [TestCoroutineScheduler.advanceTimeBy] doesn't run the tasks scheduled at the target moment. */
+ @Test
+ fun testAdvanceTimeByDoesNotRunCurrent() = runTest {
+ var entered = false
+ launch {
+ delay(15)
+ entered = true
+ }
+ testScheduler.advanceTimeBy(15)
+ assertFalse(entered)
+ testScheduler.runCurrent()
+ assertTrue(entered)
+ }
+
+ /** Tests that [TestCoroutineScheduler.advanceTimeBy] doesn't accept negative delays. */
+ @Test
+ fun testAdvanceTimeByWithNegativeDelay() {
+ val scheduler = TestCoroutineScheduler()
+ assertFailsWith<IllegalArgumentException> {
+ scheduler.advanceTimeBy(-1)
+ }
+ }
+
+ /** Tests that if [TestCoroutineScheduler.advanceTimeBy] encounters an arithmetic overflow, all the tasks scheduled
+ * until the moment [Long.MAX_VALUE] get run. */
+ @Test
+ fun testAdvanceTimeByEnormousDelays() = forTestDispatchers {
+ assertRunsFast {
+ with (TestScope(it)) {
+ launch {
+ val initialDelay = 10L
+ delay(initialDelay)
+ assertEquals(initialDelay, currentTime)
+ var enteredInfinity = false
+ launch {
+ delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing
+ assertEquals(Long.MAX_VALUE, currentTime)
+ enteredInfinity = true
+ }
+ var enteredNearInfinity = false
+ launch {
+ delay(Long.MAX_VALUE - initialDelay - 1)
+ assertEquals(Long.MAX_VALUE - 1, currentTime)
+ enteredNearInfinity = true
+ }
+ testScheduler.advanceTimeBy(Long.MAX_VALUE)
+ assertFalse(enteredInfinity)
+ assertTrue(enteredNearInfinity)
+ assertEquals(Long.MAX_VALUE, currentTime)
+ testScheduler.runCurrent()
+ assertTrue(enteredInfinity)
+ }
+ testScheduler.advanceUntilIdle()
+ }
+ }
+ }
+
+ /** Tests the basic functionality of [TestCoroutineScheduler.advanceTimeBy]. */
+ @Test
+ fun testAdvanceTimeBy() = runTest {
+ assertRunsFast {
+ var stage = 1
+ launch {
+ delay(1_000)
+ assertEquals(1_000, currentTime)
+ stage = 2
+ delay(500)
+ assertEquals(1_500, currentTime)
+ stage = 3
+ delay(501)
+ assertEquals(2_001, currentTime)
+ stage = 4
+ }
+ assertEquals(1, stage)
+ assertEquals(0, currentTime)
+ advanceTimeBy(2_000)
+ assertEquals(3, stage)
+ assertEquals(2_000, currentTime)
+ advanceTimeBy(2)
+ assertEquals(4, stage)
+ assertEquals(2_002, currentTime)
+ }
+ }
+
+ /** Tests the basic functionality of [TestCoroutineScheduler.runCurrent]. */
+ @Test
+ fun testRunCurrent() = runTest {
+ var stage = 0
+ launch {
+ delay(1)
+ ++stage
+ delay(1)
+ stage += 10
+ }
+ launch {
+ delay(1)
+ ++stage
+ delay(1)
+ stage += 10
+ }
+ testScheduler.advanceTimeBy(1)
+ assertEquals(0, stage)
+ runCurrent()
+ assertEquals(2, stage)
+ testScheduler.advanceTimeBy(1)
+ assertEquals(2, stage)
+ runCurrent()
+ assertEquals(22, stage)
+ }
+
+ /** Tests that [TestCoroutineScheduler.runCurrent] will not run new tasks after the current time has advanced. */
+ @Test
+ fun testRunCurrentNotDrainingQueue() = forTestDispatchers {
+ assertRunsFast {
+ val scheduler = it.scheduler
+ val scope = TestScope(it)
+ var stage = 1
+ scope.launch {
+ delay(SLOW)
+ launch {
+ delay(SLOW)
+ stage = 3
+ }
+ scheduler.advanceTimeBy(SLOW)
+ stage = 2
+ }
+ scheduler.advanceTimeBy(SLOW)
+ assertEquals(1, stage)
+ scheduler.runCurrent()
+ assertEquals(2, stage)
+ scheduler.runCurrent()
+ assertEquals(3, stage)
+ }
+ }
+
+ /** Tests that [TestCoroutineScheduler.advanceUntilIdle] doesn't hang when itself running in a scheduler task. */
+ @Test
+ fun testNestedAdvanceUntilIdle() = forTestDispatchers {
+ assertRunsFast {
+ val scheduler = it.scheduler
+ val scope = TestScope(it)
+ var executed = false
+ scope.launch {
+ launch {
+ delay(SLOW)
+ executed = true
+ }
+ scheduler.advanceUntilIdle()
+ }
+ scheduler.advanceUntilIdle()
+ assertTrue(executed)
+ }
+ }
+
+ /** Tests [yield] scheduling tasks for future execution and not executing immediately. */
+ @Test
+ fun testYield() = forTestDispatchers {
+ val scope = TestScope(it)
+ var stage = 0
+ scope.launch {
+ yield()
+ assertEquals(1, stage)
+ stage = 2
+ }
+ scope.launch {
+ yield()
+ assertEquals(2, stage)
+ stage = 3
+ }
+ assertEquals(0, stage)
+ stage = 1
+ scope.runCurrent()
+ }
+
+ /** Tests that dispatching the delayed tasks is ordered by their waking times. */
+ @Test
+ fun testDelaysPriority() = forTestDispatchers {
+ val scope = TestScope(it)
+ var lastMeasurement = 0L
+ fun checkTime(time: Long) {
+ assertTrue(lastMeasurement < time)
+ assertEquals(time, scope.currentTime)
+ lastMeasurement = scope.currentTime
+ }
+ scope.launch {
+ launch {
+ delay(100)
+ checkTime(100)
+ val deferred = async {
+ delay(70)
+ checkTime(170)
+ }
+ delay(1)
+ checkTime(101)
+ deferred.await()
+ delay(1)
+ checkTime(171)
+ }
+ launch {
+ delay(200)
+ checkTime(200)
+ }
+ launch {
+ delay(150)
+ checkTime(150)
+ delay(22)
+ checkTime(172)
+ }
+ delay(201)
+ }
+ scope.advanceUntilIdle()
+ checkTime(201)
+ }
+
+ private fun TestScope.checkTimeout(
+ timesOut: Boolean, timeoutMillis: Long = SLOW, block: suspend () -> Unit
+ ) = assertRunsFast {
+ var caughtException = false
+ asSpecificImplementation().enter()
+ launch {
+ try {
+ withTimeout(timeoutMillis) {
+ block()
+ }
+ } catch (e: TimeoutCancellationException) {
+ caughtException = true
+ }
+ }
+ advanceUntilIdle()
+ asSpecificImplementation().leave().throwAll()
+ if (timesOut)
+ assertTrue(caughtException)
+ else
+ assertFalse(caughtException)
+ }
+
+ /** Tests that timeouts get triggered. */
+ @Test
+ fun testSmallTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ scope.checkTimeout(true) {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half)
+ }
+ }
+
+ /** Tests that timeouts don't get triggered if the code finishes in time. */
+ @Test
+ fun testLargeTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ scope.checkTimeout(false) {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half - 1)
+ }
+ }
+
+ /** Tests that timeouts get triggered if the code fails to finish in time asynchronously. */
+ @Test
+ fun testSmallAsynchronousTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ val deferred = CompletableDeferred<Unit>()
+ scope.launch {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half)
+ deferred.complete(Unit)
+ }
+ scope.checkTimeout(true) {
+ deferred.await()
+ }
+ }
+
+ /** Tests that timeouts don't get triggered if the code finishes in time, even if it does so asynchronously. */
+ @Test
+ fun testLargeAsynchronousTimeouts() = forTestDispatchers {
+ val scope = TestScope(it)
+ val deferred = CompletableDeferred<Unit>()
+ scope.launch {
+ val half = SLOW / 2
+ delay(half)
+ delay(SLOW - half - 1)
+ deferred.complete(Unit)
+ }
+ scope.checkTimeout(false) {
+ deferred.await()
+ }
+ }
+
+ private fun forTestDispatchers(block: (TestDispatcher) -> Unit): Unit =
+ @Suppress("DEPRECATION")
+ listOf(
+ StandardTestDispatcher(),
+ UnconfinedTestDispatcher()
+ ).forEach {
+ try {
+ block(it)
+ } catch (e: Throwable) {
+ throw RuntimeException("Test failed for dispatcher $it", e)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt
deleted file mode 100644
index 4480cd9..0000000
--- a/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlin.test.*
-
-class TestCoroutineScopeTest {
- @Test
- fun whenGivenInvalidExceptionHandler_throwsException() {
- val handler = CoroutineExceptionHandler { _, _ -> }
- assertFails {
- TestCoroutineScope(handler)
- }
- }
-
- @Test
- fun whenGivenInvalidDispatcher_throwsException() {
- assertFails {
- TestCoroutineScope(Dispatchers.Default)
- }
- }
-}
diff --git a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt
index bf391ea..66a6c24 100644
--- a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt
+++ b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt
@@ -1,41 +1,67 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
+package kotlinx.coroutines.test
-import kotlinx.atomicfu.*
import kotlinx.coroutines.*
-import kotlinx.coroutines.test.*
+import kotlinx.coroutines.test.internal.*
import kotlin.coroutines.*
import kotlin.test.*
-class TestDispatchersTest {
- private val actionIndex = atomic(0)
- private val finished = atomic(false)
-
- private fun expect(index: Int) {
- val wasIndex = actionIndex.incrementAndGet()
- check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
- }
-
- private fun finish(index: Int) {
- expect(index)
- check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
- }
+@NoNative
+class TestDispatchersTest: OrderedExecutionTestBase() {
@BeforeTest
fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+ }
+
+ @AfterTest
+ fun tearDown() {
Dispatchers.resetMain()
}
+ /** Tests that asynchronous execution of tests does not happen concurrently with [AfterTest]. */
@Test
- @NoNative
+ @NoJs
+ fun testMainMocking() = runTest {
+ val mainAtStart = TestMainDispatcher.currentTestDispatcher
+ assertNotNull(mainAtStart)
+ withContext(Dispatchers.Main) {
+ delay(10)
+ }
+ withContext(Dispatchers.Default) {
+ delay(10)
+ }
+ withContext(Dispatchers.Main) {
+ delay(10)
+ }
+ assertSame(mainAtStart, TestMainDispatcher.currentTestDispatcher)
+ }
+
+ /** Tests that the mocked [Dispatchers.Main] correctly forwards [Delay] methods. */
+ @Test
+ fun testMockedMainImplementsDelay() = runTest {
+ val main = Dispatchers.Main
+ withContext(main) {
+ delay(10)
+ }
+ withContext(Dispatchers.Default) {
+ delay(10)
+ }
+ withContext(main) {
+ delay(10)
+ }
+ }
+
+ /** Tests that [Distpachers.setMain] fails when called with [Dispatchers.Main]. */
+ @Test
fun testSelfSet() {
assertFailsWith<IllegalArgumentException> { Dispatchers.setMain(Dispatchers.Main) }
}
@Test
- @NoNative
- fun testImmediateDispatcher() = runBlockingTest {
+ fun testImmediateDispatcher() = runTest {
Dispatchers.setMain(ImmediateDispatcher())
expect(1)
withContext(Dispatchers.Main) {
diff --git a/kotlinx-coroutines-test/common/test/TestModuleHelpers.kt b/kotlinx-coroutines-test/common/test/TestModuleHelpers.kt
deleted file mode 100644
index a34dbfd..0000000
--- a/kotlinx-coroutines-test/common/test/TestModuleHelpers.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.test
-
-import kotlinx.coroutines.*
-import kotlin.test.*
-import kotlin.time.*
-
-const val SLOW = 10_000L
-
-/**
- * Assert a block completes within a second or fail the suite
- */
-@OptIn(ExperimentalTime::class)
-suspend fun CoroutineScope.assertRunsFast(block: suspend CoroutineScope.() -> Unit) {
- val start = TimeSource.Monotonic.markNow()
- // don't need to be fancy with timeouts here since anything longer than a few ms is an error
- block()
- val duration = start.elapsedNow()
- assertTrue("All tests must complete within 2000ms (use longer timeouts to cause failure)") {
- duration.inWholeSeconds < 2
- }
-}
diff --git a/kotlinx-coroutines-test/common/test/TestScopeTest.kt b/kotlinx-coroutines-test/common/test/TestScopeTest.kt
new file mode 100644
index 0000000..7031056
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/TestScopeTest.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class TestScopeTest {
+ /** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */
+ @Test
+ fun testCreateThrowsOnInvalidArguments() {
+ for (ctx in invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ TestScope(ctx)
+ }
+ }
+ }
+
+ /** Tests that a newly-created [TestCoroutineScope] provides the correct scheduler. */
+ @Test
+ fun testCreateProvidesScheduler() {
+ // Creates a new scheduler.
+ run {
+ val scope = TestScope()
+ assertNotNull(scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Reuses the scheduler that the dispatcher is linked to.
+ run {
+ val dispatcher = StandardTestDispatcher()
+ val scope = TestScope(dispatcher)
+ assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Uses the scheduler passed to it.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val scope = TestScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler)
+ }
+ // Doesn't touch the passed dispatcher and the scheduler if they match.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val scope = TestScope(scheduler + dispatcher)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor])
+ }
+ }
+
+ /** Part of [testCreateProvidesScheduler], disabled for Native */
+ @Test
+ @NoNative
+ fun testCreateReusesScheduler() {
+ // Reuses the scheduler of `Dispatchers.Main`
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val mainDispatcher = StandardTestDispatcher(scheduler)
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scope = TestScope()
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed
+ run {
+ val mainDispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scheduler = TestCoroutineScheduler()
+ val scope = TestScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ }
+
+ /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */
+ @Test
+ fun testPresentDelaysThrowing() {
+ val scope = TestScope()
+ var result = false
+ scope.launch {
+ delay(5)
+ result = true
+ }
+ assertFalse(result)
+ scope.asSpecificImplementation().enter()
+ assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().leave() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure throws if there were active jobs by the end. */
+ @Test
+ fun testActiveJobsThrowing() {
+ val scope = TestScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ scope.launch {
+ deferred.await()
+ result = true
+ }
+ assertFalse(result)
+ scope.asSpecificImplementation().enter()
+ assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().leave() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure throws even if it detects that the job is already cancelled. */
+ @Test
+ fun testCancelledDelaysThrowing() {
+ val scope = TestScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ val job = scope.launch {
+ deferred.await()
+ result = true
+ }
+ job.cancel()
+ assertFalse(result)
+ scope.asSpecificImplementation().enter()
+ assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().leave() }
+ assertFalse(result)
+ }
+
+ /** Tests that uncaught exceptions are thrown at the cleanup. */
+ @Test
+ fun testGetsCancelledOnChildFailure(): TestResult {
+ val scope = TestScope()
+ val exception = TestException("test")
+ scope.launch {
+ throw exception
+ }
+ return testResultMap({
+ try {
+ it()
+ fail("should not reach")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ }
+ }
+ }
+
+ /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */
+ @Test
+ fun testSuppressedExceptions() {
+ TestScope().apply {
+ asSpecificImplementation().enter()
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ runCurrent()
+ val e = asSpecificImplementation().leave()
+ assertEquals(3, e.size)
+ assertEquals("x", e[0].message)
+ assertEquals("y", e[1].message)
+ assertEquals("z", e[2].message)
+ }
+ }
+
+ companion object {
+ internal val invalidContexts = listOf(
+ Dispatchers.Default, // not a [TestDispatcher]
+ CoroutineExceptionHandler { _, _ -> }, // exception handlers can't be overridden
+ StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
+ )
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt
new file mode 100644
index 0000000..ee63e6d
--- /dev/null
+++ b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+class UnconfinedTestDispatcherTest {
+
+ @Test
+ fun reproducer1742() {
+ class ObservableValue<T>(initial: T) {
+ var value: T = initial
+ private set
+
+ private val listeners = mutableListOf<(T) -> Unit>()
+
+ fun set(value: T) {
+ this.value = value
+ listeners.forEach { it(value) }
+ }
+
+ fun addListener(listener: (T) -> Unit) {
+ listeners.add(listener)
+ }
+
+ fun removeListener(listener: (T) -> Unit) {
+ listeners.remove(listener)
+ }
+ }
+
+ fun <T> ObservableValue<T>.observe(): Flow<T> =
+ callbackFlow {
+ val listener = { value: T ->
+ if (!isClosedForSend) {
+ trySend(value)
+ }
+ }
+ addListener(listener)
+ listener(value)
+ awaitClose { removeListener(listener) }
+ }
+
+ val intProvider = ObservableValue(0)
+ val stringProvider = ObservableValue("")
+ var data = Pair(0, "")
+ val scope = CoroutineScope(UnconfinedTestDispatcher())
+ scope.launch {
+ combine(
+ intProvider.observe(),
+ stringProvider.observe()
+ ) { intValue, stringValue -> Pair(intValue, stringValue) }
+ .collect { pair ->
+ data = pair
+ }
+ }
+
+ intProvider.set(1)
+ stringProvider.set("3")
+ intProvider.set(2)
+ intProvider.set(3)
+
+ scope.cancel()
+ assertEquals(Pair(3, "3"), data)
+ }
+
+ @Test
+ fun reproducer2082() = runTest {
+ val subject1 = MutableStateFlow(1)
+ val subject2 = MutableStateFlow("a")
+ val values = mutableListOf<Pair<Int, String>>()
+
+ val job = launch(UnconfinedTestDispatcher(testScheduler)) {
+ combine(subject1, subject2) { intVal, strVal -> intVal to strVal }
+ .collect {
+ delay(10000)
+ values += it
+ }
+ }
+
+ subject1.value = 2
+ delay(10000)
+ subject2.value = "b"
+ delay(10000)
+
+ subject1.value = 3
+ delay(10000)
+ subject2.value = "c"
+ delay(10000)
+ delay(10000)
+ delay(1)
+
+ job.cancel()
+
+ assertEquals(listOf(Pair(1, "a"), Pair(2, "a"), Pair(2, "b"), Pair(3, "b"), Pair(3, "c")), values)
+ }
+
+ @Test
+ fun reproducer2405() = createTestResult {
+ val dispatcher = UnconfinedTestDispatcher()
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ /** An example from the [UnconfinedTestDispatcher] documentation. */
+ @Test
+ fun testUnconfinedDispatcher() = runTest {
+ val values = mutableListOf<Int>()
+ val stateFlow = MutableStateFlow(0)
+ val job = launch(UnconfinedTestDispatcher(testScheduler)) {
+ stateFlow.collect {
+ values.add(it)
+ }
+ }
+ stateFlow.value = 1
+ stateFlow.value = 2
+ stateFlow.value = 3
+ job.cancel()
+ assertEquals(listOf(0, 1, 2, 3), values)
+ }
+
+ /** Tests that child coroutines are eagerly entered. */
+ @Test
+ fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
+ var entered = false
+ val deferred = CompletableDeferred<Unit>()
+ var completed = false
+ launch {
+ entered = true
+ deferred.await()
+ completed = true
+ }
+ assertTrue(entered) // `entered = true` already executed.
+ assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
+ deferred.complete(Unit) // resume the coroutine.
+ assertTrue(completed) // now the child coroutine is immediately completed.
+ }
+
+ /** Tests that the [TestCoroutineScheduler] used for [Dispatchers.Main] gets used by default. */
+ @Test
+ @NoNative
+ fun testSchedulerReuse() {
+ val dispatcher1 = StandardTestDispatcher()
+ Dispatchers.setMain(dispatcher1)
+ try {
+ val dispatcher2 = UnconfinedTestDispatcher()
+ assertSame(dispatcher1.scheduler, dispatcher2.scheduler)
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/js/src/TestBuilders.kt b/kotlinx-coroutines-test/js/src/TestBuilders.kt
new file mode 100644
index 0000000..3976885
--- /dev/null
+++ b/kotlinx-coroutines-test/js/src/TestBuilders.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+import kotlinx.coroutines.*
+import kotlin.js.*
+
+@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
+public actual typealias TestResult = Promise<Unit>
+
+internal actual fun createTestResult(testProcedure: suspend () -> Unit): TestResult =
+ GlobalScope.promise {
+ testProcedure()
+ }
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/js/test/FailingTests.kt b/kotlinx-coroutines-test/js/test/FailingTests.kt
new file mode 100644
index 0000000..4746a73
--- /dev/null
+++ b/kotlinx-coroutines-test/js/test/FailingTests.kt
@@ -0,0 +1,37 @@
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.test.internal.*
+import kotlin.test.*
+
+/** These are tests that we want to fail. They are here so that, when the issue is fixed, their failure indicates that
+ * everything is better now. */
+class FailingTests {
+
+ private var tearDownEntered = false
+
+ @BeforeTest
+ fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+ }
+
+ @AfterTest
+ fun tearDown() {
+ Dispatchers.resetMain()
+ tearDownEntered = true
+ }
+
+ /** [TestDispatchersTest.testMainMocking]. */
+ @Test
+ fun testAfterTestIsConcurrent() = runTest {
+ try {
+ val mainAtStart = TestMainDispatcher.currentTestDispatcher ?: return@runTest
+ withContext(Dispatchers.Default) {
+ // context switch
+ }
+ assertNotSame(mainAtStart, TestMainDispatcher.currentTestDispatcher!!)
+ } finally {
+ assertTrue(tearDownEntered)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/js/test/Helpers.kt b/kotlinx-coroutines-test/js/test/Helpers.kt
new file mode 100644
index 0000000..5f19d1a
--- /dev/null
+++ b/kotlinx-coroutines-test/js/test/Helpers.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlin.test.*
+
+actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult =
+ test().then(
+ {
+ block {
+ }
+ }, {
+ block {
+ throw it
+ }
+ })
+
+actual typealias NoJs = Ignore
diff --git a/kotlinx-coroutines-test/js/test/PromiseTest.kt b/kotlinx-coroutines-test/js/test/PromiseTest.kt
new file mode 100644
index 0000000..ff09d9a
--- /dev/null
+++ b/kotlinx-coroutines-test/js/test/PromiseTest.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+class PromiseTest {
+ @Test
+ fun testCompletionFromPromise() = runTest {
+ var promiseEntered = false
+ val p = promise {
+ delay(1)
+ promiseEntered = true
+ }
+ delay(2)
+ p.await()
+ assertTrue(promiseEntered)
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
new file mode 100644
index 0000000..7cafb54
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+public actual typealias TestResult = Unit
+
+internal actual fun createTestResult(testProcedure: suspend () -> Unit) {
+ runBlocking {
+ testProcedure()
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt
new file mode 100644
index 0000000..e0701ae
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+
+/**
+ * Control the virtual clock time of a [CoroutineDispatcher].
+ *
+ * Testing libraries may expose this interface to the tests instead of [TestCoroutineDispatcher].
+ */
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "Use `TestCoroutineScheduler` to control virtual time.",
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public interface DelayController {
+ /**
+ * Returns the current virtual clock-time as it is known to this Dispatcher.
+ *
+ * @return The virtual clock-time
+ */
+ @ExperimentalCoroutinesApi
+ public val currentTime: Long
+
+ /**
+ * Moves the Dispatcher's virtual clock forward by a specified amount of time.
+ *
+ * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses
+ * blocking coroutines.
+ *
+ * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested
+ * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different
+ * calls to delay.
+ *
+ * ```
+ * @Test
+ * fun advanceTimeTest() = runBlockingTest {
+ * foo()
+ * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays
+ * // virtual time is 2_000, next resume is at 2_001
+ * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced)
+ * // virtual time is 2_0002
+ * }
+ *
+ * fun CoroutineScope.foo() {
+ * launch {
+ * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000)
+ * // virtual time is 1_000
+ * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500)
+ * // virtual time is 1_500
+ * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001)
+ * // virtual time is 2_001
+ * }
+ * }
+ * ```
+ *
+ * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
+ * @return The amount of delay-time that this Dispatcher's clock has been forwarded.
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceTimeBy(delayTimeMillis: Long): Long
+
+ /**
+ * Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
+ *
+ * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
+ * returns.
+ *
+ * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
+ */
+ @ExperimentalCoroutinesApi
+ public fun advanceUntilIdle(): Long
+
+ /**
+ * Run any tasks that are pending at or before the current virtual clock-time.
+ *
+ * Calling this function will never advance the clock.
+ */
+ @ExperimentalCoroutinesApi
+ public fun runCurrent()
+
+ /**
+ * Call after test code completes to ensure that the dispatcher is properly cleaned up.
+ *
+ * @throws AssertionError if any pending tasks are active, however it will not throw for suspended
+ * coroutines.
+ */
+ @ExperimentalCoroutinesApi
+ @Throws(AssertionError::class)
+ public fun cleanupTestCoroutines()
+
+ /**
+ * Run a block of code in a paused dispatcher.
+ *
+ * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
+ * will resume auto-advancing.
+ *
+ * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
+ * setup may be done between the time the coroutine is created and started.
+ */
+ @Deprecated(
+ "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public suspend fun pauseDispatcher(block: suspend () -> Unit)
+
+ /**
+ * Pause the dispatcher.
+ *
+ * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
+ * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
+ */
+ @Deprecated(
+ "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public fun pauseDispatcher()
+
+ /**
+ * Resume the dispatcher from a paused state.
+ *
+ * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
+ * time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
+ * or [advanceUntilIdle].
+ */
+ @Deprecated(
+ "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public fun resumeDispatcher()
+}
+
+internal interface SchedulerAsDelayController : DelayController {
+ val scheduler: TestCoroutineScheduler
+
+ /** @suppress */
+ @Deprecated(
+ "This property delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.currentTime"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override val currentTime: Long
+ get() = scheduler.currentTime
+
+
+ /** @suppress */
+ @Deprecated(
+ "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override fun advanceTimeBy(delayTimeMillis: Long): Long {
+ val oldTime = scheduler.currentTime
+ scheduler.advanceTimeBy(delayTimeMillis)
+ scheduler.runCurrent()
+ return scheduler.currentTime - oldTime
+ }
+
+ /** @suppress */
+ @Deprecated(
+ "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.advanceUntilIdle()"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override fun advanceUntilIdle(): Long {
+ val oldTime = scheduler.currentTime
+ scheduler.advanceUntilIdle()
+ return scheduler.currentTime - oldTime
+ }
+
+ /** @suppress */
+ @Deprecated(
+ "This function delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
+ ReplaceWith("this.scheduler.runCurrent()"),
+ level = DeprecationLevel.WARNING
+ )
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ override fun runCurrent(): Unit = scheduler.runCurrent()
+
+ /** @suppress */
+ @ExperimentalCoroutinesApi
+ override fun cleanupTestCoroutines() {
+ // process any pending cancellations or completions, but don't advance time
+ scheduler.runCurrent()
+ if (!scheduler.isIdle(strict = false)) {
+ throw UncompletedCoroutinesError(
+ "Unfinished coroutines during tear-down. Ensure all coroutines are" +
+ " completed or cancelled by your test."
+ )
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt
new file mode 100644
index 0000000..4524bf2
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+@file:JvmName("TestBuildersKt")
+@file:JvmMultifileClass
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.selects.*
+import kotlin.coroutines.*
+import kotlin.jvm.*
+
+/**
+ * Executes a [testBody] inside an immediate execution dispatcher.
+ *
+ * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
+ * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
+ * extra time.
+ *
+ * ```
+ * @Test
+ * fun exampleTest() = runBlockingTest {
+ * val deferred = async {
+ * delay(1_000)
+ * async {
+ * delay(1_000)
+ * }.await()
+ * }
+ *
+ * deferred.await() // result available immediately
+ * }
+ *
+ * ```
+ *
+ * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
+ * conditions.
+ *
+ * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
+ *
+ * @throws AssertionError If the [testBody] does not complete (or cancel) all coroutines that it launches
+ * (including coroutines suspended on join/await).
+ *
+ * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
+ * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
+ * @param testBody The code of the unit-test.
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun runBlockingTest(
+ context: CoroutineContext = EmptyCoroutineContext,
+ testBody: suspend TestCoroutineScope.() -> Unit
+) {
+ val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
+ val scheduler = scope.testScheduler
+ val deferred = scope.async {
+ scope.testBody()
+ }
+ scheduler.advanceUntilIdle()
+ deferred.getCompletionExceptionOrNull()?.let {
+ throw it
+ }
+ scope.cleanupTestCoroutines()
+}
+
+/**
+ * A version of [runBlockingTest] that works with [TestScope].
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun runBlockingTestOnTestScope(
+ context: CoroutineContext = EmptyCoroutineContext,
+ testBody: suspend TestScope.() -> Unit
+) {
+ val completeContext = TestCoroutineDispatcher() + SupervisorJob() + context
+ val startJobs = completeContext.activeJobs()
+ val scope = TestScope(completeContext).asSpecificImplementation()
+ scope.enter()
+ scope.start(CoroutineStart.UNDISPATCHED, scope) {
+ scope.testBody()
+ }
+ scope.testScheduler.advanceUntilIdle()
+ try {
+ scope.getCompletionExceptionOrNull()
+ } catch (e: IllegalStateException) {
+ null // the deferred was not completed yet; `scope.leave()` should complain then about unfinished jobs
+ }?.let {
+ val exceptions = try {
+ scope.leave()
+ } catch (e: UncompletedCoroutinesError) {
+ listOf()
+ }
+ (listOf(it) + exceptions).throwAll()
+ return
+ }
+ scope.leave().throwAll()
+ val jobs = completeContext.activeJobs() - startJobs
+ if (jobs.isNotEmpty())
+ throw UncompletedCoroutinesError("Some jobs were not completed at the end of the test: $jobs")
+}
+
+/**
+ * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
+ runBlockingTest(coroutineContext, block)
+
+/**
+ * Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope].
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit =
+ runBlockingTestOnTestScope(coroutineContext, block)
+
+/**
+ * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
+ */
+@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
+ runBlockingTest(this, block)
+
+/**
+ * This is an overload of [runTest] that works with [TestCoroutineScope].
+ */
+@ExperimentalCoroutinesApi
+@Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun runTestWithLegacyScope(
+ context: CoroutineContext = EmptyCoroutineContext,
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ testBody: suspend TestCoroutineScope.() -> Unit
+): TestResult {
+ if (context[RunningInRunTest] != null)
+ throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
+ val testScope = TestBodyCoroutine<Unit>(createTestCoroutineScope(context + RunningInRunTest))
+ return createTestResult {
+ runTestCoroutine(testScope, dispatchTimeoutMs, testBody) {
+ try {
+ testScope.cleanup()
+ emptyList()
+ } catch (e: UncompletedCoroutinesError) {
+ throw e
+ } catch (e: Throwable) {
+ listOf(e)
+ }
+ }
+ }
+}
+
+/**
+ * Runs a test in a [TestCoroutineScope] based on this one.
+ *
+ * Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the
+ * [block] will be different from this one, but will use its [Job] as a parent.
+ *
+ * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
+ * immediately from the test body. See the docs for [TestResult] for details.
+ */
+@ExperimentalCoroutinesApi
+@Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.runTest(
+ dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
+ block: suspend TestCoroutineScope.() -> Unit
+): TestResult = runTestWithLegacyScope(coroutineContext, dispatchTimeoutMs, block)
+
+private class TestBodyCoroutine<T>(
+ private val testScope: TestCoroutineScope,
+) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
+
+ override val testScheduler get() = testScope.testScheduler
+
+ @Deprecated(
+ "This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
+ ReplaceWith("this.cleanup()"),
+ DeprecationLevel.ERROR
+ )
+ override fun cleanupTestCoroutines() =
+ throw UnsupportedOperationException(
+ "Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
+ "it will be called at the end of the test in any case."
+ )
+
+ fun cleanup() = testScope.cleanupTestCoroutines()
+}
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt
new file mode 100644
index 0000000..ec2a304
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+
+/**
+ * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests
+ * and uses a [TestCoroutineScheduler] to control its virtual clock.
+ *
+ * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are
+ * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the
+ * methods on the dispatcher's [scheduler].
+ *
+ * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will
+ * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the
+ * methods on [DelayController].
+ *
+ * @see DelayController
+ */
+@Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of " +
+ "pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.",
+ level = DeprecationLevel.WARNING)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public class TestCoroutineDispatcher(public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler()):
+ TestDispatcher(), Delay, SchedulerAsDelayController
+{
+ private var dispatchImmediately = true
+ set(value) {
+ field = value
+ if (value) {
+ // there may already be tasks from setup code we need to run
+ scheduler.advanceUntilIdle()
+ }
+ }
+
+ /** @suppress */
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ if (dispatchImmediately) {
+ scheduler.sendDispatchEvent()
+ block.run()
+ } else {
+ post(block)
+ }
+ }
+
+ /** @suppress */
+ override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+ checkSchedulerInContext(scheduler, context)
+ post(block)
+ }
+
+ /** @suppress */
+ override fun toString(): String = "TestCoroutineDispatcher[scheduler=$scheduler]"
+
+ private fun post(block: Runnable) =
+ scheduler.registerEvent(this, 0, block) { false }
+
+ /** @suppress */
+ override suspend fun pauseDispatcher(block: suspend () -> Unit) {
+ val previous = dispatchImmediately
+ dispatchImmediately = false
+ try {
+ block()
+ } finally {
+ dispatchImmediately = previous
+ }
+ }
+
+ /** @suppress */
+ override fun pauseDispatcher() {
+ dispatchImmediately = false
+ }
+
+ /** @suppress */
+ override fun resumeDispatcher() {
+ dispatchImmediately = true
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
new file mode 100644
index 0000000..9da521f
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import kotlin.coroutines.*
+
+/**
+ * Access uncaught coroutine exceptions captured during test execution.
+ */
+@Deprecated(
+ "Deprecated for removal without a replacement. " +
+ "Consider whether the default mechanism of handling uncaught exceptions is sufficient. " +
+ "If not, try writing your own `CoroutineExceptionHandler` and " +
+ "please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.",
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public interface UncaughtExceptionCaptor {
+ /**
+ * List of uncaught coroutine exceptions.
+ *
+ * The returned list is a copy of the currently caught exceptions.
+ * During [cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
+ */
+ public val uncaughtExceptions: List<Throwable>
+
+ /**
+ * Call after the test completes to ensure that there were no uncaught exceptions.
+ *
+ * The first exception in uncaughtExceptions is rethrown. All other exceptions are
+ * printed using [Throwable.printStackTrace].
+ *
+ * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
+ */
+ public fun cleanupTestCoroutines()
+}
+
+/**
+ * An exception handler that captures uncaught exceptions in tests.
+ */
+@Deprecated(
+ "Deprecated for removal without a replacement. " +
+ "It may be to define one's own `CoroutineExceptionHandler` if you just need to handle '" +
+ "uncaught exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public class TestCoroutineExceptionHandler :
+ AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor {
+ private val _exceptions = mutableListOf<Throwable>()
+ private val _lock = SynchronizedObject()
+ private var _coroutinesCleanedUp = false
+
+ @Suppress("INVISIBLE_MEMBER")
+ override fun handleException(context: CoroutineContext, exception: Throwable) {
+ synchronized(_lock) {
+ if (_coroutinesCleanedUp) {
+ handleCoroutineExceptionImpl(context, exception)
+ }
+ _exceptions += exception
+ }
+ }
+
+ public override val uncaughtExceptions: List<Throwable>
+ get() = synchronized(_lock) { _exceptions.toList() }
+
+ public override fun cleanupTestCoroutines() {
+ synchronized(_lock) {
+ _coroutinesCleanedUp = true
+ val exception = _exceptions.firstOrNull() ?: return
+ // log the rest
+ _exceptions.drop(1).forEach { it.printStackTrace() }
+ throw exception
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt
new file mode 100644
index 0000000..45a3815
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.internal.*
+import kotlin.coroutines.*
+
+/**
+ * A scope which provides detailed control over the execution of coroutines for tests.
+ */
+@ExperimentalCoroutinesApi
+@Deprecated("Use `TestScope` in combination with `runTest` instead")
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public sealed interface TestCoroutineScope : CoroutineScope {
+ /**
+ * Called after the test completes.
+ *
+ * * It checks that there were no uncaught exceptions caught by its [CoroutineExceptionHandler].
+ * If there were any, then the first one is thrown, whereas the rest are suppressed by it.
+ * * It runs the tasks pending in the scheduler at the current time. If there are any uncompleted tasks afterwards,
+ * it fails with [UncompletedCoroutinesError].
+ * * It checks whether some new child [Job]s were created but not completed since this [TestCoroutineScope] was
+ * created. If so, it fails with [UncompletedCoroutinesError].
+ *
+ * For backward compatibility, if the [CoroutineExceptionHandler] is an [UncaughtExceptionCaptor], its
+ * [TestCoroutineExceptionHandler.cleanupTestCoroutines] behavior is performed.
+ * Likewise, if the [ContinuationInterceptor] is a [DelayController], its [DelayController.cleanupTestCoroutines]
+ * is called.
+ *
+ * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
+ * @throws AssertionError if any pending tasks are active.
+ * @throws IllegalStateException if called more than once.
+ */
+ @ExperimentalCoroutinesApi
+ @Deprecated("Please call `runTest`, which automatically performs the cleanup, instead of using this function.")
+ // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+ public fun cleanupTestCoroutines()
+
+ /**
+ * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
+ */
+ @ExperimentalCoroutinesApi
+ public val testScheduler: TestCoroutineScheduler
+}
+
+private class TestCoroutineScopeImpl(
+ override val coroutineContext: CoroutineContext
+) : TestCoroutineScope {
+ private val lock = SynchronizedObject()
+ private var exceptions = mutableListOf<Throwable>()
+ private var cleanedUp = false
+
+ /**
+ * Reports an exception so that it is thrown on [cleanupTestCoroutines].
+ *
+ * If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by
+ * it.
+ *
+ * Returns `false` if [cleanupTestCoroutines] was already called.
+ */
+ fun reportException(throwable: Throwable): Boolean =
+ synchronized(lock) {
+ if (cleanedUp) {
+ false
+ } else {
+ exceptions.add(throwable)
+ true
+ }
+ }
+
+ override val testScheduler: TestCoroutineScheduler
+ get() = coroutineContext[TestCoroutineScheduler]!!
+
+ /** These jobs existed before the coroutine scope was used, so it's alright if they don't get cancelled. */
+ private val initialJobs = coroutineContext.activeJobs()
+
+ override fun cleanupTestCoroutines() {
+ val delayController = coroutineContext.delayController
+ val hasUnfinishedJobs = if (delayController != null) {
+ try {
+ delayController.cleanupTestCoroutines()
+ false
+ } catch (e: UncompletedCoroutinesError) {
+ true
+ }
+ } else {
+ testScheduler.runCurrent()
+ !testScheduler.isIdle(strict = false)
+ }
+ (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.cleanupTestCoroutines()
+ synchronized(lock) {
+ if (cleanedUp)
+ throw IllegalStateException("Attempting to clean up a test coroutine scope more than once.")
+ cleanedUp = true
+ }
+ exceptions.firstOrNull()?.let { toThrow ->
+ exceptions.drop(1).forEach { toThrow.addSuppressed(it) }
+ throw toThrow
+ }
+ if (hasUnfinishedJobs)
+ throw UncompletedCoroutinesError(
+ "Unfinished coroutines during teardown. Ensure all coroutines are" +
+ " completed or cancelled by your test."
+ )
+ val jobs = coroutineContext.activeJobs()
+ if ((jobs - initialJobs).isNotEmpty())
+ throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
+ }
+}
+
+internal fun CoroutineContext.activeJobs(): Set<Job> {
+ return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
+}
+
+/**
+ * A coroutine scope for launching test coroutines using [TestCoroutineDispatcher].
+ *
+ * [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher].
+ */
+@Deprecated(
+ "This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
+ "Please use `createTestCoroutineScope` instead.",
+ ReplaceWith(
+ "createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)",
+ "kotlin.coroutines.EmptyCoroutineContext"
+ ),
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
+ val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler()
+ return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + TestCoroutineExceptionHandler() + context)
+}
+
+/**
+ * A coroutine scope for launching test coroutines.
+ *
+ * It ensures that all the test module machinery is properly initialized.
+ * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
+ * a new one is created, unless either
+ * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
+ * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
+ * its [TestCoroutineScheduler] is used.
+ * * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created.
+ * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
+ * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
+ * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
+ * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
+ * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
+ * [TestCoroutineScope] and share your use case at
+ * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
+ * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
+ *
+ * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
+ * different scheduler.
+ * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
+ * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
+ * [UncaughtExceptionCaptor].
+ */
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "This function was introduced in order to help migrate from TestCoroutineScope to TestScope. " +
+ "Please use TestScope() construction instead, or just runTest(), without creating a scope.",
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
+ val ctxWithDispatcher = context.withDelaySkipping()
+ var scope: TestCoroutineScopeImpl? = null
+ val ownExceptionHandler =
+ object : AbstractCoroutineContextElement(CoroutineExceptionHandler), TestCoroutineScopeExceptionHandler {
+ override fun handleException(context: CoroutineContext, exception: Throwable) {
+ if (!scope!!.reportException(exception))
+ throw exception // let this exception crash everything
+ }
+ }
+ val exceptionHandler = when (val exceptionHandler = ctxWithDispatcher[CoroutineExceptionHandler]) {
+ is UncaughtExceptionCaptor -> exceptionHandler
+ null -> ownExceptionHandler
+ is TestCoroutineScopeExceptionHandler -> ownExceptionHandler
+ else -> throw IllegalArgumentException(
+ "A CoroutineExceptionHandler was passed to TestCoroutineScope. " +
+ "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
+ "if uncaught exceptions require special treatment."
+ )
+ }
+ val job: Job = ctxWithDispatcher[Job] ?: Job()
+ return TestCoroutineScopeImpl(ctxWithDispatcher + exceptionHandler + job).also {
+ scope = it
+ }
+}
+
+/** A marker that shows that this [CoroutineExceptionHandler] was created for [TestCoroutineScope]. With this,
+ * constructing a new [TestCoroutineScope] with the [CoroutineScope.coroutineContext] of an existing one will override
+ * the exception handler, instead of failing. */
+private interface TestCoroutineScopeExceptionHandler : CoroutineExceptionHandler
+
+private inline val CoroutineContext.delayController: DelayController?
+ get() {
+ val handler = this[ContinuationInterceptor]
+ return handler as? DelayController
+ }
+
+
+/**
+ * The current virtual time on [testScheduler][TestCoroutineScope.testScheduler].
+ * @see TestCoroutineScheduler.currentTime
+ */
+@ExperimentalCoroutinesApi
+public val TestCoroutineScope.currentTime: Long
+ get() = coroutineContext.delayController?.currentTime ?: testScheduler.currentTime
+
+/**
+ * Advances the [testScheduler][TestCoroutineScope.testScheduler] by [delayTimeMillis] and runs the tasks up to that
+ * moment (inclusive).
+ *
+ * @see TestCoroutineScheduler.advanceTimeBy
+ */
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The name of this function is misleading: it not only advances the time, but also runs the tasks " +
+ "scheduled *at* the ending moment.",
+ ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
+ DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit =
+ when (val controller = coroutineContext.delayController) {
+ null -> {
+ testScheduler.advanceTimeBy(delayTimeMillis)
+ testScheduler.runCurrent()
+ }
+ else -> {
+ controller.advanceTimeBy(delayTimeMillis)
+ Unit
+ }
+ }
+
+/**
+ * Advances the [testScheduler][TestCoroutineScope.testScheduler] to the point where there are no tasks remaining.
+ * @see TestCoroutineScheduler.advanceUntilIdle
+ */
+@ExperimentalCoroutinesApi
+public fun TestCoroutineScope.advanceUntilIdle() {
+ coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle()
+}
+
+/**
+ * Run any tasks that are pending at the current virtual time, according to
+ * the [testScheduler][TestCoroutineScope.testScheduler].
+ *
+ * @see TestCoroutineScheduler.runCurrent
+ */
+@ExperimentalCoroutinesApi
+public fun TestCoroutineScope.runCurrent() {
+ coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent()
+}
+
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
+ "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
+ "\"paused\", like `StandardTestDispatcher`.",
+ ReplaceWith(
+ "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
+ "kotlin.coroutines.ContinuationInterceptor"
+ ),
+ DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) {
+ delayControllerForPausing.pauseDispatcher(block)
+}
+
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
+ "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
+ "\"paused\", like `StandardTestDispatcher`.",
+ ReplaceWith(
+ "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
+ "kotlin.coroutines.ContinuationInterceptor"
+ ),
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.pauseDispatcher() {
+ delayControllerForPausing.pauseDispatcher()
+}
+
+@ExperimentalCoroutinesApi
+@Deprecated(
+ "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
+ "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
+ "\"paused\", like `StandardTestDispatcher`.",
+ ReplaceWith(
+ "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
+ "kotlin.coroutines.ContinuationInterceptor"
+ ),
+ level = DeprecationLevel.WARNING
+)
+// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
+public fun TestCoroutineScope.resumeDispatcher() {
+ delayControllerForPausing.resumeDispatcher()
+}
+
+/**
+ * List of uncaught coroutine exceptions, for backward compatibility.
+ *
+ * The returned list is a copy of the exceptions caught during execution.
+ * During [TestCoroutineScope.cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
+ *
+ * Exceptions are only collected in this list if the [UncaughtExceptionCaptor] is in the test context.
+ */
+@Deprecated(
+ "This list is only populated if `UncaughtExceptionCaptor` is in the test context, and so can be " +
+ "easily misused. It is only present for backward compatibility and will be removed in the subsequent " +
+ "releases. If you need to check the list of exceptions, please consider creating your own " +
+ "`CoroutineExceptionHandler`.",
+ level = DeprecationLevel.WARNING
+)
+public val TestCoroutineScope.uncaughtExceptions: List<Throwable>
+ get() = (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.uncaughtExceptions
+ ?: emptyList()
+
+private val TestCoroutineScope.delayControllerForPausing: DelayController
+ get() = coroutineContext.delayController
+ ?: throw IllegalStateException("This scope isn't able to pause its dispatchers")
diff --git a/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt b/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt
new file mode 100644
index 0000000..e9aa3ff
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/HelpersJvm.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult) {
+ block {
+ test()
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt
index d06f2a3..90a16d0 100644
--- a/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt
@@ -4,13 +4,14 @@
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
+import kotlin.concurrent.*
import kotlin.coroutines.*
import kotlin.test.*
-class MultithreadingTest : TestBase() {
+class MultithreadingTest {
@Test
- fun incorrectlyCalledRunblocking_doesNotHaveSameInterceptor() = runBlockingTest {
+ fun incorrectlyCalledRunBlocking_doesNotHaveSameInterceptor() = runBlockingTest {
// this code is an error as a production test, please do not use this as an example
// this test exists to document this error condition, if it's possible to make this code work please update
@@ -22,7 +23,7 @@
}
@Test
- fun testSingleThreadExecutor() = runTest {
+ fun testSingleThreadExecutor() = runBlocking {
val mainThread = Thread.currentThread()
Dispatchers.setMain(Dispatchers.Unconfined)
newSingleThreadContext("testSingleThread").use { threadPool ->
@@ -86,4 +87,26 @@
assertEquals(3, deferred.await())
}
}
-}
\ No newline at end of file
+
+ /** Tests that resuming the coroutine of [runTest] asynchronously in reasonable time succeeds. */
+ @Test
+ fun testResumingFromAnotherThread() = runTest {
+ suspendCancellableCoroutine<Unit> { cont ->
+ thread {
+ Thread.sleep(10)
+ cont.resume(Unit)
+ }
+ }
+ }
+
+ /** Tests that [StandardTestDispatcher] is confined to the thread that interacts with the scheduler. */
+ @Test
+ fun testStandardTestDispatcherIsConfined() = runTest {
+ val initialThread = Thread.currentThread()
+ withContext(Dispatchers.IO) {
+ val ioThread = Thread.currentThread()
+ assertNotSame(initialThread, ioThread)
+ }
+ assertEquals(initialThread, Thread.currentThread())
+ }
+}
diff --git a/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt
new file mode 100644
index 0000000..3edaa48
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.concurrent.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class RunTestStressTest {
+ /** Tests that notifications about asynchronous resumptions aren't lost. */
+ @Test
+ fun testRunTestActivityNotificationsRace() {
+ val n = 1_000 * stressTestMultiplier
+ for (i in 0 until n) {
+ runTest {
+ suspendCancellableCoroutine<Unit> { cont ->
+ thread {
+ cont.resume(Unit)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt
new file mode 100644
index 0000000..174baa0
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlin.test.*
+
+/** Copy of [RunTestTest], but for [runBlockingTestOnTestScope], where applicable. */
+@Suppress("DEPRECATION")
+class RunBlockingTestOnTestScopeTest {
+
+ @Test
+ fun testRunTestWithIllegalContext() {
+ for (ctx in TestScopeTest.invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ runBlockingTestOnTestScope(ctx) { }
+ }
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestBody() {
+ assertFailsWith<RuntimeException> {
+ runBlockingTestOnTestScope {
+ throw RuntimeException()
+ }
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestPendingTask() {
+ assertFailsWith<RuntimeException> {
+ runBlockingTestOnTestScope {
+ launch {
+ delay(SLOW)
+ throw RuntimeException()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun reproducer2405() = runBlockingTestOnTestScope {
+ val dispatcher = StandardTestDispatcher(testScheduler)
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ @Test
+ fun testChildrenCancellationOnTestBodyFailure() {
+ var job: Job? = null
+ assertFailsWith<AssertionError> {
+ runBlockingTestOnTestScope {
+ job = launch {
+ while (true) {
+ delay(1000)
+ }
+ }
+ throw AssertionError()
+ }
+ }
+ assertTrue(job!!.isCancelled)
+ }
+
+ @Test
+ fun testTimeout() {
+ assertFailsWith<TimeoutCancellationException> {
+ runBlockingTestOnTestScope {
+ withTimeout(50) {
+ launch {
+ delay(1000)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testRunTestThrowsRootCause() {
+ assertFailsWith<TestException> {
+ runBlockingTestOnTestScope {
+ launch {
+ throw TestException()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testCompletesOwnJob() {
+ var handlerCalled = false
+ runBlockingTestOnTestScope {
+ coroutineContext.job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ }
+ assertTrue(handlerCalled)
+ }
+
+ @Test
+ fun testDoesNotCompleteGivenJob() {
+ var handlerCalled = false
+ val job = Job()
+ job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ runBlockingTestOnTestScope(job) {
+ assertTrue(coroutineContext.job in job.children)
+ }
+ assertFalse(handlerCalled)
+ assertEquals(0, job.children.filter { it.isActive }.count())
+ }
+
+ @Test
+ fun testSuppressedExceptions() {
+ try {
+ runBlockingTestOnTestScope {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ throw TestException("w")
+ }
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("w", e.message)
+ val suppressed = e.suppressedExceptions +
+ (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
+ assertEquals(3, suppressed.size)
+ assertEquals("x", suppressed[0].message)
+ assertEquals("y", suppressed[1].message)
+ assertEquals("z", suppressed[2].message)
+ }
+ }
+
+ @Test
+ fun testScopeRunTestExceptionHandler(): TestResult {
+ val scope = TestCoroutineScope()
+ return testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
new file mode 100644
index 0000000..a76263d
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+/** Copy of [RunTestTest], but for [TestCoroutineScope] */
+@Suppress("DEPRECATION")
+class RunTestLegacyScopeTest {
+
+ @Test
+ fun testWithContextDispatching() = runTestWithLegacyScope {
+ var counter = 0
+ withContext(Dispatchers.Default) {
+ counter += 1
+ }
+ assertEquals(counter, 1)
+ }
+
+ @Test
+ fun testJoiningForkedJob() = runTestWithLegacyScope {
+ var counter = 0
+ val job = GlobalScope.launch {
+ counter += 1
+ }
+ job.join()
+ assertEquals(counter, 1)
+ }
+
+ @Test
+ fun testSuspendCoroutine() = runTestWithLegacyScope {
+ val answer = suspendCoroutine<Int> {
+ it.resume(42)
+ }
+ assertEquals(42, answer)
+ }
+
+ @Test
+ fun testNestedRunTestForbidden() = runTestWithLegacyScope {
+ assertFailsWith<IllegalStateException> {
+ runTest { }
+ }
+ }
+
+ @Test
+ fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTestWithLegacyScope(dispatchTimeoutMs = 0) {
+ // below is some arbitrary concurrent code where all dispatches go through the same scheduler.
+ launch {
+ delay(2000)
+ }
+ val deferred = async {
+ val job = launch(StandardTestDispatcher(testScheduler)) {
+ launch {
+ delay(500)
+ }
+ delay(1000)
+ }
+ job.join()
+ }
+ deferred.await()
+ }
+
+ @Test
+ fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTestWithLegacyScope(dispatchTimeoutMs = 0) {
+ withContext(Dispatchers.Default) {
+ delay(10)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ @Test
+ fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
+ assertFailsWith<UncompletedCoroutinesError> { fn() }
+ }) {
+ runTestWithLegacyScope(dispatchTimeoutMs = 100) {
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ @Test
+ fun testRunTestWithLargeTimeout() = runTestWithLegacyScope(dispatchTimeoutMs = 5000) {
+ withContext(Dispatchers.Default) {
+ delay(50)
+ }
+ }
+
+ @Test
+ fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
+ assertFailsWith<IllegalArgumentException> { fn() }
+ }) {
+ runTestWithLegacyScope(dispatchTimeoutMs = 1) {
+ coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, IllegalArgumentException())
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ 3
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+ @Test
+ fun testRunTestWithIllegalContext() {
+ for (ctx in TestScopeTest.invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ runTestWithLegacyScope(ctx) { }
+ }
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestBody() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ throw RuntimeException()
+ }
+ }
+
+ @Test
+ fun testThrowingInRunTestPendingTask() = testResultMap({
+ assertFailsWith<RuntimeException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ launch {
+ delay(SLOW)
+ throw RuntimeException()
+ }
+ }
+ }
+
+ @Test
+ fun reproducer2405() = runTestWithLegacyScope {
+ val dispatcher = StandardTestDispatcher(testScheduler)
+ var collectedError = false
+ withContext(dispatcher) {
+ flow { emit(1) }
+ .combine(
+ flow<String> { throw IllegalArgumentException() }
+ ) { int, string -> int.toString() + string }
+ .catch { emit("error") }
+ .collect {
+ assertEquals("error", it)
+ collectedError = true
+ }
+ }
+ assertTrue(collectedError)
+ }
+
+ @Test
+ fun testChildrenCancellationOnTestBodyFailure(): TestResult {
+ var job: Job? = null
+ return testResultMap({
+ assertFailsWith<AssertionError> { it() }
+ assertTrue(job!!.isCancelled)
+ }) {
+ runTestWithLegacyScope {
+ job = launch {
+ while (true) {
+ delay(1000)
+ }
+ }
+ throw AssertionError()
+ }
+ }
+ }
+
+ @Test
+ fun testTimeout() = testResultMap({
+ assertFailsWith<TimeoutCancellationException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ withTimeout(50) {
+ launch {
+ delay(1000)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testRunTestThrowsRootCause() = testResultMap({
+ assertFailsWith<TestException> { it() }
+ }) {
+ runTestWithLegacyScope {
+ launch {
+ throw TestException()
+ }
+ }
+ }
+
+ @Test
+ fun testCompletesOwnJob(): TestResult {
+ var handlerCalled = false
+ return testResultMap({
+ it()
+ assertTrue(handlerCalled)
+ }) {
+ runTestWithLegacyScope {
+ coroutineContext.job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testDoesNotCompleteGivenJob(): TestResult {
+ var handlerCalled = false
+ val job = Job()
+ job.invokeOnCompletion {
+ handlerCalled = true
+ }
+ return testResultMap({
+ it()
+ assertFalse(handlerCalled)
+ assertEquals(0, job.children.filter { it.isActive }.count())
+ }) {
+ runTestWithLegacyScope(job) {
+ assertTrue(coroutineContext.job in job.children)
+ }
+ }
+ }
+
+ @Test
+ fun testSuppressedExceptions() = testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("w", e.message)
+ val suppressed = e.suppressedExceptions +
+ (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
+ assertEquals(3, suppressed.size)
+ assertEquals("x", suppressed[0].message)
+ assertEquals("y", suppressed[1].message)
+ assertEquals("z", suppressed[2].message)
+ }
+ }) {
+ runTestWithLegacyScope {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ throw TestException("w")
+ }
+ }
+
+ @Test
+ fun testScopeRunTestExceptionHandler(): TestResult {
+ val scope = TestCoroutineScope()
+ return testResultMap({
+ try {
+ it()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ // expected
+ }
+ }) {
+ scope.runTest {
+ launch(SupervisorJob()) { throw TestException("x") }
+ }
+ }
+ }
+}
diff --git a/kotlinx-coroutines-test/common/test/TestBuildersTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt
similarity index 97%
rename from kotlinx-coroutines-test/common/test/TestBuildersTest.kt
rename to kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt
index a3167e5..6d49a01 100644
--- a/kotlinx-coroutines-test/common/test/TestBuildersTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt
@@ -8,6 +8,7 @@
import kotlin.coroutines.*
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestBuildersTest {
@Test
@@ -104,7 +105,7 @@
}
@Test
- fun whenInrunBlocking_runBlockingTest_nestsProperly() {
+ fun whenInRunBlocking_runBlockingTest_nestsProperly() {
// this is not a supported use case, but it is possible so ensure it works
val scope = TestCoroutineScope()
diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt
similarity index 70%
rename from kotlinx-coroutines-test/common/test/TestCoroutineDispatcherOrderTest.kt
rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt
index e54ba21..93fcd90 100644
--- a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherOrderTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt
@@ -8,20 +8,8 @@
import kotlinx.coroutines.*
import kotlin.test.*
-class TestCoroutineDispatcherOrderTest {
-
- private val actionIndex = atomic(0)
- private val finished = atomic(false)
-
- private fun expect(index: Int) {
- val wasIndex = actionIndex.incrementAndGet()
- check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
- }
-
- private fun finish(index: Int) {
- expect(index)
- check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
- }
+@Suppress("DEPRECATION")
+class TestCoroutineDispatcherOrderTest: OrderedExecutionTestBase() {
@Test
fun testAdvanceTimeBy_progressesOnEachDelay() {
diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt
similarity index 63%
rename from kotlinx-coroutines-test/common/test/TestCoroutineDispatcherTest.kt
rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt
index baf946f..a78d923 100644
--- a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt
@@ -7,37 +7,10 @@
import kotlinx.coroutines.*
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestCoroutineDispatcherTest {
@Test
- fun whenStringCalled_itReturnsString() {
- val subject = TestCoroutineDispatcher()
- assertEquals("TestCoroutineDispatcher[currentTime=0ms, queued=0]", subject.toString())
- }
-
- @Test
- fun whenStringCalled_itReturnsCurrentTime() {
- val subject = TestCoroutineDispatcher()
- subject.advanceTimeBy(1000)
- assertEquals("TestCoroutineDispatcher[currentTime=1000ms, queued=0]", subject.toString())
- }
-
- @Test
- fun whenStringCalled_itShowsQueuedJobs() {
- val subject = TestCoroutineDispatcher()
- val scope = TestCoroutineScope(subject)
- scope.pauseDispatcher()
- scope.launch {
- delay(1_000)
- }
- assertEquals("TestCoroutineDispatcher[currentTime=0ms, queued=1]", subject.toString())
- scope.advanceTimeBy(50)
- assertEquals("TestCoroutineDispatcher[currentTime=50ms, queued=1]", subject.toString())
- scope.advanceUntilIdle()
- assertEquals("TestCoroutineDispatcher[currentTime=1000ms, queued=0]", subject.toString())
- }
-
- @Test
- fun whenDispatcherPaused_doesntAutoProgressCurrent() {
+ fun whenDispatcherPaused_doesNotAutoProgressCurrent() {
val subject = TestCoroutineDispatcher()
subject.pauseDispatcher()
val scope = CoroutineScope(subject)
diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineExceptionHandlerTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt
similarity index 82%
rename from kotlinx-coroutines-test/common/test/TestCoroutineExceptionHandlerTest.kt
rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt
index 674fd28..20da130 100644
--- a/kotlinx-coroutines-test/common/test/TestCoroutineExceptionHandlerTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.test
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestCoroutineExceptionHandlerTest {
@Test
fun whenExceptionsCaught_availableViaProperty() {
diff --git a/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt
new file mode 100644
index 0000000..1a62613
--- /dev/null
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("DEPRECATION")
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.*
+import kotlin.test.*
+
+class TestCoroutineScopeTest {
+ /** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */
+ @Test
+ fun testCreateThrowsOnInvalidArguments() {
+ for (ctx in invalidContexts) {
+ assertFailsWith<IllegalArgumentException> {
+ createTestCoroutineScope(ctx)
+ }
+ }
+ }
+
+ /** Tests that a newly-created [TestCoroutineScope] provides the correct scheduler. */
+ @Test
+ fun testCreateProvidesScheduler() {
+ // Creates a new scheduler.
+ run {
+ val scope = createTestCoroutineScope()
+ assertNotNull(scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Reuses the scheduler that the dispatcher is linked to.
+ run {
+ val dispatcher = StandardTestDispatcher()
+ val scope = createTestCoroutineScope(dispatcher)
+ assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ }
+ // Uses the scheduler passed to it.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val scope = createTestCoroutineScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler)
+ }
+ // Doesn't touch the passed dispatcher and the scheduler if they match.
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val scope = createTestCoroutineScope(scheduler + dispatcher)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor])
+ }
+ // Reuses the scheduler of `Dispatchers.Main`
+ run {
+ val scheduler = TestCoroutineScheduler()
+ val mainDispatcher = StandardTestDispatcher(scheduler)
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scope = createTestCoroutineScope()
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed
+ run {
+ val mainDispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(mainDispatcher)
+ try {
+ val scheduler = TestCoroutineScheduler()
+ val scope = createTestCoroutineScope(scheduler)
+ assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
+ assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
+ } finally {
+ Dispatchers.resetMain()
+ }
+ }
+ }
+
+ /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */
+ @Test
+ fun testPresentDelaysThrowing() {
+ val scope = createTestCoroutineScope()
+ var result = false
+ scope.launch {
+ delay(5)
+ result = true
+ }
+ assertFalse(result)
+ assertFailsWith<AssertionError> { scope.cleanupTestCoroutines() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure throws if there were active jobs by the end. */
+ @Test
+ fun testActiveJobsThrowing() {
+ val scope = createTestCoroutineScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ scope.launch {
+ deferred.await()
+ result = true
+ }
+ assertFalse(result)
+ assertFailsWith<AssertionError> { scope.cleanupTestCoroutines() }
+ assertFalse(result)
+ }
+
+ /** Tests that the cleanup procedure doesn't throw if it detects that the job is already cancelled. */
+ @Test
+ fun testCancelledDelaysNotThrowing() {
+ val scope = createTestCoroutineScope()
+ var result = false
+ val deferred = CompletableDeferred<String>()
+ val job = scope.launch {
+ deferred.await()
+ result = true
+ }
+ job.cancel()
+ assertFalse(result)
+ scope.cleanupTestCoroutines()
+ assertFalse(result)
+ }
+
+ /** Tests that uncaught exceptions are thrown at the cleanup. */
+ @Test
+ fun testThrowsUncaughtExceptionsOnCleanup() {
+ val scope = createTestCoroutineScope()
+ val exception = TestException("test")
+ scope.launch {
+ throw exception
+ }
+ assertFailsWith<TestException> {
+ scope.cleanupTestCoroutines()
+ }
+ }
+
+ /** Tests that uncaught exceptions take priority over uncompleted jobs when throwing on cleanup. */
+ @Test
+ fun testUncaughtExceptionsPrioritizedOnCleanup() {
+ val scope = createTestCoroutineScope()
+ val exception = TestException("test")
+ scope.launch {
+ throw exception
+ }
+ scope.launch {
+ delay(1000)
+ }
+ assertFailsWith<TestException> {
+ scope.cleanupTestCoroutines()
+ }
+ }
+
+ /** Tests that cleaning up twice is forbidden. */
+ @Test
+ fun testClosingTwice() {
+ val scope = createTestCoroutineScope()
+ scope.cleanupTestCoroutines()
+ assertFailsWith<IllegalStateException> {
+ scope.cleanupTestCoroutines()
+ }
+ }
+
+ /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */
+ @Test
+ fun testSuppressedExceptions() {
+ createTestCoroutineScope().apply {
+ launch(SupervisorJob()) { throw TestException("x") }
+ launch(SupervisorJob()) { throw TestException("y") }
+ launch(SupervisorJob()) { throw TestException("z") }
+ try {
+ cleanupTestCoroutines()
+ fail("should not be reached")
+ } catch (e: TestException) {
+ assertEquals("x", e.message)
+ assertEquals(2, e.suppressedExceptions.size)
+ assertEquals("y", e.suppressedExceptions[0].message)
+ assertEquals("z", e.suppressedExceptions[1].message)
+ }
+ }
+ }
+
+ /** Tests that constructing a new [TestCoroutineScope] using another one's scope works and overrides the exception
+ * handler. */
+ @Test
+ fun testCopyingContexts() {
+ val deferred = CompletableDeferred<Unit>()
+ val scope1 = createTestCoroutineScope()
+ scope1.launch { deferred.await() } // a pending job in the outer scope
+ val scope2 = createTestCoroutineScope(scope1.coroutineContext)
+ val scope3 = createTestCoroutineScope(scope1.coroutineContext)
+ assertEquals(
+ scope1.coroutineContext.minusKey(CoroutineExceptionHandler),
+ scope2.coroutineContext.minusKey(CoroutineExceptionHandler))
+ scope2.launch(SupervisorJob()) { throw TestException("x") } // will fail the cleanup of scope2
+ try {
+ scope2.cleanupTestCoroutines()
+ fail("should not be reached")
+ } catch (e: TestException) { }
+ scope3.cleanupTestCoroutines() // the pending job in the outer scope will not cause this to fail
+ try {
+ scope1.cleanupTestCoroutines()
+ fail("should not be reached")
+ } catch (e: UncompletedCoroutinesError) {
+ // the pending job in the outer scope
+ }
+ }
+
+ companion object {
+ internal val invalidContexts = listOf(
+ Dispatchers.Default, // not a [TestDispatcher]
+ CoroutineExceptionHandler { _, _ -> }, // not an [UncaughtExceptionCaptor]
+ StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
+ )
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/common/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt
similarity index 76%
rename from kotlinx-coroutines-test/common/test/TestRunBlockingOrderTest.kt
rename to kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt
index 5d94bd2..32514d9 100644
--- a/kotlinx-coroutines-test/common/test/TestRunBlockingOrderTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt
@@ -4,24 +4,11 @@
package kotlinx.coroutines.test
-import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlin.test.*
-class TestRunBlockingOrderTest {
-
- private val actionIndex = atomic(0)
- private val finished = atomic(false)
-
- private fun expect(index: Int) {
- val wasIndex = actionIndex.incrementAndGet()
- check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
- }
-
- private fun finish(index: Int) {
- expect(index)
- check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
- }
+@Suppress("DEPRECATION")
+class TestRunBlockingOrderTest: OrderedExecutionTestBase() {
@Test
fun testLaunchImmediate() = runBlockingTest {
@@ -90,4 +77,4 @@
}
finish(2)
}
-}
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/common/test/TestRunBlockingTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt
similarity index 97%
rename from kotlinx-coroutines-test/common/test/TestRunBlockingTest.kt
rename to kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt
index c93b508..af3b248 100644
--- a/kotlinx-coroutines-test/common/test/TestRunBlockingTest.kt
+++ b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt
@@ -7,6 +7,7 @@
import kotlinx.coroutines.*
import kotlin.test.*
+@Suppress("DEPRECATION")
class TestRunBlockingTest {
@Test
@@ -128,7 +129,6 @@
@Test
fun whenUsingTimeout_inAsync_doesNotTriggerWhenNotDelayed() = runBlockingTest {
- val testScope = this
val deferred = async {
withTimeout(SLOW) {
delay(0)
@@ -195,7 +195,7 @@
assertRunsFast {
job.join()
- throw job.getCancellationException().cause ?: assertFails { "expected exception" }
+ throw job.getCancellationException().cause ?: AssertionError("expected exception")
}
}
}
@@ -235,12 +235,13 @@
fun callingAsyncFunction_executesAsyncBlockImmediately() = runBlockingTest {
assertRunsFast {
var executed = false
- async {
+ val deferred = async {
delay(SLOW)
executed = true
}
advanceTimeBy(SLOW)
+ assertTrue(deferred.isCompleted)
assertTrue(executed)
}
}
@@ -366,7 +367,7 @@
runBlockingTest {
val expectedError = TestException("hello")
- val job = launch {
+ launch {
throw expectedError
}
@@ -436,6 +437,4 @@
}
}
}
-}
-
-private class TestException(message: String? = null): Exception(message)
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/native/src/TestBuilders.kt b/kotlinx-coroutines-test/native/src/TestBuilders.kt
new file mode 100644
index 0000000..c3176a0
--- /dev/null
+++ b/kotlinx-coroutines-test/native/src/TestBuilders.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+import kotlinx.coroutines.*
+
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+public actual typealias TestResult = Unit
+
+internal actual fun createTestResult(testProcedure: suspend () -> Unit) {
+ runBlocking {
+ testProcedure()
+ }
+}
diff --git a/kotlinx-coroutines-test/native/src/TestMainDispatcher.kt b/kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
similarity index 100%
rename from kotlinx-coroutines-test/native/src/TestMainDispatcher.kt
rename to kotlinx-coroutines-test/native/src/internal/TestMainDispatcher.kt
diff --git a/kotlinx-coroutines-test/native/test/FailingTests.kt b/kotlinx-coroutines-test/native/test/FailingTests.kt
new file mode 100644
index 0000000..9fb77ce
--- /dev/null
+++ b/kotlinx-coroutines-test/native/test/FailingTests.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.test
+
+import kotlinx.coroutines.*
+import kotlin.test.*
+
+/** These are tests that we want to fail. They are here so that, when the issue is fixed, their failure indicates that
+ * everything is better now. */
+class FailingTests {
+ @Test
+ fun testRunTestLoopShutdownOnTimeout() = testResultMap({ fn ->
+ assertFailsWith<IllegalStateException> { fn() }
+ }) {
+ runTest(dispatchTimeoutMs = 1) {
+ withContext(Dispatchers.Default) {
+ delay(10000)
+ }
+ fail("shouldn't be reached")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-test/native/test/Helpers.kt b/kotlinx-coroutines-test/native/test/Helpers.kt
index 28cc28c..ef478b7 100644
--- a/kotlinx-coroutines-test/native/test/Helpers.kt
+++ b/kotlinx-coroutines-test/native/test/Helpers.kt
@@ -5,4 +5,10 @@
import kotlin.test.*
+actual fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult) {
+ block {
+ test()
+ }
+}
+
actual typealias NoNative = Ignore