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