Skip to content

Update TestCoroutineContext to support structured concurrency. #890

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

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
public abstract interface class kotlinx/coroutines/test/DelayController {
public abstract fun advanceTimeBy (JLjava/util/concurrent/TimeUnit;)J
public abstract fun advanceTimeToNextDelayed ()J
public abstract fun advanceUntilIdle ()J
public abstract fun cleanupTestCoroutines ()V
public abstract fun currentTime (Ljava/util/concurrent/TimeUnit;)J
public abstract fun pauseDispatcher ()V
public abstract fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun resumeDispatcher ()V
public abstract fun runCurrent ()V
}

public final class kotlinx/coroutines/test/DelayController$DefaultImpls {
public static synthetic fun advanceTimeBy$default (Lkotlinx/coroutines/test/DelayController;JLjava/util/concurrent/TimeUnit;ILjava/lang/Object;)J
public static synthetic fun currentTime$default (Lkotlinx/coroutines/test/DelayController;Ljava/util/concurrent/TimeUnit;ILjava/lang/Object;)J
}

public final class kotlinx/coroutines/test/TestBuilders {
public static final fun runBlockingTest (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineDispatcher;Lkotlin/jvm/functions/Function2;)V
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun runBlockingTest$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun withTestContext (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function2;)V
}

public final class kotlinx/coroutines/test/TestCoroutineCoroutineExceptionHandler : kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor {
public fun <init> ()V
public fun cleanupTestCoroutines ()V
public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element;
public synthetic fun getKey ()Lkotlin/coroutines/CoroutineContext$Key;
public fun getKey ()Lkotlinx/coroutines/CoroutineExceptionHandler$Key;
public fun getUncaughtExceptions ()Ljava/util/List;
public fun handleException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V
public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext;
public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext;
}

public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/DelayController {
public fun <init> ()V
public fun advanceTimeBy (JLjava/util/concurrent/TimeUnit;)J
public fun advanceTimeToNextDelayed ()J
public fun advanceUntilIdle ()J
public fun cleanupTestCoroutines ()V
public fun currentTime (Ljava/util/concurrent/TimeUnit;)J
public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle;
public fun pauseDispatcher ()V
public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resumeDispatcher ()V
public fun runCurrent ()V
public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V
public fun toString ()Ljava/lang/String;
}

public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope, kotlinx/coroutines/test/DelayController, kotlinx/coroutines/test/UncaughtExceptionCaptor {
}

public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
public static final fun TestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
}

public final class kotlinx/coroutines/test/TestDispatchers {
public static final fun resetMain (Lkotlinx/coroutines/Dispatchers;)V
public static final fun setMain (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;)V
}

public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
public abstract fun cleanupTestCoroutines ()V
public abstract fun getUncaughtExceptions ()Ljava/util/List;
}

public final class kotlinx/coroutines/test/UncompletedCoroutinesError : java/lang/AssertionError {
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}

10 changes: 7 additions & 3 deletions core/kotlinx-coroutines-core/src/test_/TestCoroutineContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import kotlin.coroutines.*
*
* @param name A user-readable name for debugging purposes.
*/
@ObsoleteCoroutinesApi
@Deprecated("This API has been deprecated to integrate with Structured Concurrency.",
ReplaceWith("TestCoroutineScope", "kotlin.coroutines.test"),
level = DeprecationLevel.WARNING)
class TestCoroutineContext(private val name: String? = null) : CoroutineContext {
private val uncaughtExceptions = mutableListOf<Throwable>()

Expand Down Expand Up @@ -281,12 +283,14 @@ private class TimedRunnable(
* provided instead.
* @param testBody The code of the unit-test.
*/
@ObsoleteCoroutinesApi
@Deprecated("This API has been deprecated to integrate with Structured Concurrency.",
ReplaceWith("testContext.runBlockingTest(testBody)", "kotlin.coroutines.test"),
level = DeprecationLevel.WARNING)
public fun withTestContext(testContext: TestCoroutineContext = TestCoroutineContext(), testBody: TestCoroutineContext.() -> Unit) {
with (testContext) {
testBody()
if (!exceptions.all { it is CancellationException }) {
throw AssertionError("Coroutine encountered unhandled exceptions:\n$exceptions")
}
}
}
}
118 changes: 118 additions & 0 deletions core/kotlinx-coroutines-test/src/TestBuilders.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
@file:JvmName("TestBuilders")

package kotlinx.coroutines.test

import kotlinx.coroutines.*
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext

/**
* Executes a [testBody] inside an immediate execution dispatcher.
*
* This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
* You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
* extra time.
**
* ```
* @Test
* fun exampleTest() = runBlockingTest {
* val deferred = async {
* delay(1_000)
* async {
* delay(1_000)
* }.await()
* }
*
* deferred.await() // result available immediately
* }
*
* ```
*
* This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
* conditions.
*
* Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
*
* @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
* (including coroutines suspended on join/await).
*
* @param context An optional context that MUST contain a [DelayController] and/or [TestCoroutineExceptionHandler]
* @param testBody The code of the unit-test.
*/
@ExperimentalCoroutinesApi
fun runBlockingTest(context: CoroutineContext? = null, testBody: suspend TestCoroutineScope.() -> Unit) {
val (safeContext, dispatcher) = context.checkArguments()
// smart cast dispatcher to expose interface
dispatcher as DelayController

val startingJobs = safeContext.activeJobs()

val scope = TestCoroutineScope(safeContext)
val deferred = scope.async {
scope.testBody()
}
dispatcher.advanceUntilIdle()
deferred.getCompletionExceptionOrNull()?.let {
throw it
}
scope.cleanupTestCoroutines()
val endingJobs = safeContext.activeJobs()
if ((endingJobs - startingJobs).isNotEmpty()) {
throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs")
}
}

private fun CoroutineContext.activeJobs(): Set<Job> {
return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
}

/**
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
*/
@ExperimentalCoroutinesApi
fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
runBlockingTest(coroutineContext, block)
}

/**
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
*/
@ExperimentalCoroutinesApi
fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
runBlockingTest(this, block)
}

private fun CoroutineContext?.checkArguments(): Pair<CoroutineContext, ContinuationInterceptor> {
var safeContext = this ?: TestCoroutineExceptionHandler() + TestCoroutineDispatcher()

val dispatcher = safeContext[ContinuationInterceptor].run {
this?.let {
require(this is DelayController) { "Dispatcher must implement DelayController" }
}
this ?: TestCoroutineDispatcher()
}

val exceptionHandler = safeContext[CoroutineExceptionHandler].run {
this?.let {
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor" }
}
this ?: TestCoroutineExceptionHandler()
}

val job = safeContext[Job] ?: SupervisorJob()

safeContext = safeContext + dispatcher + exceptionHandler + job
return Pair(safeContext, dispatcher)
}

/**
* This method is deprecated.
*
* @see [runBlocking]
*/
@Deprecated("This API has been deprecated to integrate with Structured Concurrency.",
ReplaceWith("scope.runBlockingTest(testBody)", "kotlinx.coroutines.test"),
level = DeprecationLevel.ERROR)
fun withTestContext(scope: TestCoroutineScope, testBody: suspend TestCoroutineScope.() -> Unit) {
scope.runBlockingTest(testBody)
}
Loading