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
14 /**
15 * Executes a [testBody] inside an immediate execution dispatcher.
16 *
17 * This method is deprecated in favor of [runTest]. Please see the
18 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
19 * for an instruction on how to update the code for the new API.
20 *
21 * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
22 * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
23 * extra time.
24 *
25 * ```
26 * @Test
27 * fun exampleTest() = runBlockingTest {
28 * val deferred = async {
29 * delay(1_000)
30 * async {
31 * delay(1_000)
32 * }.await()
33 * }
34 *
35 * deferred.await() // result available immediately
36 * }
37 *
38 * ```
39 *
40 * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
41 * conditions.
42 *
43 * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
44 *
45 * @throws AssertionError If the [testBody] does not complete (or cancel) all coroutines that it launches
46 * (including coroutines suspended on join/await).
47 *
48 * @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
49 * then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
50 * @param testBody The code of the unit-test.
51 */
52 @Deprecated("Use `runTest` instead to support completing from other dispatchers. " +
53 "Please see the migration guide for details: " +
54 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
55 level = DeprecationLevel.WARNING)
56 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
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.WARNING)
77 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
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.leave()` 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.leave()
100 } catch (e: UncompletedCoroutinesError) {
101 listOf()
102 }
103 (listOf(it) + exceptions).throwAll()
104 return
105 }
106 scope.leave().throwAll()
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("Use `runTest` instead to support completing from other dispatchers. " +
121 "Please see the migration guide for details: " +
122 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
123 level = DeprecationLevel.WARNING)
124 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
runBlockingTestnull125 public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
126 runBlockingTest(coroutineContext, block)
127
128 /**
129 * Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope].
130 */
131 @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
132 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
133 public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit =
134 runBlockingTestOnTestScope(coroutineContext, block)
135
136 /**
137 * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
138 *
139 * This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
140 * Please see the
141 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
142 * for an instruction on how to update the code for the new API.
143 */
144 @Deprecated("Use `runTest` instead to support completing from other dispatchers. " +
145 "Please see the migration guide for details: " +
146 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
147 level = DeprecationLevel.WARNING)
148 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
149 public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
150 runBlockingTest(this, block)
151
152 /**
153 * This is an overload of [runTest] that works with [TestCoroutineScope].
154 */
155 @ExperimentalCoroutinesApi
156 @Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING)
157 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
158 public fun runTestWithLegacyScope(
159 context: CoroutineContext = EmptyCoroutineContext,
160 dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
161 testBody: suspend TestCoroutineScope.() -> Unit
162 ): TestResult {
163 if (context[RunningInRunTest] != null)
164 throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
165 val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest))
166 return createTestResult {
167 runTestCoroutine(testScope, dispatchTimeoutMs, TestBodyCoroutine::tryGetCompletionCause, testBody) {
168 try {
169 testScope.cleanup()
170 emptyList()
171 } catch (e: UncompletedCoroutinesError) {
172 throw e
173 } catch (e: Throwable) {
174 listOf(e)
175 }
176 }
177 }
178 }
179
180 /**
181 * Runs a test in a [TestCoroutineScope] based on this one.
182 *
183 * Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the
184 * [block] will be different from this one, but will use its [Job] as a parent.
185 *
186 * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
187 * immediately from the test body. See the docs for [TestResult] for details.
188 */
189 @ExperimentalCoroutinesApi
190 @Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING)
191 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
runTestnull192 public fun TestCoroutineScope.runTest(
193 dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
194 block: suspend TestCoroutineScope.() -> Unit
195 ): TestResult = runTestWithLegacyScope(coroutineContext, dispatchTimeoutMs, block)
196
197 private class TestBodyCoroutine(
198 private val testScope: TestCoroutineScope,
199 ) : AbstractCoroutine<Unit>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
200
201 override val testScheduler get() = testScope.testScheduler
202
203 @Deprecated(
204 "This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
205 ReplaceWith("this.cleanup()"),
206 DeprecationLevel.ERROR
207 )
208 override fun cleanupTestCoroutines() =
209 throw UnsupportedOperationException(
210 "Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
211 "it will be called at the end of the test in any case."
212 )
213
214 fun cleanup() = testScope.cleanupTestCoroutines()
215
216 /** Throws an exception if the coroutine is not completing. */
217 fun tryGetCompletionCause(): Throwable? = completionCause
218 }
219