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.flow.*
11 import kotlinx.coroutines.selects.*
12 import kotlin.coroutines.*
13 import kotlin.jvm.*
14 import kotlin.time.*
15 import kotlin.time.Duration.Companion.milliseconds
16 import kotlin.time.Duration.Companion.seconds
17 import kotlinx.coroutines.internal.*
18
19 /**
20 * A test result.
21 *
22 * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
23 * platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
24 * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
25 * finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
26 *
27 * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
28 * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
29 * test finishes.
30 * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
31 * with a [TestResult] is to immediately `return` it from a test.
32 * * Don't nest functions returning a [TestResult].
33 */
34 @Suppress("NO_ACTUAL_FOR_EXPECT")
35 public expect class TestResult
36
37 /**
38 * Executes [testBody] as a test in a new coroutine, returning [TestResult].
39 *
40 * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
41 * will skip delays. This allows to use [delay] in tests without causing them to take more time than necessary.
42 * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
43 *
44 * ```
45 * @Test
46 * fun exampleTest() = runTest {
47 * val deferred = async {
48 * delay(1.seconds)
49 * async {
50 * delay(1.seconds)
51 * }.await()
52 * }
53 *
54 * deferred.await() // result available immediately
55 * }
56 * ```
57 *
58 * The platform difference entails that, in order to use this function correctly in common code, one must always
59 * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
60 * [TestResult] for details on this.
61 *
62 * The test is run on a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
63 * Because of this, child coroutines are not executed in parallel to the test body.
64 * In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
65 * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
66 *
67 * ```
68 * @Test
69 * fun exampleWaitingForAsyncTasks1() = runTest {
70 * // 1
71 * val job = launch {
72 * // 3
73 * }
74 * // 2
75 * job.join() // the main test coroutine suspends here, so the child is executed
76 * // 4
77 * }
78 *
79 * @Test
80 * fun exampleWaitingForAsyncTasks2() = runTest {
81 * // 1
82 * launch {
83 * // 3
84 * }
85 * // 2
86 * testScheduler.advanceUntilIdle() // runs the tasks until their queue is empty
87 * // 4
88 * }
89 * ```
90 *
91 * ### Task scheduling
92 *
93 * Delay skipping is achieved by using virtual time.
94 * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
95 * then its [TestCoroutineScheduler] is used;
96 * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
97 * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
98 * The scheduler can be accessed via [TestScope.testScheduler].
99 *
100 * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
101 * ```
102 * @Test
103 * fun exampleTest() = runTest {
104 * val elapsed = TimeSource.Monotonic.measureTime {
105 * val deferred = async {
106 * delay(1.seconds) // will be skipped
107 * withContext(Dispatchers.Default) {
108 * delay(5.seconds) // Dispatchers.Default doesn't know about TestCoroutineScheduler
109 * }
110 * }
111 * deferred.await()
112 * }
113 * println(elapsed) // about five seconds
114 * }
115 * ```
116 *
117 * ### Failures
118 *
119 * #### Test body failures
120 *
121 * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
122 *
123 * #### Timing out
124 *
125 * There's a built-in timeout of 10 seconds for the test body. If the test body doesn't complete within this time,
126 * then the test fails with an [AssertionError]. The timeout can be changed by setting the [timeout] parameter.
127 *
128 * On timeout, the test body is cancelled so that the test finishes. If the code inside the test body does not
129 * respond to cancellation, the timeout will not be able to make the test execution stop.
130 * In that case, the test will hang despite the attempt to terminate it.
131 *
132 * On the JVM, if `DebugProbes` from the `kotlinx-coroutines-debug` module are installed, the current dump of the
133 * coroutines' stack is printed to the console on timeout before the test body is cancelled.
134 *
135 * #### Reported exceptions
136 *
137 * Unhandled exceptions will be thrown at the end of the test.
138 * If uncaught exceptions happen after the test finishes, they are propagated in a platform-specific manner:
139 * see [handleCoroutineException] for details.
140 * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
141 *
142 * #### Uncompleted coroutines
143 *
144 * Otherwise, the test will hang until all the coroutines launched inside [testBody] complete.
145 * This may be an issue when there are some coroutines that are not supposed to complete, like infinite loops that
146 * perform some background work and are supposed to outlive the test.
147 * In that case, [TestScope.backgroundScope] can be used to launch such coroutines.
148 * They will be cancelled automatically when the test finishes.
149 *
150 * ### Configuration
151 *
152 * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
153 * scope created for the test, [context] also can be used to change how the test is executed.
154 * See the [TestScope] constructor function documentation for details.
155 *
156 * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
157 */
158 public fun runTest(
159 context: CoroutineContext = EmptyCoroutineContext,
160 timeout: Duration = DEFAULT_TIMEOUT,
161 testBody: suspend TestScope.() -> Unit
162 ): TestResult {
163 check(context[RunningInRunTest] == null) {
164 "Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details."
165 }
166 return TestScope(context + RunningInRunTest).runTest(timeout, testBody)
167 }
168
169 /**
170 * Executes [testBody] as a test in a new coroutine, returning [TestResult].
171 *
172 * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
173 * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
174 * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
175 *
176 * ```
177 * @Test
178 * fun exampleTest() = runTest {
179 * val deferred = async {
180 * delay(1.seconds)
181 * async {
182 * delay(1.seconds)
183 * }.await()
184 * }
185 *
186 * deferred.await() // result available immediately
187 * }
188 * ```
189 *
190 * The platform difference entails that, in order to use this function correctly in common code, one must always
191 * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
192 * [TestResult] for details on this.
193 *
194 * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
195 * Because of this, child coroutines are not executed in parallel to the test body.
196 * In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
197 * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
198 *
199 * ```
200 * @Test
201 * fun exampleWaitingForAsyncTasks1() = runTest {
202 * // 1
203 * val job = launch {
204 * // 3
205 * }
206 * // 2
207 * job.join() // the main test coroutine suspends here, so the child is executed
208 * // 4
209 * }
210 *
211 * @Test
212 * fun exampleWaitingForAsyncTasks2() = runTest {
213 * // 1
214 * launch {
215 * // 3
216 * }
217 * // 2
218 * advanceUntilIdle() // runs the tasks until their queue is empty
219 * // 4
220 * }
221 * ```
222 *
223 * ### Task scheduling
224 *
225 * Delay-skipping is achieved by using virtual time.
226 * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
227 * then its [TestCoroutineScheduler] is used;
228 * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
229 * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
230 * Some convenience methods are available on [TestScope] to control the scheduler.
231 *
232 * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
233 * ```
234 * @Test
235 * fun exampleTest() = runTest {
236 * val elapsed = TimeSource.Monotonic.measureTime {
237 * val deferred = async {
238 * delay(1.seconds) // will be skipped
239 * withContext(Dispatchers.Default) {
240 * delay(5.seconds) // Dispatchers.Default doesn't know about TestCoroutineScheduler
241 * }
242 * }
243 * deferred.await()
244 * }
245 * println(elapsed) // about five seconds
246 * }
247 * ```
248 *
249 * ### Failures
250 *
251 * #### Test body failures
252 *
253 * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
254 *
255 * #### Reported exceptions
256 *
257 * Unhandled exceptions will be thrown at the end of the test.
258 * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
259 * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
260 *
261 * #### Uncompleted coroutines
262 *
263 * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
264 * [testBody] also complete, or are cancelled.
265 * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
266 * [AssertionError], whereas on JS, the `Promise` will fail with it).
267 *
268 * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
269 * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
270 * for [dispatchTimeoutMs] from the moment when [TestCoroutineScheduler] becomes
271 * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
272 * task during that time, the timer gets reset.
273 *
274 * ### Configuration
275 *
276 * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
277 * scope created for the test, [context] also can be used to change how the test is executed.
278 * See the [TestScope] constructor function documentation for details.
279 *
280 * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
281 */
282 @Deprecated(
283 "Define a total timeout for the whole test instead of using dispatchTimeoutMs. " +
284 "Warning: the proposed replacement is not identical as it uses 'dispatchTimeoutMs' as the timeout for the whole test!",
285 ReplaceWith("runTest(context, timeout = dispatchTimeoutMs.milliseconds, testBody)",
286 "kotlin.time.Duration.Companion.milliseconds"),
287 DeprecationLevel.WARNING
288 ) // Warning since 1.7.0, was experimental in 1.6.x
runTestnull289 public fun runTest(
290 context: CoroutineContext = EmptyCoroutineContext,
291 dispatchTimeoutMs: Long,
292 testBody: suspend TestScope.() -> Unit
293 ): TestResult {
294 if (context[RunningInRunTest] != null)
295 throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
296 @Suppress("DEPRECATION")
297 return TestScope(context + RunningInRunTest).runTest(dispatchTimeoutMs = dispatchTimeoutMs, testBody)
298 }
299
300 /**
301 * Performs [runTest] on an existing [TestScope]. See the documentation for [runTest] for details.
302 */
runTestnull303 public fun TestScope.runTest(
304 timeout: Duration = DEFAULT_TIMEOUT,
305 testBody: suspend TestScope.() -> Unit
306 ): TestResult = asSpecificImplementation().let { scope ->
307 scope.enter()
308 createTestResult {
309 /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
310 scope.start(CoroutineStart.UNDISPATCHED, scope) {
311 /* we're using `UNDISPATCHED` to avoid the event loop, but we do want to set up the timeout machinery
312 before any code executes, so we have to park here. */
313 yield()
314 testBody()
315 }
316 var timeoutError: Throwable? = null
317 var cancellationException: CancellationException? = null
318 val workRunner = launch(CoroutineName("kotlinx.coroutines.test runner")) {
319 while (true) {
320 val executedSomething = testScheduler.tryRunNextTaskUnless { !isActive }
321 if (executedSomething) {
322 /** yield to check for cancellation. On JS, we can't use [ensureActive] here, as the cancellation
323 * procedure needs a chance to run concurrently. */
324 yield()
325 } else {
326 // waiting for the next task to be scheduled, or for the test runner to be cancelled
327 testScheduler.receiveDispatchEvent()
328 }
329 }
330 }
331 try {
332 withTimeout(timeout) {
333 coroutineContext.job.invokeOnCompletion(onCancelling = true) { exception ->
334 if (exception is TimeoutCancellationException) {
335 dumpCoroutines()
336 val activeChildren = scope.children.filter(Job::isActive).toList()
337 val completionCause = if (scope.isCancelled) scope.tryGetCompletionCause() else null
338 var message = "After waiting for $timeout"
339 if (completionCause == null)
340 message += ", the test coroutine is not completing"
341 if (activeChildren.isNotEmpty())
342 message += ", there were active child jobs: $activeChildren"
343 if (completionCause != null && activeChildren.isEmpty()) {
344 message += if (scope.isCompleted)
345 ", the test coroutine completed"
346 else
347 ", the test coroutine was not completed"
348 }
349 timeoutError = UncompletedCoroutinesError(message)
350 cancellationException = CancellationException("The test timed out")
351 (scope as Job).cancel(cancellationException!!)
352 }
353 }
354 scope.join()
355 workRunner.cancelAndJoin()
356 }
357 } catch (_: TimeoutCancellationException) {
358 scope.join()
359 val completion = scope.getCompletionExceptionOrNull()
360 if (completion != null && completion !== cancellationException) {
361 timeoutError!!.addSuppressed(completion)
362 }
363 workRunner.cancelAndJoin()
364 } finally {
365 backgroundScope.cancel()
366 testScheduler.advanceUntilIdleOr { false }
367 val uncaughtExceptions = scope.leave()
368 throwAll(timeoutError ?: scope.getCompletionExceptionOrNull(), uncaughtExceptions)
369 }
370 }
371 }
372
373 /**
374 * Performs [runTest] on an existing [TestScope].
375 *
376 * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
377 * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
378 * for [dispatchTimeoutMs] from the moment when [TestCoroutineScheduler] becomes
379 * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
380 * task during that time, the timer gets reset.
381 */
382 @Deprecated(
383 "Define a total timeout for the whole test instead of using dispatchTimeoutMs. " +
384 "Warning: the proposed replacement is not identical as it uses 'dispatchTimeoutMs' as the timeout for the whole test!",
385 ReplaceWith("this.runTest(timeout = dispatchTimeoutMs.milliseconds, testBody)",
386 "kotlin.time.Duration.Companion.milliseconds"),
387 DeprecationLevel.WARNING
388 ) // Warning since 1.7.0, was experimental in 1.6.x
runTestnull389 public fun TestScope.runTest(
390 dispatchTimeoutMs: Long,
391 testBody: suspend TestScope.() -> Unit
392 ): TestResult = asSpecificImplementation().let {
393 it.enter()
394 @Suppress("DEPRECATION")
395 createTestResult {
396 runTestCoroutineLegacy(it, dispatchTimeoutMs.milliseconds, TestScopeImpl::tryGetCompletionCause, testBody) {
397 backgroundScope.cancel()
398 testScheduler.advanceUntilIdleOr { false }
399 it.legacyLeave()
400 }
401 }
402 }
403
404 /**
405 * Runs [testProcedure], creating a [TestResult].
406 */
407 @Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult`
createTestResultnull408 internal expect fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult
409
410 /** A coroutine context element indicating that the coroutine is running inside `runTest`. */
411 internal object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
412 override val key: CoroutineContext.Key<*>
413 get() = this
414
415 override fun toString(): String = "RunningInRunTest"
416 }
417
418 /** The default timeout to use when waiting for asynchronous completions of the coroutines managed by
419 * a [TestCoroutineScheduler]. */
420 internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
421
422 /**
423 * The default timeout to use when running a test.
424 */
425 internal val DEFAULT_TIMEOUT = 10.seconds
426
427 /**
428 * Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
429 * [dispatchTimeout] and performing the [cleanup] procedure at the end.
430 *
431 * [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected.
432 *
433 * The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or
434 * return a list of uncaught exceptions that should be reported at the end of the test.
435 */
436 @Deprecated("Used for support of legacy behavior")
runTestCoroutineLegacynull437 internal suspend fun <T : AbstractCoroutine<Unit>> CoroutineScope.runTestCoroutineLegacy(
438 coroutine: T,
439 dispatchTimeout: Duration,
440 tryGetCompletionCause: T.() -> Throwable?,
441 testBody: suspend T.() -> Unit,
442 cleanup: () -> List<Throwable>,
443 ) {
444 val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!!
445 /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
446 coroutine.start(CoroutineStart.UNDISPATCHED, coroutine) {
447 testBody()
448 }
449 /**
450 * This is the legacy behavior, kept for now for compatibility only.
451 *
452 * The general procedure here is as follows:
453 * 1. Try running the work that the scheduler knows about, both background and foreground.
454 *
455 * 2. Wait until we run out of foreground work to do. This could mean one of the following:
456 * * The main coroutine is already completed. This is checked separately; then we leave the procedure.
457 * * It's switched to another dispatcher that doesn't know about the [TestCoroutineScheduler].
458 * * Generally, it's waiting for something external (like a network request, or just an arbitrary callback).
459 * * The test simply hanged.
460 * * The main coroutine is waiting for some background work.
461 *
462 * 3. We await progress from things that are not the code under test:
463 * the background work that the scheduler knows about, the external callbacks,
464 * the work on dispatchers not linked to the scheduler, etc.
465 *
466 * When we observe that the code under test can proceed, we go to step 1 again.
467 * If there is no activity for [dispatchTimeoutMs] milliseconds, we consider the test to have hanged.
468 *
469 * The background work is not running on a dedicated thread.
470 * Instead, the test thread itself is used, by spawning a separate coroutine.
471 */
472 var completed = false
473 while (!completed) {
474 scheduler.advanceUntilIdle()
475 if (coroutine.isCompleted) {
476 /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
477 non-trivial dispatches. */
478 completed = true
479 continue
480 }
481 // in case progress depends on some background work, we need to keep spinning it.
482 val backgroundWorkRunner = launch(CoroutineName("background work runner")) {
483 while (true) {
484 val executedSomething = scheduler.tryRunNextTaskUnless { !isActive }
485 if (executedSomething) {
486 // yield so that the `select` below has a chance to finish successfully or time out
487 yield()
488 } else {
489 // no more tasks, we should suspend until there are some more.
490 // this doesn't interfere with the `select` below, because different channels are used.
491 scheduler.receiveDispatchEvent()
492 }
493 }
494 }
495 try {
496 select<Unit> {
497 coroutine.onJoin {
498 // observe that someone completed the test coroutine and leave without waiting for the timeout
499 completed = true
500 }
501 scheduler.onDispatchEventForeground {
502 // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
503 }
504 onTimeout(dispatchTimeout) {
505 throw handleTimeout(coroutine, dispatchTimeout, tryGetCompletionCause, cleanup)
506 }
507 }
508 } finally {
509 backgroundWorkRunner.cancelAndJoin()
510 }
511 }
512 coroutine.getCompletionExceptionOrNull()?.let { exception ->
513 val exceptions = try {
514 cleanup()
515 } catch (e: UncompletedCoroutinesError) {
516 // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
517 emptyList()
518 }
519 throwAll(exception, exceptions)
520 }
521 throwAll(null, cleanup())
522 }
523
524 /**
525 * Invoked on timeout in [runTest]. Just builds a nice [UncompletedCoroutinesError] and returns it.
526 */
handleTimeoutnull527 private inline fun <T : AbstractCoroutine<Unit>> handleTimeout(
528 coroutine: T,
529 dispatchTimeout: Duration,
530 tryGetCompletionCause: T.() -> Throwable?,
531 cleanup: () -> List<Throwable>,
532 ): AssertionError {
533 val uncaughtExceptions = try {
534 cleanup()
535 } catch (e: UncompletedCoroutinesError) {
536 // we expect these and will instead throw a more informative exception.
537 emptyList()
538 }
539 val activeChildren = coroutine.children.filter { it.isActive }.toList()
540 val completionCause = if (coroutine.isCancelled) coroutine.tryGetCompletionCause() else null
541 var message = "After waiting for $dispatchTimeout"
542 if (completionCause == null)
543 message += ", the test coroutine is not completing"
544 if (activeChildren.isNotEmpty())
545 message += ", there were active child jobs: $activeChildren"
546 if (completionCause != null && activeChildren.isEmpty()) {
547 message += if (coroutine.isCompleted)
548 ", the test coroutine completed"
549 else
550 ", the test coroutine was not completed"
551 }
552 val error = UncompletedCoroutinesError(message)
553 completionCause?.let { cause -> error.addSuppressed(cause) }
554 uncaughtExceptions.forEach { error.addSuppressed(it) }
555 return error
556 }
557
throwAllnull558 internal fun throwAll(head: Throwable?, other: List<Throwable>) {
559 if (head != null) {
560 other.forEach { head.addSuppressed(it) }
561 throw head
562 } else {
563 with(other) {
564 firstOrNull()?.apply {
565 drop(1).forEach { addSuppressed(it) }
566 throw this
567 }
568 }
569 }
570 }
571
dumpCoroutinesnull572 internal expect fun dumpCoroutines()
573
574 @Deprecated(
575 "This is for binary compatibility with the `runTest` overload that existed at some point",
576 level = DeprecationLevel.HIDDEN
577 )
578 @JvmName("runTest\$default")
579 @Suppress("DEPRECATION", "UNUSED_PARAMETER")
580 public fun TestScope.runTestLegacy(
581 dispatchTimeoutMs: Long,
582 testBody: suspend TestScope.() -> Unit,
583 marker: Int,
584 unused2: Any?,
585 ): TestResult = runTest(dispatchTimeoutMs = if (marker and 1 != 0) dispatchTimeoutMs else 60_000L, testBody)
586