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