Skip to content

Commit f2ab7d0

Browse files
committed
Prevent nested 'runTest'
1 parent 0df38d9 commit f2ab7d0

File tree

3 files changed

+41
-6
lines changed

3 files changed

+41
-6
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ public final class kotlinx/coroutines/test/TestBuildersKt {
1515
public static final fun runBlockingTest (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function2;)V
1616
public static synthetic fun runBlockingTest$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
1717
public static final fun runTest (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V
18+
public static final fun runTest (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;)V
19+
public static final fun runTest (Lkotlinx/coroutines/test/TestDispatcher;JLkotlin/jvm/functions/Function2;)V
1820
public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
21+
public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
22+
public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestDispatcher;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
1923
}
2024

2125
public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/coroutines/test/TestDispatcher, kotlinx/coroutines/Delay, kotlinx/coroutines/test/SchedulerAsDelayController {

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

+30-6
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import kotlin.coroutines.*
4242
* then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
4343
* @param testBody The code of the unit-test.
4444
*/
45-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
4645
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
4746
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
4847
val scope = TestCoroutineScope(context)
@@ -61,14 +60,14 @@ public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, te
6160
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
6261
*/
6362
// todo: need documentation on how this extension is supposed to be used
64-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
63+
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
6564
public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
6665
runBlockingTest(coroutineContext, block)
6766

6867
/**
6968
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
7069
*/
71-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
70+
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
7271
public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
7372
runBlockingTest(this, block)
7473

@@ -164,10 +163,10 @@ public expect class TestResult
164163
*/
165164
public fun runTest(
166165
context: CoroutineContext = EmptyCoroutineContext,
167-
dispatchTimeoutMs: Long = 10_000,
166+
dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
168167
testBody: suspend TestCoroutineScope.() -> Unit
169168
): TestResult = createTestResult {
170-
val testScope = TestCoroutineScope(context)
169+
val testScope = TestCoroutineScope(context + RunningInRunTest())
171170
val scheduler = testScope.testScheduler
172171
val deferred = testScope.async {
173172
testScope.testBody()
@@ -206,4 +205,29 @@ public fun runTest(
206205
* Runs [testProcedure], creating a [TestResult].
207206
*/
208207
@Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult`
209-
internal expect fun createTestResult(testProcedure: suspend () -> Unit): TestResult
208+
internal expect fun createTestResult(testProcedure: suspend () -> Unit): TestResult
209+
210+
/** TODO: docs */
211+
public fun TestCoroutineScope.runTest(
212+
dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
213+
block: suspend TestCoroutineScope.() -> Unit
214+
): TestResult {
215+
val ctx = this.coroutineContext
216+
if (ctx[RunningInRunTest] != null)
217+
throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
218+
return runTest(ctx, dispatchTimeoutMs, block)
219+
}
220+
221+
/** TODO: docs */
222+
public fun TestDispatcher.runTest(
223+
dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
224+
block: suspend TestCoroutineScope.() -> Unit
225+
): TestResult =
226+
runTest(this, dispatchTimeoutMs, block)
227+
228+
/** A coroutine context element indicating that the coroutine is running inside `runTest`. */
229+
private class RunningInRunTest: AbstractCoroutineContextElement(RunningInRunTest), CoroutineContext.Element {
230+
companion object Key : CoroutineContext.Key<RunningInRunTest>
231+
}
232+
233+
private const val DEFAULT_DISPATCH_TIMEOUT_MS = 10_000L

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

+7
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@ class TestRunTest {
3838
assertEquals(42, answer)
3939
}
4040

41+
@Test
42+
fun testNestedRunTestForbidden() = runTest {
43+
assertFailsWith<IllegalStateException> {
44+
runTest { }
45+
}
46+
}
47+
4148
}

0 commit comments

Comments
 (0)