|
| 1 | +/* |
| 2 | + * 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.channels.* |
| 9 | +import kotlinx.coroutines.flow.* |
| 10 | +import kotlin.coroutines.* |
| 11 | + |
| 12 | +/** |
| 13 | + * Creates an instance of an unconfined [TestDispatcher]. |
| 14 | + * |
| 15 | + * This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular |
| 16 | + * thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do. |
| 17 | + * |
| 18 | + * Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and |
| 19 | + * in which order the queued coroutines are executed. |
| 20 | + * The typical use case for this is launching child coroutines that are resumed immediately, without going through a |
| 21 | + * dispatch; this can be helpful for testing [Channel] and [StateFlow] usages. |
| 22 | + * |
| 23 | + * ``` |
| 24 | + * @Test |
| 25 | + * fun testUnconfinedDispatcher() = runTest { |
| 26 | + * val values = mutableListOf<Int>() |
| 27 | + * val stateFlow = MutableStateFlow(0) |
| 28 | + * val job = launch(UnconfinedTestDispatcher(testScheduler)) { |
| 29 | + * stateFlow.collect { |
| 30 | + * values.add(it) |
| 31 | + * } |
| 32 | + * } |
| 33 | + * stateFlow.value = 1 |
| 34 | + * stateFlow.value = 2 |
| 35 | + * stateFlow.value = 3 |
| 36 | + * job.cancel() |
| 37 | + * // each assignment will immediately resume the collecting child coroutine, |
| 38 | + * // so no values will be skipped. |
| 39 | + * assertEquals(listOf(0, 1, 2, 3), values) |
| 40 | + * } |
| 41 | + * ``` |
| 42 | + * |
| 43 | + * However, please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order |
| 44 | + * guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing |
| 45 | + * functionality, not the specific order of actions. |
| 46 | + * See [Dispatchers.Unconfined] for a discussion of the execution order guarantees. |
| 47 | + * |
| 48 | + * In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control |
| 49 | + * the virtual time and can be shared among many test dispatchers. If no [scheduler] is passed as an argument, a new one |
| 50 | + * is created. |
| 51 | + * |
| 52 | + * Additionally, [name] can be set to distinguish each dispatcher instance when debugging. |
| 53 | + * |
| 54 | + * @see StandardTestDispatcher for a more predictable [TestDispatcher]. |
| 55 | + */ |
| 56 | +@ExperimentalCoroutinesApi |
| 57 | +@Suppress("FunctionName") |
| 58 | +public fun UnconfinedTestDispatcher( |
| 59 | + scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), |
| 60 | + name: String? = null |
| 61 | +): TestDispatcher = UnconfinedTestDispatcherImpl(scheduler, name) |
| 62 | + |
| 63 | +private class UnconfinedTestDispatcherImpl( |
| 64 | + override val scheduler: TestCoroutineScheduler, |
| 65 | + private val name: String? = null |
| 66 | +): TestDispatcher() { |
| 67 | + |
| 68 | + override fun isDispatchNeeded(context: CoroutineContext): Boolean = false |
| 69 | + |
| 70 | + @Suppress("INVISIBLE_MEMBER") |
| 71 | + override fun dispatch(context: CoroutineContext, block: Runnable) { |
| 72 | + checkSchedulerInContext(scheduler, context) |
| 73 | + scheduler.sendDispatchEvent() |
| 74 | + |
| 75 | + /** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */ |
| 76 | + /** It can only be called by the [yield] function. See also code of [yield] function. */ |
| 77 | + val yieldContext = context[YieldContext] |
| 78 | + if (yieldContext !== null) { |
| 79 | + // report to "yield" that it is an unconfined dispatcher and don't call "block.run()" |
| 80 | + yieldContext.dispatcherWasUnconfined = true |
| 81 | + return |
| 82 | + } |
| 83 | + throw UnsupportedOperationException( |
| 84 | + "Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " + |
| 85 | + "the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " + |
| 86 | + "isDispatchNeeded and dispatch calls." |
| 87 | + ) |
| 88 | + } |
| 89 | + |
| 90 | + override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]" |
| 91 | +} |
| 92 | + |
| 93 | +/** |
| 94 | + * Creates an instance of a [TestDispatcher] whose tasks are run inside calls to the [scheduler]. |
| 95 | + * |
| 96 | + * This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its |
| 97 | + * [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent], |
| 98 | + * [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these |
| 99 | + * tasks in a blocking manner. |
| 100 | + * |
| 101 | + * In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are |
| 102 | + * parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to |
| 103 | + * run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when |
| 104 | + * inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines. |
| 105 | + * |
| 106 | + * If a [scheduler] is not passed as an argument, a new one is created. |
| 107 | + * |
| 108 | + * One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging. |
| 109 | + * |
| 110 | + * @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread. |
| 111 | + */ |
| 112 | +@Suppress("FunctionName") |
| 113 | +public fun StandardTestDispatcher( |
| 114 | + scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), |
| 115 | + name: String? = null |
| 116 | +): TestDispatcher = StandardTestDispatcherImpl(scheduler, name) |
| 117 | + |
| 118 | +private class StandardTestDispatcherImpl( |
| 119 | + override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), |
| 120 | + private val name: String? = null |
| 121 | +): TestDispatcher() { |
| 122 | + |
| 123 | + override fun dispatch(context: CoroutineContext, block: Runnable) { |
| 124 | + checkSchedulerInContext(scheduler, context) |
| 125 | + scheduler.registerEvent(this, 0, block) { false } |
| 126 | + } |
| 127 | + |
| 128 | + override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]" |
| 129 | +} |
0 commit comments