• 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 
5 package kotlinx.coroutines.test
6 
7 import kotlinx.coroutines.*
8 import kotlinx.coroutines.internal.*
9 import kotlinx.coroutines.test.internal.*
10 import kotlin.coroutines.*
11 import kotlin.time.*
12 
13 /**
14  * A coroutine scope that for launching test coroutines.
15  *
16  * The scope provides the following functionality:
17  * * The [coroutineContext] includes a [coroutine dispatcher][TestDispatcher] that supports delay-skipping, using
18  *   a [TestCoroutineScheduler] for orchestrating the virtual time.
19  *   This scheduler is also available via the [testScheduler] property, and some helper extension
20  *   methods are defined to more conveniently interact with it: see [TestScope.currentTime], [TestScope.runCurrent],
21  *   [TestScope.advanceTimeBy], and [TestScope.advanceUntilIdle].
22  * * When inside [runTest], uncaught exceptions from the child coroutines of this scope will be reported at the end of
23  *   the test.
24  *   It is invalid for child coroutines to throw uncaught exceptions when outside the call to [TestScope.runTest]:
25  *   the only guarantee in this case is the best effort to deliver the exception.
26  *
27  * The usual way to access a [TestScope] is to call [runTest], but it can also be constructed manually, in order to
28  * use it to initialize the components that participate in the test.
29  *
30  * #### Differences from the deprecated [TestCoroutineScope]
31  *
32  * * This doesn't provide an equivalent of [TestCoroutineScope.cleanupTestCoroutines], and so can't be used as a
33  *   standalone mechanism for writing tests: it does require that [runTest] is eventually called.
34  *   The reason for this is that a proper cleanup procedure that supports using non-test dispatchers and arbitrary
35  *   coroutine suspensions would be equivalent to [runTest], but would also be more error-prone, due to the potential
36  *   for forgetting to perform the cleanup.
37  * * [TestCoroutineScope.advanceTimeBy] also calls [TestCoroutineScheduler.runCurrent] after advancing the virtual time.
38  * * No support for dispatcher pausing, like [DelayController] allows. [TestCoroutineDispatcher], which supported
39  *   pausing, is deprecated; now, instead of pausing a dispatcher, one can use [withContext] to run a dispatcher that's
40  *   paused by default, like [StandardTestDispatcher].
41  * * No access to the list of unhandled exceptions.
42  */
43 public sealed interface TestScope : CoroutineScope {
44     /**
45      * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
46      */
47     public val testScheduler: TestCoroutineScheduler
48 
49     /**
50      * A scope for background work.
51      *
52      * This scope is automatically cancelled when the test finishes.
53      * The coroutines in this scope are run as usual when using [advanceTimeBy] and [runCurrent].
54      * [advanceUntilIdle], on the other hand, will stop advancing the virtual time once only the coroutines in this
55      * scope are left unprocessed.
56      *
57      * Failures in coroutines in this scope do not terminate the test.
58      * Instead, they are reported at the end of the test.
59      * Likewise, failure in the [TestScope] itself will not affect its [backgroundScope],
60      * because there's no parent-child relationship between them.
61      *
62      * A typical use case for this scope is to launch tasks that would outlive the tested code in
63      * the production environment.
64      *
65      * In this example, the coroutine that continuously sends new elements to the channel will get
66      * cancelled:
67      * ```
68      * @Test
69      * fun testExampleBackgroundJob() = runTest {
70      *   val channel = Channel<Int>()
71      *   backgroundScope.launch {
72      *     var i = 0
73      *     while (true) {
74      *       channel.send(i++)
75      *     }
76      *   }
77      *   repeat(100) {
78      *     assertEquals(it, channel.receive())
79      *   }
80      * }
81      * ```
82      */
83     public val backgroundScope: CoroutineScope
84 }
85 
86 /**
87  * The current virtual time on [testScheduler][TestScope.testScheduler].
88  * @see TestCoroutineScheduler.currentTime
89  */
90 @ExperimentalCoroutinesApi
91 public val TestScope.currentTime: Long
92     get() = testScheduler.currentTime
93 
94 /**
95  * Advances the [testScheduler][TestScope.testScheduler] to the point where there are no tasks remaining.
96  * @see TestCoroutineScheduler.advanceUntilIdle
97  */
98 @ExperimentalCoroutinesApi
advanceUntilIdlenull99 public fun TestScope.advanceUntilIdle(): Unit = testScheduler.advanceUntilIdle()
100 
101 /**
102  * Run any tasks that are pending at the current virtual time, according to
103  * the [testScheduler][TestScope.testScheduler].
104  *
105  * @see TestCoroutineScheduler.runCurrent
106  */
107 @ExperimentalCoroutinesApi
108 public fun TestScope.runCurrent(): Unit = testScheduler.runCurrent()
109 
110 /**
111  * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the
112  * scheduled tasks in the meantime.
113  *
114  * In contrast with `TestCoroutineScope.advanceTimeBy`, this function does not run the tasks scheduled at the moment
115  * [currentTime] + [delayTimeMillis].
116  *
117  * @throws IllegalStateException if passed a negative [delay][delayTimeMillis].
118  * @see TestCoroutineScheduler.advanceTimeBy
119  */
120 @ExperimentalCoroutinesApi
121 public fun TestScope.advanceTimeBy(delayTimeMillis: Long): Unit = testScheduler.advanceTimeBy(delayTimeMillis)
122 
123 /**
124  * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTime], running the
125  * scheduled tasks in the meantime.
126  *
127  * @throws IllegalStateException if passed a negative [delay][delayTime].
128  * @see TestCoroutineScheduler.advanceTimeBy
129  */
130 @ExperimentalCoroutinesApi
131 public fun TestScope.advanceTimeBy(delayTime: Duration): Unit = testScheduler.advanceTimeBy(delayTime)
132 
133 /**
134  * The [test scheduler][TestScope.testScheduler] as a [TimeSource].
135  * @see TestCoroutineScheduler.timeSource
136  */
137 @ExperimentalCoroutinesApi
138 @ExperimentalTime
139 public val TestScope.testTimeSource: TimeSource.WithComparableMarks get() = testScheduler.timeSource
140 
141 /**
142  * Creates a [TestScope].
143  *
144  * It ensures that all the test module machinery is properly initialized.
145  * * If [context] doesn't provide a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
146  *   a new one is created, unless either
147  *   - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
148  *   - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
149  *     its [TestCoroutineScheduler] is used.
150  * * If [context] doesn't have a [TestDispatcher], a [StandardTestDispatcher] is created.
151  * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
152  *   any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
153  *   already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
154  *   [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
155  *   If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
156  *   [TestCoroutineScope] and share your use case at
157  *   [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
158  * * If [context] provides a [Job], that job is used as a parent for the new scope.
159  *
160  * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
161  * different scheduler.
162  * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
163  * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
164  * [UncaughtExceptionCaptor].
165  */
166 @Suppress("FunctionName")
167 public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope {
168     val ctxWithDispatcher = context.withDelaySkipping()
169     var scope: TestScopeImpl? = null
170     val exceptionHandler = when (ctxWithDispatcher[CoroutineExceptionHandler]) {
171         null -> CoroutineExceptionHandler { _, exception ->
172             scope!!.reportException(exception)
173         }
174         else -> throw IllegalArgumentException(
175             "A CoroutineExceptionHandler was passed to TestScope. " +
176                 "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
177                 "if uncaught exceptions require special treatment."
178         )
179     }
180     return TestScopeImpl(ctxWithDispatcher + exceptionHandler).also { scope = it }
181 }
182 
183 /**
184  * Adds a [TestDispatcher] and a [TestCoroutineScheduler] to the context if there aren't any already.
185  *
186  * @throws IllegalArgumentException if both a [TestCoroutineScheduler] and a [TestDispatcher] are passed.
187  * @throws IllegalArgumentException if a [ContinuationInterceptor] is passed that is not a [TestDispatcher].
188  */
withDelaySkippingnull189 internal fun CoroutineContext.withDelaySkipping(): CoroutineContext {
190     val dispatcher: TestDispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
191         is TestDispatcher -> {
192             val ctxScheduler = get(TestCoroutineScheduler)
193             if (ctxScheduler != null) {
194                 require(dispatcher.scheduler === ctxScheduler) {
195                     "Both a TestCoroutineScheduler $ctxScheduler and TestDispatcher $dispatcher linked to " +
196                         "another scheduler were passed."
197                 }
198             }
199             dispatcher
200         }
201         null -> StandardTestDispatcher(get(TestCoroutineScheduler))
202         else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
203     }
204     return this + dispatcher + dispatcher.scheduler
205 }
206 
207 internal class TestScopeImpl(context: CoroutineContext) :
208     AbstractCoroutine<Unit>(context, initParentJob = true, active = true), TestScope {
209 
210     override val testScheduler get() = context[TestCoroutineScheduler]!!
211 
212     private var entered = false
213     private var finished = false
214     private val uncaughtExceptions = mutableListOf<Throwable>()
215     private val lock = SynchronizedObject()
216 
217     override val backgroundScope: CoroutineScope =
<lambda>null218         CoroutineScope(coroutineContext + BackgroundWork + ReportingSupervisorJob {
219             if (it !is CancellationException) reportException(it)
220         })
221 
222     /** Called upon entry to [runTest]. Will throw if called more than once. */
enternull223     fun enter() {
224         val exceptions = synchronized(lock) {
225             if (entered)
226                 throw IllegalStateException("Only a single call to `runTest` can be performed during one test.")
227             entered = true
228             check(!finished)
229             /** the order is important: [reportException] is only guaranteed not to throw if [entered] is `true` but
230              * [finished] is `false`.
231              * However, we also want [uncaughtExceptions] to be queried after the callback is registered,
232              * because the exception collector will be able to report the exceptions that arrived before this test but
233              * after the previous one, and learning about such exceptions as soon is possible is nice. */
234             @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
235             run { ensurePlatformExceptionHandlerLoaded(ExceptionCollector) }
236             if (catchNonTestRelatedExceptions) {
237                 ExceptionCollector.addOnExceptionCallback(lock, this::reportException)
238             }
239             uncaughtExceptions
240         }
241         if (exceptions.isNotEmpty()) {
242             throw UncaughtExceptionsBeforeTest().apply {
243                 for (e in exceptions)
244                     addSuppressed(e)
245             }
246         }
247     }
248 
249     /** Called at the end of the test. May only be called once. Returns the list of caught unhandled exceptions. */
<lambda>null250     fun leave(): List<Throwable> = synchronized(lock) {
251         check(entered && !finished)
252         /** After [finished] becomes `true`, it is no longer valid to have [reportException] as the callback. */
253         ExceptionCollector.removeOnExceptionCallback(lock)
254         finished = true
255         uncaughtExceptions
256     }
257 
258     /** Called at the end of the test. May only be called once. */
legacyLeavenull259     fun legacyLeave(): List<Throwable> {
260         val exceptions = synchronized(lock) {
261             check(entered && !finished)
262             /** After [finished] becomes `true`, it is no longer valid to have [reportException] as the callback. */
263             ExceptionCollector.removeOnExceptionCallback(lock)
264             finished = true
265             uncaughtExceptions
266         }
267         val activeJobs = children.filter { it.isActive }.toList() // only non-empty if used with `runBlockingTest`
268         if (exceptions.isEmpty()) {
269             if (activeJobs.isNotEmpty())
270                 throw UncompletedCoroutinesError(
271                     "Active jobs found during the tear-down. " +
272                         "Ensure that all coroutines are completed or cancelled by your test. " +
273                         "The active jobs: $activeJobs"
274                 )
275             if (!testScheduler.isIdle())
276                 throw UncompletedCoroutinesError(
277                     "Unfinished coroutines found during the tear-down. " +
278                         "Ensure that all coroutines are completed or cancelled by your test."
279                 )
280         }
281         return exceptions
282     }
283 
284     /** Stores an exception to report after [runTest], or rethrows it if not inside [runTest]. */
reportExceptionnull285     fun reportException(throwable: Throwable) {
286         synchronized(lock) {
287             if (finished) {
288                 throw throwable
289             } else {
290                 @Suppress("INVISIBLE_MEMBER")
291                 for (existingThrowable in uncaughtExceptions) {
292                     // avoid reporting exceptions that already were reported.
293                     if (unwrap(throwable) == unwrap(existingThrowable))
294                         return
295                 }
296                 uncaughtExceptions.add(throwable)
297                 if (!entered)
298                     throw UncaughtExceptionsBeforeTest().apply { addSuppressed(throwable) }
299             }
300         }
301     }
302 
303     /** Throws an exception if the coroutine is not completing. */
tryGetCompletionCausenull304     fun tryGetCompletionCause(): Throwable? = completionCause
305 
306     override fun toString(): String =
307         "TestScope[" + (if (finished) "test ended" else if (entered) "test started" else "test not started") + "]"
308 }
309 
310 /** Use the knowledge that any [TestScope] that we receive is necessarily a [TestScopeImpl]. */
311 @Suppress("NO_ELSE_IN_WHEN") // TODO: a problem with `sealed` in MPP not allowing total pattern-matching
312 internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) {
313     is TestScopeImpl -> this
314 }
315 
316 internal class UncaughtExceptionsBeforeTest : IllegalStateException(
317     "There were uncaught exceptions before the test started. Please avoid this," +
318         " as such exceptions are also reported in a platform-dependent manner so that they are not lost."
319 )
320 
321 /**
322  * Thrown when a test has completed and there are tasks that are not completed or cancelled.
323  */
324 @ExperimentalCoroutinesApi
325 internal class UncompletedCoroutinesError(message: String) : AssertionError(message)
326 
327 /**
328  * A flag that controls whether [TestScope] should attempt to catch arbitrary exceptions flying through the system.
329  * If it is enabled, then any exception that is not caught by the user code will be reported as a test failure.
330  * By default, it is enabled, but some tests may want to disable it to test the behavior of the system when they have
331  * their own exception handling procedures.
332  */
333 @PublishedApi
334 internal var catchNonTestRelatedExceptions: Boolean = true
335