Skip to content

Commit 780860f

Browse files
committed
Allow specifying the timeout for runTest
Deprecate `dispatchTimeoutMs`, as this is a confusing implementation detail that made it to the final API. We use the fact that the `runTest(Duration)` overload was never published, so we can reuse it to have the `Duration` mean the whole-test timeout in a backward-compatible manner.
1 parent eac0b07 commit 780860f

12 files changed

+242
-103
lines changed

kotlinx-coroutines-test/api/kotlinx-coroutines-test.api

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ public final class kotlinx/coroutines/test/TestBuildersKt {
2222
public static final fun runTest (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V
2323
public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
2424
public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
25-
public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
26-
public static final fun runTest-8Mi8wO0 (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
27-
public static final fun runTest-8Mi8wO0 (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V
28-
public static synthetic fun runTest-8Mi8wO0$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
25+
public static final fun runTest-Kx4hsE0 (Lkotlin/coroutines/CoroutineContext;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;)V
26+
public static final fun runTest-Kx4hsE0 (Lkotlinx/coroutines/test/TestScope;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;)V
27+
public static synthetic fun runTest-Kx4hsE0$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
28+
public static synthetic fun runTest-Kx4hsE0$default (Lkotlinx/coroutines/test/TestScope;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
2929
public static final fun runTestWithLegacyScope (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
3030
public static synthetic fun runTestWithLegacyScope$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
3131
}
@@ -66,6 +66,7 @@ public final class kotlinx/coroutines/test/TestCoroutineScheduler : kotlin/corou
6666
public static final field Key Lkotlinx/coroutines/test/TestCoroutineScheduler$Key;
6767
public fun <init> ()V
6868
public final fun advanceTimeBy (J)V
69+
public final fun advanceTimeBy-LRDsOJo (J)V
6970
public final fun advanceUntilIdle ()V
7071
public final fun getCurrentTime ()J
7172
public final fun getTimeSource ()Lkotlin/time/TimeSource;

kotlinx-coroutines-test/common/src/TestBuilders.kt

Lines changed: 156 additions & 75 deletions
Large diffs are not rendered by default.

kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ private class UnconfinedTestDispatcherImpl(
137137
*
138138
* @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread.
139139
*/
140-
@ExperimentalCoroutinesApi
141140
@Suppress("FunctionName")
142141
public fun StandardTestDispatcher(
143142
scheduler: TestCoroutineScheduler? = null,

kotlinx-coroutines-test/common/src/TestCoroutineScheduler.kt

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import kotlin.time.*
2626
* virtual time as needed (via [advanceUntilIdle]), or run the tasks that are scheduled to run as soon as possible but
2727
* haven't yet been dispatched (via [runCurrent]).
2828
*/
29-
@ExperimentalCoroutinesApi
3029
public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCoroutineScheduler),
3130
CoroutineContext.Element {
3231

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

51+
/** A channel for notifying about the fact that a foreground work dispatch recently happened. */
52+
private val dispatchEventsForeground: Channel<Unit> = Channel(CONFLATED)
53+
5254
/** A channel for notifying about the fact that a dispatch recently happened. */
5355
private val dispatchEvents: Channel<Unit> = Channel(CONFLATED)
5456

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

115116
/**
@@ -125,7 +126,6 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
125126
/**
126127
* Runs the tasks that are scheduled to execute at this moment of virtual time.
127128
*/
128-
@ExperimentalCoroutinesApi
129129
public fun runCurrent() {
130130
val timeMark = synchronized(lock) { currentTime }
131131
while (true) {
@@ -177,6 +177,14 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
177177
}
178178
}
179179

180+
/**
181+
* Moves the virtual clock of this dispatcher forward by [the specified amount][delayTime], running the
182+
* scheduled tasks in the meantime.
183+
*
184+
* @throws IllegalStateException if passed a negative [delay][delayTime].
185+
*/
186+
public fun advanceTimeBy(delayTime: Duration): Unit = advanceTimeBy(delayTime.inWholeMicroseconds)
187+
180188
/**
181189
* Checks that the only tasks remaining in the scheduler are cancelled.
182190
*/
@@ -191,19 +199,24 @@ public class TestCoroutineScheduler : AbstractCoroutineContextElement(TestCorout
191199
* [context] is the context in which the task will be dispatched.
192200
*/
193201
internal fun sendDispatchEvent(context: CoroutineContext) {
202+
dispatchEvents.trySend(Unit)
194203
if (context[BackgroundWork] !== BackgroundWork)
195-
dispatchEvents.trySend(Unit)
204+
dispatchEventsForeground.trySend(Unit)
196205
}
197206

198207
/**
199-
* Consumes the knowledge that a dispatch event happened recently.
208+
* Waits for a notification about a dispatch event.
209+
*/
210+
internal suspend fun receiveDispatchEvent() = dispatchEvents.receive()
211+
212+
/**
213+
* Consumes the knowledge that a foreground work dispatch event happened recently.
200214
*/
201-
internal val onDispatchEvent: SelectClause1<Unit> get() = dispatchEvents.onReceive
215+
internal val onDispatchEventForeground: SelectClause1<Unit> get() = dispatchEventsForeground.onReceive
202216

203217
/**
204218
* Returns the [TimeSource] representation of the virtual time of this scheduler.
205219
*/
206-
@ExperimentalCoroutinesApi
207220
@ExperimentalTime
208221
public val timeSource: TimeSource = object : AbstractLongTimeSource(DurationUnit.MILLISECONDS) {
209222
override fun read(): Long = currentTime

kotlinx-coroutines-test/common/src/TestDispatcher.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ import kotlin.jvm.*
1616
* * [UnconfinedTestDispatcher] is a dispatcher that behaves like [Dispatchers.Unconfined] while allowing to control
1717
* the virtual time.
1818
*/
19-
@ExperimentalCoroutinesApi
2019
public abstract class TestDispatcher internal constructor() : CoroutineDispatcher(), Delay {
2120
/** The scheduler that this dispatcher is linked to. */
22-
@ExperimentalCoroutinesApi
2321
public abstract val scheduler: TestCoroutineScheduler
2422

2523
/** Notifies the dispatcher that it should process a single event marked with [marker] happening at time [time]. */

kotlinx-coroutines-test/common/src/TestScope.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,10 @@ import kotlin.time.*
4040
* paused by default, like [StandardTestDispatcher].
4141
* * No access to the list of unhandled exceptions.
4242
*/
43-
@ExperimentalCoroutinesApi
4443
public sealed interface TestScope : CoroutineScope {
4544
/**
4645
* The delay-skipping scheduler used by the test dispatchers running the code in this scope.
4746
*/
48-
@ExperimentalCoroutinesApi
4947
public val testScheduler: TestCoroutineScheduler
5048

5149
/**
@@ -82,7 +80,6 @@ public sealed interface TestScope : CoroutineScope {
8280
* }
8381
* ```
8482
*/
85-
@ExperimentalCoroutinesApi
8683
public val backgroundScope: CoroutineScope
8784
}
8885

@@ -156,7 +153,6 @@ public val TestScope.testTimeSource: TimeSource get() = testScheduler.timeSource
156153
* @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
157154
* [UncaughtExceptionCaptor].
158155
*/
159-
@ExperimentalCoroutinesApi
160156
@Suppress("FunctionName")
161157
public fun TestScope(context: CoroutineContext = EmptyCoroutineContext): TestScope {
162158
val ctxWithDispatcher = context.withDelaySkipping()

kotlinx-coroutines-test/common/test/RunTestTest.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ class RunTestTest {
7272
/** Tests that too low of a dispatch timeout causes crashes. */
7373
@Test
7474
fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
75-
assertFailsWith<UncompletedCoroutinesError> { fn() }
75+
try {
76+
fn()
77+
fail("shouldn't be reached")
78+
} catch (e: Throwable) {
79+
assertIs<UncompletedCoroutinesError>(e)
80+
}
7681
}) {
7782
runTest(dispatchTimeoutMs = 100) {
7883
withContext(Dispatchers.Default) {

kotlinx-coroutines-test/js/src/TestBuilders.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
1313
GlobalScope.promise {
1414
testProcedure()
1515
}
16+
17+
internal actual fun getLastKnownPosition(): Any? = null
18+
19+
internal actual fun dumpCoroutinesAndThrow(exception: Throwable, lastKnownPosition: Any?) {
20+
console.error(exception)
21+
throw exception
22+
}

kotlinx-coroutines-test/jvm/src/TestBuildersJvm.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package kotlinx.coroutines.test
55

66
import kotlinx.coroutines.*
7+
import kotlinx.coroutines.debug.internal.*
78

89
@Suppress("ACTUAL_WITHOUT_EXPECT")
910
public actual typealias TestResult = Unit
@@ -13,3 +14,21 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
1314
testProcedure()
1415
}
1516
}
17+
18+
internal actual fun getLastKnownPosition(): Any? = Thread.currentThread()
19+
20+
internal actual fun dumpCoroutinesAndThrow(exception: Throwable, lastKnownPosition: Any?) {
21+
val thread = lastKnownPosition as? Thread
22+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
23+
if (DebugProbesImpl.isInstalled) {
24+
DebugProbesImpl.install()
25+
try {
26+
DebugProbesImpl.dumpCoroutines(System.err)
27+
System.err.flush()
28+
} finally {
29+
DebugProbesImpl.uninstall()
30+
}
31+
}
32+
thread?.stackTrace?.let { exception.stackTrace = it }
33+
throw exception
34+
}

kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public fun runTestWithLegacyScope(
165165
throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
166166
val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest))
167167
return createTestResult {
168-
runTestCoroutine(testScope, dispatchTimeoutMs.milliseconds, TestBodyCoroutine::tryGetCompletionCause, testBody) {
168+
runTestCoroutine(testScope, dispatchTimeoutMs.milliseconds, null, TestBodyCoroutine::tryGetCompletionCause, testBody) {
169169
try {
170170
testScope.cleanup()
171171
emptyList()

kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,24 @@ class MultithreadingTest {
9999
}
100100
}
101101

102-
/** Tests that [StandardTestDispatcher] is confined to the thread that interacts with the scheduler. */
102+
/** Tests that [StandardTestDispatcher] is not executed in-place but confined to the thread in which the
103+
* virtual time control happens. */
103104
@Test
104-
fun testStandardTestDispatcherIsConfined() = runTest {
105+
fun testStandardTestDispatcherIsConfined(): Unit = runBlocking {
106+
val scheduler = TestCoroutineScheduler()
105107
val initialThread = Thread.currentThread()
106-
withContext(Dispatchers.IO) {
107-
val ioThread = Thread.currentThread()
108-
assertNotSame(initialThread, ioThread)
108+
val job = launch(StandardTestDispatcher(scheduler)) {
109+
assertEquals(initialThread, Thread.currentThread())
110+
withContext(Dispatchers.IO) {
111+
val ioThread = Thread.currentThread()
112+
assertNotSame(initialThread, ioThread)
113+
}
114+
assertEquals(initialThread, Thread.currentThread())
115+
}
116+
scheduler.advanceUntilIdle()
117+
while (job.isActive) {
118+
scheduler.receiveDispatchEvent()
119+
scheduler.advanceUntilIdle()
109120
}
110-
assertEquals(initialThread, Thread.currentThread())
111121
}
112122
}

kotlinx-coroutines-test/native/src/TestBuilders.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.coroutines.test
66
import kotlinx.coroutines.*
7+
import kotlin.native.concurrent.*
78

89
@Suppress("ACTUAL_WITHOUT_EXPECT")
910
public actual typealias TestResult = Unit
@@ -13,3 +14,12 @@ internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() ->
1314
testProcedure()
1415
}
1516
}
17+
18+
internal actual fun getLastKnownPosition(): Any? = null
19+
20+
@OptIn(ExperimentalStdlibApi::class)
21+
internal actual fun dumpCoroutinesAndThrow(exception: Throwable, lastKnownPosition: Any?) {
22+
// log exception
23+
processUnhandledException(exception)
24+
throw exception
25+
}

0 commit comments

Comments
 (0)