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