Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new test dispatchers #2986

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Expand Up @@ -545,6 +545,15 @@ public final class kotlinx/coroutines/TimeoutKt {
public static final fun withTimeoutOrNull-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class kotlinx/coroutines/YieldContext : kotlin/coroutines/AbstractCoroutineContextElement {
public static final field Key Lkotlinx/coroutines/YieldContext$Key;
public field dispatcherWasUnconfined Z
public fun <init> ()V
}

public final class kotlinx/coroutines/YieldContext$Key : kotlin/coroutines/CoroutineContext$Key {
}

public final class kotlinx/coroutines/YieldKt {
public static final fun yield (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
Expand Down
1 change: 1 addition & 0 deletions kotlinx-coroutines-core/common/src/Unconfined.kt
Expand Up @@ -38,6 +38,7 @@ internal object Unconfined : CoroutineDispatcher() {
/**
* Used to detect calls to [Unconfined.dispatch] from [yield] function.
*/
@PublishedApi
internal class YieldContext : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<YieldContext>

Expand Down
10 changes: 9 additions & 1 deletion kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
Expand Up @@ -40,6 +40,13 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor
public fun toString ()Ljava/lang/String;
}

public final class kotlinx/coroutines/test/TestCoroutineDispatchersKt {
public static final fun StandardTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher;
public static synthetic fun StandardTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher;
public static final fun UnconfinedTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher;
public static synthetic fun UnconfinedTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher;
}

public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor {
public fun <init> ()V
public fun cleanupTestCoroutinesCaptor ()V
Expand Down Expand Up @@ -69,6 +76,8 @@ public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestCoroutineScope;J)V
public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestCoroutineScope;)V
public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J
public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand All @@ -77,7 +86,6 @@ public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
}

public abstract class kotlinx/coroutines/test/TestDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay {
public fun <init> ()V
public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle;
Expand Down
20 changes: 15 additions & 5 deletions kotlinx-coroutines-test/common/src/DelayController.kt
@@ -1,6 +1,7 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("DEPRECATION")

package kotlinx.coroutines.test

Expand Down Expand Up @@ -102,7 +103,10 @@ public interface DelayController {
* This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
* setup may be done between the time the coroutine is created and started.
*/
@ExperimentalCoroutinesApi
@Deprecated(
"Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
level = DeprecationLevel.WARNING
)
public suspend fun pauseDispatcher(block: suspend () -> Unit)

/**
Expand All @@ -111,7 +115,10 @@ public interface DelayController {
* When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
* [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
*/
@ExperimentalCoroutinesApi
@Deprecated(
"Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
level = DeprecationLevel.WARNING
)
public fun pauseDispatcher()

/**
Expand All @@ -121,12 +128,15 @@ public interface DelayController {
* time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
* or [advanceUntilIdle].
*/
@ExperimentalCoroutinesApi
@Deprecated(
"Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
level = DeprecationLevel.WARNING
)
public fun resumeDispatcher()
}

internal interface SchedulerAsDelayController : DelayController {
public val scheduler: TestCoroutineScheduler
val scheduler: TestCoroutineScheduler

/** @suppress */
@Deprecated(
Expand Down Expand Up @@ -178,7 +188,7 @@ internal interface SchedulerAsDelayController : DelayController {
scheduler.runCurrent()
if (!scheduler.isIdle()) {
throw UncompletedCoroutinesError(
"Unfinished coroutines during teardown. Ensure all coroutines are" +
"Unfinished coroutines during tear-down. Ensure all coroutines are" +
" completed or cancelled by your test."
)
}
Expand Down
9 changes: 4 additions & 5 deletions kotlinx-coroutines-test/common/src/TestBuilders.kt
Expand Up @@ -44,7 +44,7 @@ import kotlin.coroutines.*
*/
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
val scope = TestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
val scheduler = scope.testScheduler
val deferred = scope.async {
scope.testBody()
Expand Down Expand Up @@ -197,10 +197,9 @@ public expect class TestResult
*
* [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
* scope created for the test, [context] also can be used to change how the test is executed.
* See the [TestCoroutineScope] constructor documentation for details.
* See the [createTestCoroutineScope] documentation for details.
*
* @throws IllegalArgumentException if the [context] is invalid. See the [TestCoroutineScope] constructor docs for
* details.
* @throws IllegalArgumentException if the [context] is invalid. See the [createTestCoroutineScope] docs for details.
*/
@ExperimentalCoroutinesApi
public fun runTest(
Expand All @@ -210,7 +209,7 @@ public fun runTest(
): TestResult {
if (context[RunningInRunTest] != null)
throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
val testScope = TestBodyCoroutine<Unit>(TestCoroutineScope(context + RunningInRunTest))
val testScope = TestBodyCoroutine<Unit>(createTestCoroutineScope(context + RunningInRunTest))
val scheduler = testScope.testScheduler
return createTestResult {
/** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
Expand Down
10 changes: 3 additions & 7 deletions kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt
Expand Up @@ -21,7 +21,9 @@ import kotlin.coroutines.*
*
* @see DelayController
*/
@ExperimentalCoroutinesApi
@Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of " +
"pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.",
level = DeprecationLevel.WARNING)
public class TestCoroutineDispatcher(public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler()):
TestDispatcher(), Delay, SchedulerAsDelayController
{
Expand All @@ -34,12 +36,6 @@ public class TestCoroutineDispatcher(public override val scheduler: TestCoroutin
}
}

/** @suppress */
override fun processEvent(time: Long, marker: Any) {
check(marker is Runnable)
marker.run()
}

/** @suppress */
override fun dispatch(context: CoroutineContext, block: Runnable) {
checkSchedulerInContext(scheduler, context)
Expand Down
129 changes: 129 additions & 0 deletions kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt
@@ -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")
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
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]"
}