• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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