• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

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