Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement new test dispatchers (#2986)
Defines two test dispatchers: * StandardTestDispatcher, which, combined with runTest, gives an illusion of an event loop; * UnconfinedTestDispatcher, which is like Dispatchers.Unconfined, but skips delays. By default, StandardTestDispatcher is used due to the somewhat chaotic execution order of Dispatchers.Unconfined. TestCoroutineDispatcher is deprecated. Fixes #1626 Fixes #1742 Fixes #2082 Fixes #2102 Fixes #2405 Fixes #2462
- Loading branch information
1 parent
888d7a0
commit 8798846
Showing
23 changed files
with
629 additions
and
147 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/* | ||
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines.test | ||
|
||
import kotlinx.coroutines.* | ||
import kotlinx.coroutines.channels.* | ||
import kotlinx.coroutines.flow.* | ||
import kotlin.coroutines.* | ||
|
||
/** | ||
* Creates an instance of an unconfined [TestDispatcher]. | ||
* | ||
* This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular | ||
* thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do. | ||
* | ||
* Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and | ||
* in which order the queued coroutines are executed. | ||
* The typical use case for this is launching child coroutines that are resumed immediately, without going through a | ||
* dispatch; this can be helpful for testing [Channel] and [StateFlow] usages. | ||
* | ||
* ``` | ||
* @Test | ||
* fun testUnconfinedDispatcher() = runTest { | ||
* val values = mutableListOf<Int>() | ||
* val stateFlow = MutableStateFlow(0) | ||
* val job = launch(UnconfinedTestDispatcher(testScheduler)) { | ||
* stateFlow.collect { | ||
* values.add(it) | ||
* } | ||
* } | ||
* stateFlow.value = 1 | ||
* stateFlow.value = 2 | ||
* stateFlow.value = 3 | ||
* job.cancel() | ||
* // each assignment will immediately resume the collecting child coroutine, | ||
* // so no values will be skipped. | ||
* assertEquals(listOf(0, 1, 2, 3), values) | ||
* } | ||
* ``` | ||
* | ||
* However, please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order | ||
* guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing | ||
* functionality, not the specific order of actions. | ||
* See [Dispatchers.Unconfined] for a discussion of the execution order guarantees. | ||
* | ||
* In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control | ||
* the virtual time and can be shared among many test dispatchers. If no [scheduler] is passed as an argument, a new one | ||
* is created. | ||
* | ||
* Additionally, [name] can be set to distinguish each dispatcher instance when debugging. | ||
* | ||
* @see StandardTestDispatcher for a more predictable [TestDispatcher]. | ||
*/ | ||
@ExperimentalCoroutinesApi | ||
@Suppress("FunctionName") | ||
public fun UnconfinedTestDispatcher( | ||
scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), | ||
name: String? = null | ||
): TestDispatcher = UnconfinedTestDispatcherImpl(scheduler, name) | ||
|
||
private class UnconfinedTestDispatcherImpl( | ||
override val scheduler: TestCoroutineScheduler, | ||
private val name: String? = null | ||
): TestDispatcher() { | ||
|
||
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false | ||
|
||
@Suppress("INVISIBLE_MEMBER") | ||
override fun dispatch(context: CoroutineContext, block: Runnable) { | ||
checkSchedulerInContext(scheduler, context) | ||
scheduler.sendDispatchEvent() | ||
|
||
/** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */ | ||
/** It can only be called by the [yield] function. See also code of [yield] function. */ | ||
val yieldContext = context[YieldContext] | ||
if (yieldContext !== null) { | ||
// report to "yield" that it is an unconfined dispatcher and don't call "block.run()" | ||
yieldContext.dispatcherWasUnconfined = true | ||
return | ||
} | ||
throw UnsupportedOperationException( | ||
"Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " + | ||
"the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " + | ||
"isDispatchNeeded and dispatch calls." | ||
) | ||
} | ||
|
||
override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]" | ||
} | ||
|
||
/** | ||
* Creates an instance of a [TestDispatcher] whose tasks are run inside calls to the [scheduler]. | ||
* | ||
* This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its | ||
* [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent], | ||
* [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these | ||
* tasks in a blocking manner. | ||
* | ||
* In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are | ||
* parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to | ||
* run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when | ||
* inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines. | ||
* | ||
* If a [scheduler] is not passed as an argument, a new one is created. | ||
* | ||
* One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging. | ||
* | ||
* @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread. | ||
*/ | ||
@Suppress("FunctionName") | ||
public fun StandardTestDispatcher( | ||
scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), | ||
name: String? = null | ||
): TestDispatcher = StandardTestDispatcherImpl(scheduler, name) | ||
|
||
private class StandardTestDispatcherImpl( | ||
override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), | ||
private val name: String? = null | ||
): TestDispatcher() { | ||
|
||
override fun dispatch(context: CoroutineContext, block: Runnable) { | ||
checkSchedulerInContext(scheduler, context) | ||
scheduler.registerEvent(this, 0, block) { false } | ||
} | ||
|
||
override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]" | ||
} |
Oops, something went wrong.