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_ERROR", "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, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.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, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.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
89 @Deprecated("Please call `runTest`, which automatically performs the cleanup, instead of using this function.")
cleanupTestCoroutinesnull90 override fun cleanupTestCoroutines() {
91 val delayController = coroutineContext.delayController
92 val hasUnfinishedJobs = if (delayController != null) {
93 try {
94 delayController.cleanupTestCoroutines()
95 false
96 } catch (e: UncompletedCoroutinesError) {
97 true
98 }
99 } else {
100 testScheduler.runCurrent()
101 !testScheduler.isIdle(strict = false)
102 }
103 (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.cleanupTestCoroutines()
104 synchronized(lock) {
105 if (cleanedUp)
106 throw IllegalStateException("Attempting to clean up a test coroutine scope more than once.")
107 cleanedUp = true
108 }
109 exceptions.firstOrNull()?.let { toThrow ->
110 exceptions.drop(1).forEach { toThrow.addSuppressed(it) }
111 throw toThrow
112 }
113 if (hasUnfinishedJobs)
114 throw UncompletedCoroutinesError(
115 "Unfinished coroutines during teardown. Ensure all coroutines are" +
116 " completed or cancelled by your test."
117 )
118 val jobs = coroutineContext.activeJobs()
119 if ((jobs - initialJobs).isNotEmpty())
120 throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
121 }
122 }
123
activeJobsnull124 internal fun CoroutineContext.activeJobs(): Set<Job> {
125 return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
126 }
127
128 /**
129 * A coroutine scope for launching test coroutines using [TestCoroutineDispatcher].
130 *
131 * [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher].
132 */
133 @Deprecated(
134 "This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
135 "Please use `createTestCoroutineScope` instead.",
136 ReplaceWith(
137 "createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)",
138 "kotlin.coroutines.EmptyCoroutineContext"
139 ),
140 level = DeprecationLevel.WARNING
141 )
142 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
TestCoroutineScopenull143 public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
144 val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler()
145 return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + TestCoroutineExceptionHandler() + context)
146 }
147
148 /**
149 * A coroutine scope for launching test coroutines.
150 *
151 * This is a function for aiding in migration from [TestCoroutineScope] to [TestScope].
152 * Please see the
153 * [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
154 * for an instruction on how to update the code for the new API.
155 *
156 * It ensures that all the test module machinery is properly initialized.
157 * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
158 * a new one is created, unless either
159 * - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
160 * - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
161 * its [TestCoroutineScheduler] is used.
162 * * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created.
163 * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
164 * any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
165 * already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
166 * [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
167 * If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
168 * [TestCoroutineScope] and share your use case at
169 * [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
170 * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
171 *
172 * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
173 * different scheduler.
174 * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
175 * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
176 * [UncaughtExceptionCaptor].
177 */
178 @ExperimentalCoroutinesApi
179 @Deprecated(
180 "This function was introduced in order to help migrate from TestCoroutineScope to TestScope. " +
181 "Please use TestScope() construction instead, or just runTest(), without creating a scope.",
182 level = DeprecationLevel.WARNING
183 )
184 // Since 1.6.0, kept as warning in 1.7.0, ERROR in 1.8.0 and removed as experimental in 1.9.0
createTestCoroutineScopenull185 public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
186 val ctxWithDispatcher = context.withDelaySkipping()
187 var scope: TestCoroutineScopeImpl? = null
188 val ownExceptionHandler =
189 object : AbstractCoroutineContextElement(CoroutineExceptionHandler), TestCoroutineScopeExceptionHandler {
190 override fun handleException(context: CoroutineContext, exception: Throwable) {
191 if (!scope!!.reportException(exception))
192 throw exception // let this exception crash everything
193 }
194 }
195 val exceptionHandler = when (val exceptionHandler = ctxWithDispatcher[CoroutineExceptionHandler]) {
196 is UncaughtExceptionCaptor -> exceptionHandler
197 null -> ownExceptionHandler
198 is TestCoroutineScopeExceptionHandler -> ownExceptionHandler
199 else -> throw IllegalArgumentException(
200 "A CoroutineExceptionHandler was passed to TestCoroutineScope. " +
201 "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
202 "if uncaught exceptions require special treatment."
203 )
204 }
205 val job: Job = ctxWithDispatcher[Job] ?: Job()
206 return TestCoroutineScopeImpl(ctxWithDispatcher + exceptionHandler + job).also {
207 scope = it
208 }
209 }
210
211 /** A marker that shows that this [CoroutineExceptionHandler] was created for [TestCoroutineScope]. With this,
212 * constructing a new [TestCoroutineScope] with the [CoroutineScope.coroutineContext] of an existing one will override
213 * the exception handler, instead of failing. */
214 private interface TestCoroutineScopeExceptionHandler : CoroutineExceptionHandler
215
216 private inline val CoroutineContext.delayController: DelayController?
217 get() {
218 val handler = this[ContinuationInterceptor]
219 return handler as? DelayController
220 }
221
222
223 /**
224 * The current virtual time on [testScheduler][TestCoroutineScope.testScheduler].
225 * @see TestCoroutineScheduler.currentTime
226 */
227 @ExperimentalCoroutinesApi
228 public val TestCoroutineScope.currentTime: Long
229 get() = coroutineContext.delayController?.currentTime ?: testScheduler.currentTime
230
231 /**
232 * Advances the [testScheduler][TestCoroutineScope.testScheduler] by [delayTimeMillis] and runs the tasks up to that
233 * moment (inclusive).
234 *
235 * @see TestCoroutineScheduler.advanceTimeBy
236 */
237 @ExperimentalCoroutinesApi
238 @Deprecated(
239 "The name of this function is misleading: it not only advances the time, but also runs the tasks " +
240 "scheduled *at* the ending moment.",
241 ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
242 DeprecationLevel.ERROR
243 )
244 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
advanceTimeBynull245 public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit =
246 when (val controller = coroutineContext.delayController) {
247 null -> {
248 testScheduler.advanceTimeBy(delayTimeMillis)
249 testScheduler.runCurrent()
250 }
251 else -> {
252 controller.advanceTimeBy(delayTimeMillis)
253 Unit
254 }
255 }
256
257 /**
258 * Advances the [testScheduler][TestCoroutineScope.testScheduler] to the point where there are no tasks remaining.
259 * @see TestCoroutineScheduler.advanceUntilIdle
260 */
261 @ExperimentalCoroutinesApi
advanceUntilIdlenull262 public fun TestCoroutineScope.advanceUntilIdle() {
263 coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle()
264 }
265
266 /**
267 * Run any tasks that are pending at the current virtual time, according to
268 * the [testScheduler][TestCoroutineScope.testScheduler].
269 *
270 * @see TestCoroutineScheduler.runCurrent
271 */
272 @ExperimentalCoroutinesApi
runCurrentnull273 public fun TestCoroutineScope.runCurrent() {
274 coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent()
275 }
276
277 @ExperimentalCoroutinesApi
278 @Deprecated(
279 "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
280 "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
281 "\"paused\", like `StandardTestDispatcher`.",
282 ReplaceWith(
283 "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
284 "kotlin.coroutines.ContinuationInterceptor"
285 ),
286 DeprecationLevel.ERROR
287 )
288 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
pauseDispatchernull289 public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) {
290 delayControllerForPausing.pauseDispatcher(block)
291 }
292
293 @ExperimentalCoroutinesApi
294 @Deprecated(
295 "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
296 "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
297 "\"paused\", like `StandardTestDispatcher`.",
298 ReplaceWith(
299 "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
300 "kotlin.coroutines.ContinuationInterceptor"
301 ),
302 level = DeprecationLevel.ERROR
303 )
304 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
pauseDispatchernull305 public fun TestCoroutineScope.pauseDispatcher() {
306 delayControllerForPausing.pauseDispatcher()
307 }
308
309 @ExperimentalCoroutinesApi
310 @Deprecated(
311 "The test coroutine scope isn't able to pause its dispatchers in the general case. " +
312 "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
313 "\"paused\", like `StandardTestDispatcher`.",
314 ReplaceWith(
315 "(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
316 "kotlin.coroutines.ContinuationInterceptor"
317 ),
318 level = DeprecationLevel.ERROR
319 )
320 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
resumeDispatchernull321 public fun TestCoroutineScope.resumeDispatcher() {
322 delayControllerForPausing.resumeDispatcher()
323 }
324
325 /**
326 * List of uncaught coroutine exceptions, for backward compatibility.
327 *
328 * The returned list is a copy of the exceptions caught during execution.
329 * During [TestCoroutineScope.cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
330 *
331 * Exceptions are only collected in this list if the [UncaughtExceptionCaptor] is in the test context.
332 */
333 @Deprecated(
334 "This list is only populated if `UncaughtExceptionCaptor` is in the test context, and so can be " +
335 "easily misused. It is only present for backward compatibility and will be removed in the subsequent " +
336 "releases. If you need to check the list of exceptions, please consider creating your own " +
337 "`CoroutineExceptionHandler`.",
338 level = DeprecationLevel.ERROR
339 )
340 // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
341 public val TestCoroutineScope.uncaughtExceptions: List<Throwable>
342 get() = (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.uncaughtExceptions
343 ?: emptyList()
344
345 private val TestCoroutineScope.delayControllerForPausing: DelayController
346 get() = coroutineContext.delayController
347 ?: throw IllegalStateException("This scope isn't able to pause its dispatchers")
348