• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 @file:JvmName("TestBuildersKt")
5 @file:JvmMultifileClass
6 
7 package kotlinx.coroutines.test
8 
9 import kotlinx.coroutines.*
10 import kotlinx.coroutines.selects.*
11 import kotlin.coroutines.*
12 import kotlin.jvm.*
13 
14 /**
15  * A test result.
16  *
17  * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
18  *   platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
19  * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
20  *   finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
21  *
22  * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
23  * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
24  *   test finishes.
25  * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
26  *   with a [TestResult] is to immediately `return` it from a test.
27  * * Don't nest functions returning a [TestResult].
28  */
29 @Suppress("NO_ACTUAL_FOR_EXPECT")
30 @ExperimentalCoroutinesApi
31 public expect class TestResult
32 
33 /**
34  * Executes [testBody] as a test in a new coroutine, returning [TestResult].
35  *
36  * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
37  * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
38  * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
39  *
40  * ```
41  * @Test
42  * fun exampleTest() = runTest {
43  *     val deferred = async {
44  *         delay(1_000)
45  *         async {
46  *             delay(1_000)
47  *         }.await()
48  *     }
49  *
50  *     deferred.await() // result available immediately
51  * }
52  * ```
53  *
54  * The platform difference entails that, in order to use this function correctly in common code, one must always
55  * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
56  * [TestResult] for details on this.
57  *
58  * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
59  * Because of this, child coroutines are not executed in parallel to the test body.
60  * In order to for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
61  * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
62  *
63  * ```
64  * @Test
65  * fun exampleWaitingForAsyncTasks1() = runTest {
66  *     // 1
67  *     val job = launch {
68  *         // 3
69  *     }
70  *     // 2
71  *     job.join() // the main test coroutine suspends here, so the child is executed
72  *     // 4
73  * }
74  *
75  * @Test
76  * fun exampleWaitingForAsyncTasks2() = runTest {
77  *     // 1
78  *     launch {
79  *         // 3
80  *     }
81  *     // 2
82  *     advanceUntilIdle() // runs the tasks until their queue is empty
83  *     // 4
84  * }
85  * ```
86  *
87  * ### Task scheduling
88  *
89  * Delay-skipping is achieved by using virtual time.
90  * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
91  * then its [TestCoroutineScheduler] is used;
92  * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
93  * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
94  * Some convenience methods are available on [TestScope] to control the scheduler.
95  *
96  * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
97  * ```
98  * @Test
99  * fun exampleTest() = runTest {
100  *     val elapsed = TimeSource.Monotonic.measureTime {
101  *         val deferred = async {
102  *             delay(1_000) // will be skipped
103  *             withContext(Dispatchers.Default) {
104  *                 delay(5_000) // Dispatchers.Default doesn't know about TestCoroutineScheduler
105  *             }
106  *         }
107  *         deferred.await()
108  *     }
109  *     println(elapsed) // about five seconds
110  * }
111  * ```
112  *
113  * ### Failures
114  *
115  * #### Test body failures
116  *
117  * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
118  *
119  * #### Reported exceptions
120  *
121  * Unhandled exceptions will be thrown at the end of the test.
122  * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
123  * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
124  *
125  * #### Uncompleted coroutines
126  *
127  * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
128  * [testBody] also complete, or are cancelled.
129  * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
130  * [AssertionError], whereas on JS, the `Promise` will fail with it).
131  *
132  * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
133  * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
134  * for [dispatchTimeoutMs] milliseconds (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes
135  * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
136  * task during that time, the timer gets reset.
137  *
138  * ### Configuration
139  *
140  * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
141  * scope created for the test, [context] also can be used to change how the test is executed.
142  * See the [TestScope] constructor function documentation for details.
143  *
144  * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
145  */
146 @ExperimentalCoroutinesApi
147 public fun runTest(
148     context: CoroutineContext = EmptyCoroutineContext,
149     dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
150     testBody: suspend TestScope.() -> Unit
151 ): TestResult {
152     if (context[RunningInRunTest] != null)
153         throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
154     return TestScope(context + RunningInRunTest).runTest(dispatchTimeoutMs, testBody)
155 }
156 
157 /**
158  * Performs [runTest] on an existing [TestScope].
159  */
160 @ExperimentalCoroutinesApi
runTestnull161 public fun TestScope.runTest(
162     dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
163     testBody: suspend TestScope.() -> Unit
164 ): TestResult = asSpecificImplementation().let {
165     it.enter()
166     createTestResult {
167         runTestCoroutine(it, dispatchTimeoutMs, TestScopeImpl::tryGetCompletionCause, testBody) {
168             backgroundScope.cancel()
169             testScheduler.advanceUntilIdleOr { false }
170             it.leave()
171         }
172     }
173 }
174 
175 /**
176  * Runs [testProcedure], creating a [TestResult].
177  */
178 @Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult`
createTestResultnull179 internal expect fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult
180 
181 /** A coroutine context element indicating that the coroutine is running inside `runTest`. */
182 internal object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
183     override val key: CoroutineContext.Key<*>
184         get() = this
185 
186     override fun toString(): String = "RunningInRunTest"
187 }
188 
189 /** The default timeout to use when waiting for asynchronous completions of the coroutines managed by
190  * a [TestCoroutineScheduler]. */
191 internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
192 
193 /**
194  * Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
195  * [dispatchTimeoutMs] milliseconds, and performing the [cleanup] procedure at the end.
196  *
197  * [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected.
198  *
199  * The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or
200  * return a list of uncaught exceptions that should be reported at the end of the test.
201  */
runTestCoroutinenull202 internal suspend fun <T: AbstractCoroutine<Unit>> CoroutineScope.runTestCoroutine(
203     coroutine: T,
204     dispatchTimeoutMs: Long,
205     tryGetCompletionCause: T.() -> Throwable?,
206     testBody: suspend T.() -> Unit,
207     cleanup: () -> List<Throwable>,
208 ) {
209     val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!!
210     /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
211     coroutine.start(CoroutineStart.UNDISPATCHED, coroutine) {
212         testBody()
213     }
214     /**
215      * The general procedure here is as follows:
216      * 1. Try running the work that the scheduler knows about, both background and foreground.
217      *
218      * 2. Wait until we run out of foreground work to do. This could mean one of the following:
219      *    * The main coroutine is already completed. This is checked separately; then we leave the procedure.
220      *    * It's switched to another dispatcher that doesn't know about the [TestCoroutineScheduler].
221      *    * Generally, it's waiting for something external (like a network request, or just an arbitrary callback).
222      *    * The test simply hanged.
223      *    * The main coroutine is waiting for some background work.
224      *
225      * 3. We await progress from things that are not the code under test:
226      *    the background work that the scheduler knows about, the external callbacks,
227      *    the work on dispatchers not linked to the scheduler, etc.
228      *
229      *    When we observe that the code under test can proceed, we go to step 1 again.
230      *    If there is no activity for [dispatchTimeoutMs] milliseconds, we consider the test to have hanged.
231      *
232      *    The background work is not running on a dedicated thread.
233      *    Instead, the test thread itself is used, by spawning a separate coroutine.
234      */
235     var completed = false
236     while (!completed) {
237         scheduler.advanceUntilIdle()
238         if (coroutine.isCompleted) {
239             /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
240                non-trivial dispatches. */
241             completed = true
242             continue
243         }
244         // in case progress depends on some background work, we need to keep spinning it.
245         val backgroundWorkRunner = launch(CoroutineName("background work runner")) {
246             while (true) {
247                 scheduler.tryRunNextTaskUnless { !isActive }
248                 // yield so that the `select` below has a chance to check if its conditions are fulfilled
249                 yield()
250             }
251         }
252         try {
253             select<Unit> {
254                 coroutine.onJoin {
255                     // observe that someone completed the test coroutine and leave without waiting for the timeout
256                     completed = true
257                 }
258                 scheduler.onDispatchEvent {
259                     // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
260                 }
261                 onTimeout(dispatchTimeoutMs) {
262                     handleTimeout(coroutine, dispatchTimeoutMs, tryGetCompletionCause, cleanup)
263                 }
264             }
265         } finally {
266             backgroundWorkRunner.cancelAndJoin()
267         }
268     }
269     coroutine.getCompletionExceptionOrNull()?.let { exception ->
270         val exceptions = try {
271             cleanup()
272         } catch (e: UncompletedCoroutinesError) {
273             // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
274             emptyList()
275         }
276         (listOf(exception) + exceptions).throwAll()
277     }
278     cleanup().throwAll()
279 }
280 
281 /**
282  * Invoked on timeout in [runTest]. Almost always just builds a nice [UncompletedCoroutinesError] and throws it.
283  * However, sometimes it detects that the coroutine completed, in which case it returns normally.
284  */
handleTimeoutnull285 private inline fun<T: AbstractCoroutine<Unit>> handleTimeout(
286     coroutine: T,
287     dispatchTimeoutMs: Long,
288     tryGetCompletionCause: T.() -> Throwable?,
289     cleanup: () -> List<Throwable>,
290 ) {
291     val uncaughtExceptions = try {
292         cleanup()
293     } catch (e: UncompletedCoroutinesError) {
294         // we expect these and will instead throw a more informative exception.
295         emptyList()
296     }
297     val activeChildren = coroutine.children.filter { it.isActive }.toList()
298     val completionCause = if (coroutine.isCancelled) coroutine.tryGetCompletionCause() else null
299     var message = "After waiting for $dispatchTimeoutMs ms"
300     if (completionCause == null)
301         message += ", the test coroutine is not completing"
302     if (activeChildren.isNotEmpty())
303         message += ", there were active child jobs: $activeChildren"
304     if (completionCause != null && activeChildren.isEmpty()) {
305         if (coroutine.isCompleted)
306             return
307         // TODO: can this really ever happen?
308         message += ", the test coroutine was not completed"
309     }
310     val error = UncompletedCoroutinesError(message)
311     completionCause?.let { cause -> error.addSuppressed(cause) }
312     uncaughtExceptions.forEach { error.addSuppressed(it) }
313     throw error
314 }
315 
throwAllnull316 internal fun List<Throwable>.throwAll() {
317     firstOrNull()?.apply {
318         drop(1).forEach { addSuppressed(it) }
319         throw this
320     }
321 }
322