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