1 /*
2 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4 @file:Suppress("DEPRECATION")
5 @file:JvmName("TestBuildersKt")
6 @file:JvmMultifileClass
7
8 package kotlinx.coroutines.test
9
10 import kotlinx.coroutines.*
11 import kotlin.coroutines.*
12 import kotlin.jvm.*
13 import kotlin.time.Duration.Companion.milliseconds
14
15 /**
16 * Executes a [testBody] inside an immediate execution dispatcher.
17 *
18 * This method is deprecated in favor of [runTest]. Please see the
19 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
20 * for an instruction on how to update the code for the new API.
21 *
22 * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
23 * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
24 * extra time.
25 *
26 * ```
27 * @Test
28 * fun exampleTest() = runBlockingTest {
29 * val deferred = async {
30 * delay(1_000)
31 * async {
32 * delay(1_000)
33 * }.await()
34 * }
35 *
36 * deferred.await() // result available immediately
37 * }
38 *
39 * ```
40 *
41 * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
42 * conditions.
43 *
44 * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
45 *
46 * @throws AssertionError If the [testBody] does not complete (or cancel) all coroutines that it launches
47 * (including coroutines suspended on join/await).
48 *
49 * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
50 * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
51 * @param testBody The code of the unit-test.
52 */
53 @Deprecated(
54 "Use `runTest` instead to support completing from other dispatchers. " +
55 "Please see the migration guide for details: " +
56 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
57 level = DeprecationLevel.WARNING
58 )
59 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
runBlockingTestnull60 public fun runBlockingTest(
61 context: CoroutineContext = EmptyCoroutineContext,
62 testBody: suspend TestCoroutineScope.() -> Unit
63 ) {
64 val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
65 val scheduler = scope.testScheduler
66 val deferred = scope.async {
67 scope.testBody()
68 }
69 scheduler.advanceUntilIdle()
70 deferred.getCompletionExceptionOrNull()?.let {
71 throw it
72 }
73 scope.cleanupTestCoroutines()
74 }
75
76 /**
77 * A version of [runBlockingTest] that works with [TestScope].
78 */
79 @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
80 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
runBlockingTestOnTestScopenull81 public fun runBlockingTestOnTestScope(
82 context: CoroutineContext = EmptyCoroutineContext,
83 testBody: suspend TestScope.() -> Unit
84 ) {
85 val completeContext = TestCoroutineDispatcher() + SupervisorJob() + context
86 val startJobs = completeContext.activeJobs()
87 val scope = TestScope(completeContext).asSpecificImplementation()
88 scope.enter()
89 scope.start(CoroutineStart.UNDISPATCHED, scope) {
90 scope.testBody()
91 }
92 scope.testScheduler.advanceUntilIdle()
93 val throwable = try {
94 scope.getCompletionExceptionOrNull()
95 } catch (e: IllegalStateException) {
96 null // the deferred was not completed yet; `scope.legacyLeave()` should complain then about unfinished jobs
97 }
98 scope.backgroundScope.cancel()
99 scope.testScheduler.advanceUntilIdleOr { false }
100 throwable?.let {
101 val exceptions = try {
102 scope.legacyLeave()
103 } catch (e: UncompletedCoroutinesError) {
104 listOf()
105 }
106 throwAll(it, exceptions)
107 return
108 }
109 throwAll(null, scope.legacyLeave())
110 val jobs = completeContext.activeJobs() - startJobs
111 if (jobs.isNotEmpty())
112 throw UncompletedCoroutinesError("Some jobs were not completed at the end of the test: $jobs")
113 }
114
115 /**
116 * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
117 *
118 * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
119 * Please see the
120 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
121 * for an instruction on how to update the code for the new API.
122 */
123 @Deprecated(
124 "Use `runTest` instead to support completing from other dispatchers. " +
125 "Please see the migration guide for details: " +
126 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
127 level = DeprecationLevel.WARNING
128 )
129 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
runBlockingTestnull130 public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
131 runBlockingTest(coroutineContext, block)
132
133 /**
134 * Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope].
135 */
136 @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
137 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
138 public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit =
139 runBlockingTestOnTestScope(coroutineContext, block)
140
141 /**
142 * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
143 *
144 * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
145 * Please see the
146 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
147 * for an instruction on how to update the code for the new API.
148 */
149 @Deprecated(
150 "Use `runTest` instead to support completing from other dispatchers. " +
151 "Please see the migration guide for details: " +
152 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
153 level = DeprecationLevel.WARNING
154 )
155 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
156 public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
157 runBlockingTest(this, block)
158
159 /**
160 * This is an overload of [runTest] that works with [TestCoroutineScope].
161 */
162 @ExperimentalCoroutinesApi
163 @Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING)
164 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
165 public fun runTestWithLegacyScope(
166 context: CoroutineContext = EmptyCoroutineContext,
167 dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
168 testBody: suspend TestCoroutineScope.() -> Unit
169 ) {
170 if (context[RunningInRunTest] != null)
171 throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
172 val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest))
173 return createTestResult {
174 runTestCoroutineLegacy(
175 testScope,
176 dispatchTimeoutMs.milliseconds,
177 TestBodyCoroutine::tryGetCompletionCause,
178 testBody
179 ) {
180 try {
181 testScope.cleanup()
182 emptyList()
183 } catch (e: UncompletedCoroutinesError) {
184 throw e
185 } catch (e: Throwable) {
186 listOf(e)
187 }
188 }
189 }
190 }
191
192 /**
193 * Runs a test in a [TestCoroutineScope] based on this one.
194 *
195 * Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the
196 * [block] will be different from this one, but will use its [Job] as a parent.
197 *
198 * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
199 * immediately from the test body. See the docs for [TestResult] for details.
200 */
201 @ExperimentalCoroutinesApi
202 @Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING)
203 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
runTestnull204 public fun TestCoroutineScope.runTest(
205 dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
206 block: suspend TestCoroutineScope.() -> Unit
207 ): TestResult = runTestWithLegacyScope(coroutineContext, dispatchTimeoutMs, block)
208
209 private class TestBodyCoroutine(
210 private val testScope: TestCoroutineScope,
211 ) : AbstractCoroutine<Unit>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
212
213 override val testScheduler get() = testScope.testScheduler
214
215 @Deprecated(
216 "This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
217 ReplaceWith("this.cleanup()"),
218 DeprecationLevel.ERROR
219 )
220 override fun cleanupTestCoroutines() =
221 throw UnsupportedOperationException(
222 "Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
223 "it will be called at the end of the test in any case."
224 )
225
226 fun cleanup() = testScope.cleanupTestCoroutines()
227
228 /** Throws an exception if the coroutine is not completing. */
229 fun tryGetCompletionCause(): Throwable? = completionCause
230 }
231