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

Allow specifying the timeout for runTest #3603

Merged
merged 11 commits into from
Feb 21, 2023
9 changes: 5 additions & 4 deletions kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public final class kotlinx/coroutines/test/TestBuildersKt {
public static final fun runTest (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V
public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun runTest-8Mi8wO0 (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
public static final fun runTest-8Mi8wO0 (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V
public static synthetic fun runTest-8Mi8wO0$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun runTest-Kx4hsE0 (Lkotlin/coroutines/CoroutineContext;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;)V
public static final fun runTest-Kx4hsE0 (Lkotlinx/coroutines/test/TestScope;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun runTest-Kx4hsE0$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static synthetic fun runTest-Kx4hsE0$default (Lkotlinx/coroutines/test/TestScope;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public static final fun runTestWithLegacyScope (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
public static synthetic fun runTestWithLegacyScope$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
}
Expand Down Expand Up @@ -66,6 +66,7 @@ public final class kotlinx/coroutines/test/TestCoroutineScheduler : kotlin/corou
public static final field Key Lkotlinx/coroutines/test/TestCoroutineScheduler$Key;
public fun <init> ()V
public final fun advanceTimeBy (J)V
public final fun advanceTimeBy-LRDsOJo (J)V
public final fun advanceUntilIdle ()V
public final fun getCurrentTime ()J
public final fun getTimeSource ()Lkotlin/time/TimeSource;
Expand Down
231 changes: 156 additions & 75 deletions kotlinx-coroutines-test/common/src/TestBuilders.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ private class UnconfinedTestDispatcherImpl(
*
* @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread.
*/
@ExperimentalCoroutinesApi
@Suppress("FunctionName")
public fun StandardTestDispatcher(
scheduler: TestCoroutineScheduler? = null,
Expand Down
31 changes: 22 additions & 9 deletions kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import kotlin.time.*
* virtual time as needed (via [advanceUntilIdle]), or run the tasks that are scheduled to run as soon as possible but
* haven't yet been dispatched (via [runCurrent]).
*/
@ExperimentalCoroutinesApi
public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCoroutineScheduler),
CoroutineContext.Element {

Expand All @@ -49,6 +48,9 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
get() = synchronized(lock) { field }
private set

/** A channel for notifying about the fact that a foreground work dispatch recently happened. */
private val dispatchEventsForeground: Channel<Unit> = Channel(CONFLATED)

/** A channel for notifying about the fact that a dispatch recently happened. */
private val dispatchEvents: Channel<Unit> = Channel(CONFLATED)

Expand All @@ -73,8 +75,8 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
val time = addClamping(currentTime, timeDeltaMillis)
val event = TestDispatchEvent(dispatcher, count, time, marker as Any, isForeground) { isCancelled(marker) }
events.addLast(event)
/** can't be moved above: otherwise, [onDispatchEvent] could consume the token sent here before there's
* actually anything in the event queue. */
/** can't be moved above: otherwise, [onDispatchEventForeground] or [receiveDispatchEvent] could consume the
* token sent here before there's actually anything in the event queue. */
sendDispatchEvent(context)
DisposableHandle {
synchronized(lock) {
Expand Down Expand Up @@ -109,7 +111,6 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
* milliseconds by which the execution of this method has advanced the virtual time. If you want to recreate that
* functionality, query [currentTime] before and after the execution to achieve the same result.
*/
@ExperimentalCoroutinesApi
public fun advanceUntilIdle(): Unit = advanceUntilIdleOr { events.none(TestDispatchEvent<*>::isForeground) }

/**
Expand All @@ -125,7 +126,6 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
/**
* Runs the tasks that are scheduled to execute at this moment of virtual time.
*/
@ExperimentalCoroutinesApi
public fun runCurrent() {
val timeMark = synchronized(lock) { currentTime }
while (true) {
Expand Down Expand Up @@ -177,6 +177,14 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
}
}

/**
* Moves the virtual clock of this dispatcher forward by [the specified amount][delayTime], running the
* scheduled tasks in the meantime.
*
* @throws IllegalStateException if passed a negative [delay][delayTime].
*/
public fun advanceTimeBy(delayTime: Duration): Unit = advanceTimeBy(delayTime.inWholeMicroseconds)
dkhalanskyjb marked this conversation as resolved.
Show resolved Hide resolved

/**
* Checks that the only tasks remaining in the scheduler are cancelled.
*/
Expand All @@ -191,19 +199,24 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
* [context] is the context in which the task will be dispatched.
*/
internal fun sendDispatchEvent(context: CoroutineContext) {
dispatchEvents.trySend(Unit)
if (context[BackgroundWork] !== BackgroundWork)
dispatchEvents.trySend(Unit)
dispatchEventsForeground.trySend(Unit)
}

/**
* Consumes the knowledge that a dispatch event happened recently.
* Waits for a notification about a dispatch event.
*/
internal suspend fun receiveDispatchEvent() = dispatchEvents.receive()

/**
* Consumes the knowledge that a foreground work dispatch event happened recently.
*/
internal val onDispatchEvent: SelectClause1<Unit> get() = dispatchEvents.onReceive
internal val onDispatchEventForeground: SelectClause1<Unit> get() = dispatchEventsForeground.onReceive

/**
* Returns the [TimeSource] representation of the virtual time of this scheduler.
*/
@ExperimentalCoroutinesApi
@ExperimentalTime
public val timeSource: TimeSource = object : AbstractLongTimeSource(DurationUnit.MILLISECONDS) {
override fun read(): Long = currentTime
Expand Down
2 changes: 0 additions & 2 deletions kotlinx-coroutines-test/common/src/TestDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ import kotlin.jvm.*
* * [UnconfinedTestDispatcher] is a dispatcher that behaves like [Dispatchers.Unconfined] while allowing to control
* the virtual time.
*/
@ExperimentalCoroutinesApi
public abstract class TestDispatcher internal constructor() : CoroutineDispatcher(), Delay {
/** The scheduler that this dispatcher is linked to. */
@ExperimentalCoroutinesApi
public abstract val scheduler: TestCoroutineScheduler

/** Notifies the dispatcher that it should process a single event marked with [marker] happening at time [time]. */
Expand Down
4 changes: 0 additions & 4 deletions kotlinx-coroutines-test/common/src/TestScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,10 @@ import kotlin.time.*
* paused by default, like [StandardTestDispatcher].
* * No access to the list of unhandled exceptions.
*/
@ExperimentalCoroutinesApi
public sealed interface TestScope : CoroutineScope {
/**
* The delay-skipping scheduler used by the test dispatchers running the code in this scope.
*/
@ExperimentalCoroutinesApi
public val testScheduler: TestCoroutineScheduler

/**
Expand Down Expand Up @@ -82,7 +80,6 @@ public sealed interface TestScope : CoroutineScope {
* }
* ```
*/
@ExperimentalCoroutinesApi
public val backgroundScope: CoroutineScope
}

Expand Down Expand Up @@ -156,7 +153,6 @@ public val TestScope.testTimeSource: TimeSource get() = testScheduler.timeSource
* @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
* [UncaughtExceptionCaptor].
*/
@ExperimentalCoroutinesApi
@Suppress("FunctionName")
public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope {
val ctxWithDispatcher = context.withDelaySkipping()
Expand Down
7 changes: 6 additions & 1 deletion kotlinx-coroutines-test/common/test/RunTestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ class RunTestTest {
/** Tests that too low of a dispatch timeout causes crashes. */
@Test
fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
assertFailsWith<UncompletedCoroutinesError> { fn() }
try {
fn()
fail("shouldn't be reached")
} catch (e: Throwable) {
assertIs<UncompletedCoroutinesError>(e)
}
}) {
runTest(dispatchTimeoutMs = 100) {
withContext(Dispatchers.Default) {
Expand Down
7 changes: 7 additions & 0 deletions kotlinx-coroutines-test/js/src/TestBuilders.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
GlobalScope.promise {
testProcedure()
}

internal actual fun getLastKnownPosition(): Any? = null

internal actual fun dumpCoroutinesAndThrow(exception: Throwable, lastKnownPosition: Any?) {
console.error(exception)
throw exception
}
19 changes: 19 additions & 0 deletions kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package kotlinx.coroutines.test

import kotlinx.coroutines.*
import kotlinx.coroutines.debug.internal.*

@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias TestResult = Unit
Expand All @@ -13,3 +14,21 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
testProcedure()
}
}

internal actual fun getLastKnownPosition(): Any? = Thread.currentThread()

internal actual fun dumpCoroutinesAndThrow(exception: Throwable, lastKnownPosition: Any?) {
val thread = lastKnownPosition as? Thread
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
if (DebugProbesImpl.isInstalled) {
DebugProbesImpl.install()
try {
DebugProbesImpl.dumpCoroutines(System.err)
System.err.flush()
} finally {
DebugProbesImpl.uninstall()
}
}
thread?.stackTrace?.let { exception.stackTrace = it }
throw exception
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public fun runTestWithLegacyScope(
throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest))
return createTestResult {
runTestCoroutine(testScope, dispatchTimeoutMs.milliseconds, TestBodyCoroutine::tryGetCompletionCause, testBody) {
runTestCoroutine(testScope, dispatchTimeoutMs.milliseconds, null, TestBodyCoroutine::tryGetCompletionCause, testBody) {
try {
testScope.cleanup()
emptyList()
Expand Down
22 changes: 16 additions & 6 deletions kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,24 @@ class MultithreadingTest {
}
}

/** Tests that [StandardTestDispatcher] is confined to the thread that interacts with the scheduler. */
/** Tests that [StandardTestDispatcher] is not executed in-place but confined to the thread in which the
* virtual time control happens. */
@Test
fun testStandardTestDispatcherIsConfined() = runTest {
fun testStandardTestDispatcherIsConfined(): Unit = runBlocking {
val scheduler = TestCoroutineScheduler()
val initialThread = Thread.currentThread()
withContext(Dispatchers.IO) {
val ioThread = Thread.currentThread()
assertNotSame(initialThread, ioThread)
val job = launch(StandardTestDispatcher(scheduler)) {
assertEquals(initialThread, Thread.currentThread())
withContext(Dispatchers.IO) {
val ioThread = Thread.currentThread()
assertNotSame(initialThread, ioThread)
}
assertEquals(initialThread, Thread.currentThread())
}
scheduler.advanceUntilIdle()
while (job.isActive) {
scheduler.receiveDispatchEvent()
scheduler.advanceUntilIdle()
}
assertEquals(initialThread, Thread.currentThread())
}
}
10 changes: 10 additions & 0 deletions kotlinx-coroutines-test/native/src/TestBuilders.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package kotlinx.coroutines.test
import kotlinx.coroutines.*
import kotlin.native.concurrent.*

@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias TestResult = Unit
Expand All @@ -13,3 +14,12 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
testProcedure()
}
}

internal actual fun getLastKnownPosition(): Any? = null

@OptIn(ExperimentalStdlibApi::class)
internal actual fun dumpCoroutinesAndThrow(exception: Throwable, lastKnownPosition: Any?) {
// log exception
processUnhandledException(exception)
throw exception
}