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:Suppress("DEPRECATION")
5
6 package kotlinx.coroutines.test
7
8 import kotlinx.coroutines.*
9 import kotlinx.coroutines.internal.*
10 import kotlin.coroutines.*
11
12 /**
13 * A scope which provides detailed control over the execution of coroutines for tests.
14 *
15 * This scope is deprecated in favor of [TestScope].
16 * Please see the
17 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
18 * for an instruction on how to update the code for the new API.
19 */
20 @ExperimentalCoroutinesApi
21 @Deprecated("Use `TestScope` in combination with `runTest` instead." +
22 "Please see the migration guide for details: " +
23 "https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
24 level = DeprecationLevel.WARNING)
25 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
26 public interface TestCoroutineScope : CoroutineScope {
27 /**
28 * Called after the test completes.
29 *
30 * * It checks that there were no uncaught exceptions caught by its [CoroutineExceptionHandler].
31 * If there were any, then the first one is thrown, whereas the rest are suppressed by it.
32 * * It runs the tasks pending in the scheduler at the current time. If there are any uncompleted tasks afterwards,
33 * it fails with [UncompletedCoroutinesError].
34 * * It checks whether some new child [Job]s were created but not completed since this [TestCoroutineScope] was
35 * created. If so, it fails with [UncompletedCoroutinesError].
36 *
37 * For backward compatibility, if the [CoroutineExceptionHandler] is an [UncaughtExceptionCaptor], its
38 * [TestCoroutineExceptionHandler.cleanupTestCoroutines] behavior is performed.
39 * Likewise, if the [ContinuationInterceptor] is a [DelayController], its [DelayController.cleanupTestCoroutines]
40 * is called.
41 *
42 * @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
43 * @throws AssertionError if any pending tasks are active.
44 * @throws IllegalStateException if called more than once.
45 */
46 @ExperimentalCoroutinesApi
47 @Deprecated("Please call `runTest`, which automatically performs the cleanup, instead of using this function.")
48 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
49 public fun cleanupTestCoroutines()
50
51 /**
52 * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
53 */
54 @ExperimentalCoroutinesApi
55 public val testScheduler: TestCoroutineScheduler
56 }
57
58 private class TestCoroutineScopeImpl(
59 override val coroutineContext: CoroutineContext
60 ) : TestCoroutineScope {
61 private val lock = SynchronizedObject()
62 private var exceptions = mutableListOf<Throwable>()
63 private var cleanedUp = false
64
65 /**
66 * Reports an exception so that it is thrown on [cleanupTestCoroutines].
67 *
68 * If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by
69 * it.
70 *
71 * Returns `false` if [cleanupTestCoroutines] was already called.
72 */
reportExceptionnull73 fun reportException(throwable: Throwable): Boolean =
74 synchronized(lock) {
75 if (cleanedUp) {
76 false
77 } else {
78 exceptions.add(throwable)
79 true
80 }
81 }
82
83 override val testScheduler: TestCoroutineScheduler
84 get() = coroutineContext[TestCoroutineScheduler]!!
85
86 /** These jobs existed before the coroutine scope was used, so it's alright if they don't get cancelled. */
87 private val initialJobs = coroutineContext.activeJobs()
88
cleanupTestCoroutinesnull89 override fun cleanupTestCoroutines() {
90 val delayController = coroutineContext.delayController
91 val hasUnfinishedJobs = if (delayController != null) {
92 try {
93 delayController.cleanupTestCoroutines()
94 false
95 } catch (e: UncompletedCoroutinesError) {
96 true
97 }
98 } else {
99 testScheduler.runCurrent()
100 !testScheduler.isIdle(strict = false)
101 }
102 (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.cleanupTestCoroutines()
103 synchronized(lock) {
104 if (cleanedUp)
105 throw IllegalStateException("Attempting to clean up a test coroutine scope more than once.")
106 cleanedUp = true
107 }
108 exceptions.firstOrNull()?.let { toThrow ->
109 exceptions.drop(1).forEach { toThrow.addSuppressed(it) }
110 throw toThrow
111 }
112 if (hasUnfinishedJobs)
113 throw UncompletedCoroutinesError(
114 "Unfinished coroutines during teardown. Ensure all coroutines are" +
115 " completed or cancelled by your test."
116 )
117 val jobs = coroutineContext.activeJobs()
118 if ((jobs - initialJobs).isNotEmpty())
119 throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
120 }
121 }
122
activeJobsnull123 internal fun CoroutineContext.activeJobs(): Set<Job> {
124 return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
125 }
126
127 /**
128 * A coroutine scope for launching test coroutines using [TestCoroutineDispatcher].
129 *
130 * [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher].
131 */
132 @Deprecated(
133 "This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
134 "Please use `createTestCoroutineScope` instead.",
135 ReplaceWith(
136 "createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)",
137 "kotlin.coroutines.EmptyCoroutineContext"
138 ),
139 level = DeprecationLevel.WARNING
140 )
141 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
TestCoroutineScopenull142 public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
143 val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler()
144 return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + TestCoroutineExceptionHandler() + context)
145 }
146
147 /**
148 * A coroutine scope for launching test coroutines.
149 *
150 * This is a function for aiding in migration from [TestCoroutineScope] to [TestScope].
151 * Please see the
152 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
153 * for an instruction on how to update the code for the new API.
154 *
155 * It ensures that all the test module machinery is properly initialized.
156 * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
157 * a new one is created, unless either
158 * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
159 * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
160 * its [TestCoroutineScheduler] is used.
161 * * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created.
162 * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
163 * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
164 * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
165 * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
166 * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
167 * [TestCoroutineScope] and share your use case at
168 * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
169 * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
170 *
171 * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
172 * different scheduler.
173 * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
174 * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
175 * [UncaughtExceptionCaptor].
176 */
177 @ExperimentalCoroutinesApi
178 @Deprecated(
179 "This function was introduced in order to help migrate from TestCoroutineScope to TestScope. " +
180 "Please use TestScope() construction instead, or just runTest(), without creating a scope.",
181 level = DeprecationLevel.WARNING
182 )
183 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
createTestCoroutineScopenull184 public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
185 val ctxWithDispatcher = context.withDelaySkipping()
186 var scope: TestCoroutineScopeImpl? = null
187 val ownExceptionHandler =
188 object : AbstractCoroutineContextElement(CoroutineExceptionHandler), TestCoroutineScopeExceptionHandler {
189 override fun handleException(context: CoroutineContext, exception: Throwable) {
190 if (!scope!!.reportException(exception))
191 throw exception // let this exception crash everything
192 }
193 }
194 val exceptionHandler = when (val exceptionHandler = ctxWithDispatcher[CoroutineExceptionHandler]) {
195 is UncaughtExceptionCaptor -> exceptionHandler
196 null -> ownExceptionHandler
197 is TestCoroutineScopeExceptionHandler -> ownExceptionHandler
198 else -> throw IllegalArgumentException(
199 "A CoroutineExceptionHandler was passed to TestCoroutineScope. " +
200 "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
201 "if uncaught exceptions require special treatment."
202 )
203 }
204 val job: Job = ctxWithDispatcher[Job] ?: Job()
205 return TestCoroutineScopeImpl(ctxWithDispatcher + exceptionHandler + job).also {
206 scope = it
207 }
208 }
209
210 /** A marker that shows that this [CoroutineExceptionHandler] was created for [TestCoroutineScope]. With this,
211 * constructing a new [TestCoroutineScope] with the [CoroutineScope.coroutineContext] of an existing one will override
212 * the exception handler, instead of failing. */
213 private interface TestCoroutineScopeExceptionHandler : CoroutineExceptionHandler
214
215 private inline val CoroutineContext.delayController: DelayController?
216 get() {
217 val handler = this[ContinuationInterceptor]
218 return handler as? DelayController
219 }
220
221
222 /**
223 * The current virtual time on [testScheduler][TestCoroutineScope.testScheduler].
224 * @see TestCoroutineScheduler.currentTime
225 */
226 @ExperimentalCoroutinesApi
227 public val TestCoroutineScope.currentTime: Long
228 get() = coroutineContext.delayController?.currentTime ?: testScheduler.currentTime
229
230 /**
231 * Advances the [testScheduler][TestCoroutineScope.testScheduler] by [delayTimeMillis] and runs the tasks up to that
232 * moment (inclusive).
233 *
234 * @see TestCoroutineScheduler.advanceTimeBy
235 */
236 @ExperimentalCoroutinesApi
237 @Deprecated(
238 "The name of this function is misleading: it not only advances the time, but also runs the tasks " +
239 "scheduled *at* the ending moment.",
240 ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
241 DeprecationLevel.WARNING
242 )
243 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
advanceTimeBynull244 public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit =
245 when (val controller = coroutineContext.delayController) {
246 null -> {
247 testScheduler.advanceTimeBy(delayTimeMillis)
248 testScheduler.runCurrent()
249 }
250 else -> {
251 controller.advanceTimeBy(delayTimeMillis)
252 Unit
253 }
254 }
255
256 /**
257 * Advances the [testScheduler][TestCoroutineScope.testScheduler] to the point where there are no tasks remaining.
258 * @see TestCoroutineScheduler.advanceUntilIdle
259 */
260 @ExperimentalCoroutinesApi
advanceUntilIdlenull261 public fun TestCoroutineScope.advanceUntilIdle() {
262 coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle()
263 }
264
265 /**
266 * Run any tasks that are pending at the current virtual time, according to
267 * the [testScheduler][TestCoroutineScope.testScheduler].
268 *
269 * @see TestCoroutineScheduler.runCurrent
270 */
271 @ExperimentalCoroutinesApi
runCurrentnull272 public fun TestCoroutineScope.runCurrent() {
273 coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent()
274 }
275
276 @ExperimentalCoroutinesApi
277 @Deprecated(
278 "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
279 "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
280 "\"paused\", like `StandardTestDispatcher`.",
281 ReplaceWith(
282 "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
283 "kotlin.coroutines.ContinuationInterceptor"
284 ),
285 DeprecationLevel.WARNING
286 )
287 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
pauseDispatchernull288 public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) {
289 delayControllerForPausing.pauseDispatcher(block)
290 }
291
292 @ExperimentalCoroutinesApi
293 @Deprecated(
294 "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
295 "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
296 "\"paused\", like `StandardTestDispatcher`.",
297 ReplaceWith(
298 "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
299 "kotlin.coroutines.ContinuationInterceptor"
300 ),
301 level = DeprecationLevel.WARNING
302 )
303 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
pauseDispatchernull304 public fun TestCoroutineScope.pauseDispatcher() {
305 delayControllerForPausing.pauseDispatcher()
306 }
307
308 @ExperimentalCoroutinesApi
309 @Deprecated(
310 "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
311 "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
312 "\"paused\", like `StandardTestDispatcher`.",
313 ReplaceWith(
314 "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
315 "kotlin.coroutines.ContinuationInterceptor"
316 ),
317 level = DeprecationLevel.WARNING
318 )
319 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
resumeDispatchernull320 public fun TestCoroutineScope.resumeDispatcher() {
321 delayControllerForPausing.resumeDispatcher()
322 }
323
324 /**
325 * List of uncaught coroutine exceptions, for backward compatibility.
326 *
327 * The returned list is a copy of the exceptions caught during execution.
328 * During [TestCoroutineScope.cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
329 *
330 * Exceptions are only collected in this list if the [UncaughtExceptionCaptor] is in the test context.
331 */
332 @Deprecated(
333 "This list is only populated if `UncaughtExceptionCaptor` is in the test context, and so can be " +
334 "easily misused. It is only present for backward compatibility and will be removed in the subsequent " +
335 "releases. If you need to check the list of exceptions, please consider creating your own " +
336 "`CoroutineExceptionHandler`.",
337 level = DeprecationLevel.WARNING
338 )
339 public val TestCoroutineScope.uncaughtExceptions: List<Throwable>
340 get() = (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.uncaughtExceptions
341 ?: emptyList()
342
343 private val TestCoroutineScope.delayControllerForPausing: DelayController
344 get() = coroutineContext.delayController
345 ?: throw IllegalStateException("This scope isn't able to pause its dispatchers")
346