• 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 @ExperimentalCoroutinesApi
44 public sealed interface TestScope : CoroutineScope {
45     /**
46      * The delay-skipping scheduler used by the test dispatchers running the code in this scope.
47      */
48     @ExperimentalCoroutinesApi
49     public val testScheduler: TestCoroutineScheduler
50 
51     /**
52      * A scope for background work.
53      *
54      * This scope is automatically cancelled when the test finishes.
55      * Additionally, while the coroutines in this scope are run as usual when
56      * using [advanceTimeBy] and [runCurrent], [advanceUntilIdle] will stop advancing the virtual time
57      * once only the coroutines in this scope are left unprocessed.
58      *
59      * Failures in coroutines in this scope do not terminate the test.
60      * Instead, they are reported at the end of the test.
61      * Likewise, failure in the [TestScope] itself will not affect its [backgroundScope],
62      * because there's no parent-child relationship between them.
63      *
64      * A typical use case for this scope is to launch tasks that would outlive the tested code in
65      * the production environment.
66      *
67      * In this example, the coroutine that continuously sends new elements to the channel will get
68      * cancelled:
69      * ```
70      * @Test
71      * fun testExampleBackgroundJob() = runTest {
72      *   val channel = Channel<Int>()
73      *   backgroundScope.launch {
74      *     var i = 0
75      *     while (true) {
76      *       channel.send(i++)
77      *     }
78      *   }
79      *   repeat(100) {
80      *     assertEquals(it, channel.receive())
81      *   }
82      * }
83      * ```
84      */
85     @ExperimentalCoroutinesApi
86     public val backgroundScope: CoroutineScope
87 }
88 
89 /**
90  * The current virtual time on [testScheduler][TestScope.testScheduler].
91  * @see TestCoroutineScheduler.currentTime
92  */
93 @ExperimentalCoroutinesApi
94 public val TestScope.currentTime: Long
95     get() = testScheduler.currentTime
96 
97 /**
98  * Advances the [testScheduler][TestScope.testScheduler] to the point where there are no tasks remaining.
99  * @see TestCoroutineScheduler.advanceUntilIdle
100  */
101 @ExperimentalCoroutinesApi
advanceUntilIdlenull102 public fun TestScope.advanceUntilIdle(): Unit = testScheduler.advanceUntilIdle()
103 
104 /**
105  * Run any tasks that are pending at the current virtual time, according to
106  * the [testScheduler][TestScope.testScheduler].
107  *
108  * @see TestCoroutineScheduler.runCurrent
109  */
110 @ExperimentalCoroutinesApi
111 public fun TestScope.runCurrent(): Unit = testScheduler.runCurrent()
112 
113 /**
114  * Moves the virtual clock of this dispatcher forward by [the specified amount][delayTimeMillis], running the
115  * scheduled tasks in the meantime.
116  *
117  * In contrast with `TestCoroutineScope.advanceTimeBy`, this function does not run the tasks scheduled at the moment
118  * [currentTime] + [delayTimeMillis].
119  *
120  * @throws IllegalStateException if passed a negative [delay][delayTimeMillis].
121  * @see TestCoroutineScheduler.advanceTimeBy
122  */
123 @ExperimentalCoroutinesApi
124 public fun TestScope.advanceTimeBy(delayTimeMillis: Long): Unit = testScheduler.advanceTimeBy(delayTimeMillis)
125 
126 /**
127  * The [test scheduler][TestScope.testScheduler] as a [TimeSource].
128  * @see TestCoroutineScheduler.timeSource
129  */
130 @ExperimentalCoroutinesApi
131 @ExperimentalTime
132 public val TestScope.testTimeSource: TimeSource get() = testScheduler.timeSource
133 
134 /**
135  * Creates a [TestScope].
136  *
137  * It ensures that all the test module machinery is properly initialized.
138  * * If [context] doesn't provide a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
139  *   a new one is created, unless either
140  *   - a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used;
141  *   - at the moment of the creation of the scope, [Dispatchers.Main] is delegated to a [TestDispatcher], in which case
142  *     its [TestCoroutineScheduler] is used.
143  * * If [context] doesn't have a [TestDispatcher], a [StandardTestDispatcher] is created.
144  * * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
145  *   any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
146  *   already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
147  *   [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
148  *   If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
149  *   [TestCoroutineScope] and share your use case at
150  *   [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
151  * * If [context] provides a [Job], that job is used as a parent for the new scope.
152  *
153  * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
154  * different scheduler.
155  * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
156  * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
157  * [UncaughtExceptionCaptor].
158  */
159 @ExperimentalCoroutinesApi
160 @Suppress("FunctionName")
161 public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope {
162     val ctxWithDispatcher = context.withDelaySkipping()
163     var scope: TestScopeImpl? = null
164     val exceptionHandler = when (ctxWithDispatcher[CoroutineExceptionHandler]) {
165         null -> CoroutineExceptionHandler { _, exception ->
166             scope!!.reportException(exception)
167         }
168         else -> throw IllegalArgumentException(
169             "A CoroutineExceptionHandler was passed to TestScope. " +
170                 "Please pass it as an argument to a `launch` or `async` block on an already-created scope " +
171                 "if uncaught exceptions require special treatment."
172         )
173     }
174     return TestScopeImpl(ctxWithDispatcher + exceptionHandler).also { scope = it }
175 }
176 
177 /**
178  * Adds a [TestDispatcher] and a [TestCoroutineScheduler] to the context if there aren't any already.
179  *
180  * @throws IllegalArgumentException if both a [TestCoroutineScheduler] and a [TestDispatcher] are passed.
181  * @throws IllegalArgumentException if a [ContinuationInterceptor] is passed that is not a [TestDispatcher].
182  */
withDelaySkippingnull183 internal fun CoroutineContext.withDelaySkipping(): CoroutineContext {
184     val dispatcher: TestDispatcher = when (val dispatcher = get(ContinuationInterceptor)) {
185         is TestDispatcher -> {
186             val ctxScheduler = get(TestCoroutineScheduler)
187             if (ctxScheduler != null) {
188                 require(dispatcher.scheduler === ctxScheduler) {
189                     "Both a TestCoroutineScheduler $ctxScheduler and TestDispatcher $dispatcher linked to " +
190                         "another scheduler were passed."
191                 }
192             }
193             dispatcher
194         }
195         null -> StandardTestDispatcher(get(TestCoroutineScheduler))
196         else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
197     }
198     return this + dispatcher + dispatcher.scheduler
199 }
200 
201 internal class TestScopeImpl(context: CoroutineContext) :
202     AbstractCoroutine<Unit>(context, initParentJob = true, active = true), TestScope {
203 
204     override val testScheduler get() = context[TestCoroutineScheduler]!!
205 
206     private var entered = false
207     private var finished = false
208     private val uncaughtExceptions = mutableListOf<Throwable>()
209     private val lock = SynchronizedObject()
210 
211     override val backgroundScope: CoroutineScope =
<lambda>null212         CoroutineScope(coroutineContext + BackgroundWork + ReportingSupervisorJob {
213             if (it !is CancellationException) reportException(it)
214         })
215 
216     /** Called upon entry to [runTest]. Will throw if called more than once. */
enternull217     fun enter() {
218         val exceptions = synchronized(lock) {
219             if (entered)
220                 throw IllegalStateException("Only a single call to `runTest` can be performed during one test.")
221             entered = true
222             check(!finished)
223             uncaughtExceptions
224         }
225         if (exceptions.isNotEmpty()) {
226             throw UncaughtExceptionsBeforeTest().apply {
227                 for (e in exceptions)
228                     addSuppressed(e)
229             }
230         }
231     }
232 
233     /** Called at the end of the test. May only be called once. */
leavenull234     fun leave(): List<Throwable> {
235         val exceptions = synchronized(lock) {
236             check(entered && !finished)
237             finished = true
238             uncaughtExceptions
239         }
240         val activeJobs = children.filter { it.isActive }.toList() // only non-empty if used with `runBlockingTest`
241         if (exceptions.isEmpty()) {
242             if (activeJobs.isNotEmpty())
243                 throw UncompletedCoroutinesError(
244                     "Active jobs found during the tear-down. " +
245                         "Ensure that all coroutines are completed or cancelled by your test. " +
246                         "The active jobs: $activeJobs"
247                 )
248             if (!testScheduler.isIdle())
249                 throw UncompletedCoroutinesError(
250                     "Unfinished coroutines found during the tear-down. " +
251                         "Ensure that all coroutines are completed or cancelled by your test."
252                 )
253         }
254         return exceptions
255     }
256 
257     /** Stores an exception to report after [runTest], or rethrows it if not inside [runTest]. */
reportExceptionnull258     fun reportException(throwable: Throwable) {
259         synchronized(lock) {
260             if (finished) {
261                 throw throwable
262             } else {
263                 @Suppress("INVISIBLE_MEMBER")
264                 for (existingThrowable in uncaughtExceptions) {
265                     // avoid reporting exceptions that already were reported.
266                     if (unwrap(throwable) == unwrap(existingThrowable))
267                         return
268                 }
269                 uncaughtExceptions.add(throwable)
270                 if (!entered)
271                     throw UncaughtExceptionsBeforeTest().apply { addSuppressed(throwable) }
272             }
273         }
274     }
275 
276     /** Throws an exception if the coroutine is not completing. */
tryGetCompletionCausenull277     fun tryGetCompletionCause(): Throwable? = completionCause
278 
279     override fun toString(): String =
280         "TestScope[" + (if (finished) "test ended" else if (entered) "test started" else "test not started") + "]"
281 }
282 
283 /** Use the knowledge that any [TestScope] that we receive is necessarily a [TestScopeImpl]. */
284 @Suppress("NO_ELSE_IN_WHEN") // TODO: a problem with `sealed` in MPP not allowing total pattern-matching
285 internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) {
286     is TestScopeImpl -> this
287 }
288 
289 internal class UncaughtExceptionsBeforeTest : IllegalStateException(
290     "There were uncaught exceptions in coroutines launched from TestScope before the test started. Please avoid this," +
291         " as such exceptions are also reported in a platform-dependent manner so that they are not lost."
292 )
293 
294 /**
295  * Thrown when a test has completed and there are tasks that are not completed or cancelled.
296  */
297 @ExperimentalCoroutinesApi
298 internal class UncompletedCoroutinesError(message: String) : AssertionError(message)
299