Skip to content

Commit cf63d45

Browse files
committed
Update exception handling in the test module (#2953)
1 parent 6cefa2f commit cf63d45

10 files changed

+296
-78
lines changed

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatchersKt {
4949

5050
public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor {
5151
public fun <init> ()V
52-
public fun cleanupTestCoroutinesCaptor ()V
52+
public fun cleanupTestCoroutines ()V
5353
public fun getUncaughtExceptions ()Ljava/util/List;
5454
public fun handleException (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Throwable;)V
5555
}
@@ -66,7 +66,7 @@ public final class kotlinx/coroutines/test/TestCoroutineScheduler : kotlin/corou
6666
public final class kotlinx/coroutines/test/TestCoroutineScheduler$Key : kotlin/coroutines/CoroutineContext$Key {
6767
}
6868

69-
public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope, kotlinx/coroutines/test/UncaughtExceptionCaptor {
69+
public abstract interface class kotlinx/coroutines/test/TestCoroutineScope : kotlinx/coroutines/CoroutineScope {
7070
public abstract fun cleanupTestCoroutines ()V
7171
public abstract fun getTestScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler;
7272
}
@@ -79,6 +79,7 @@ public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
7979
public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope;
8080
public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope;
8181
public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J
82+
public static final fun getUncaughtExceptions (Lkotlinx/coroutines/test/TestCoroutineScope;)Ljava/util/List;
8283
public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
8384
public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
8485
public static final fun resumeDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V
@@ -98,7 +99,7 @@ public final class kotlinx/coroutines/test/TestDispatchers {
9899
}
99100

100101
public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
101-
public abstract fun cleanupTestCoroutinesCaptor ()V
102+
public abstract fun cleanupTestCoroutines ()V
102103
public abstract fun getUncaughtExceptions ()Ljava/util/List;
103104
}
104105

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

+22-10
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ import kotlin.coroutines.*
4343
* @param testBody The code of the unit-test.
4444
*/
4545
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
46-
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
46+
public fun runBlockingTest(
47+
context: CoroutineContext = EmptyCoroutineContext,
48+
testBody: suspend TestCoroutineScope.() -> Unit
49+
) {
4750
val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
4851
val scheduler = scope.testScheduler
4952
val deferred = scope.async {
@@ -235,7 +238,7 @@ public fun runTest(
235238
}
236239
onTimeout(dispatchTimeoutMs) {
237240
try {
238-
testScope.cleanupTestCoroutines()
241+
testScope.cleanup()
239242
} catch (e: UncompletedCoroutinesError) {
240243
// we expect these and will instead throw a more informative exception just below.
241244
}
@@ -245,15 +248,15 @@ public fun runTest(
245248
}
246249
testScope.getCompletionExceptionOrNull()?.let {
247250
try {
248-
testScope.cleanupTestCoroutines()
251+
testScope.cleanup()
249252
} catch (e: UncompletedCoroutinesError) {
250253
// it's normal that some jobs are not completed if the test body has failed, won't clutter the output
251254
} catch (e: Throwable) {
252255
it.addSuppressed(e)
253256
}
254257
throw it
255258
}
256-
testScope.cleanupTestCoroutines()
259+
testScope.cleanup()
257260
}
258261
}
259262

@@ -266,7 +269,7 @@ internal expect fun createTestResult(testProcedure: suspend () -> Unit): TestRes
266269
/**
267270
* Runs a test in a [TestCoroutineScope] based on this one.
268271
*
269-
* Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run
272+
* Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the
270273
* [block] will be different from this one, but will use its [Job] as a parent.
271274
*
272275
* Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
@@ -295,7 +298,7 @@ public fun TestDispatcher.runTest(
295298
runTest(this, dispatchTimeoutMs, block)
296299

297300
/** A coroutine context element indicating that the coroutine is running inside `runTest`. */
298-
private object RunningInRunTest: CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
301+
private object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
299302
override val key: CoroutineContext.Key<*>
300303
get() = this
301304

@@ -308,11 +311,20 @@ private const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
308311

309312
private class TestBodyCoroutine<T>(
310313
private val testScope: TestCoroutineScope,
311-
) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope,
312-
UncaughtExceptionCaptor by testScope.coroutineContext.uncaughtExceptionCaptor
313-
{
314+
) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
315+
314316
override val testScheduler get() = testScope.testScheduler
315317

316-
override fun cleanupTestCoroutines() = testScope.cleanupTestCoroutines()
318+
@Deprecated(
319+
"This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
320+
ReplaceWith("this.cleanup()"),
321+
DeprecationLevel.ERROR
322+
)
323+
override fun cleanupTestCoroutines() =
324+
throw UnsupportedOperationException(
325+
"Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
326+
"it will be called at the end of the test in any case."
327+
)
317328

329+
fun cleanup() = testScope.cleanupTestCoroutines()
318330
}

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

+24-11
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ import kotlin.coroutines.*
1111
/**
1212
* Access uncaught coroutine exceptions captured during test execution.
1313
*/
14-
@ExperimentalCoroutinesApi
14+
@Deprecated(
15+
"Deprecated for removal without a replacement. " +
16+
"Consider whether the default mechanism of handling uncaught exceptions is sufficient. " +
17+
"If not, try writing your own `CoroutineExceptionHandler` and " +
18+
"please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.",
19+
level = DeprecationLevel.WARNING
20+
)
1521
public interface UncaughtExceptionCaptor {
1622
/**
1723
* List of uncaught coroutine exceptions.
1824
*
1925
* The returned list is a copy of the currently caught exceptions.
20-
* During [cleanupTestCoroutinesCaptor] the first element of this list is rethrown if it is not empty.
26+
* During [cleanupTestCoroutines] the first element of this list is rethrown if it is not empty.
2127
*/
2228
public val uncaughtExceptions: List<Throwable>
2329

@@ -29,33 +35,40 @@ public interface UncaughtExceptionCaptor {
2935
*
3036
* @throws Throwable the first uncaught exception, if there are any uncaught exceptions.
3137
*/
32-
public fun cleanupTestCoroutinesCaptor()
38+
public fun cleanupTestCoroutines()
3339
}
3440

3541
/**
3642
* An exception handler that captures uncaught exceptions in tests.
3743
*/
38-
@ExperimentalCoroutinesApi
44+
@Deprecated(
45+
"Deprecated for removal without a replacement. " +
46+
"It may be to define one's own `CoroutineExceptionHandler` if you just need to handle '" +
47+
"uncaught exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING
48+
)
3949
public class TestCoroutineExceptionHandler :
40-
AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
41-
{
50+
AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor {
4251
private val _exceptions = mutableListOf<Throwable>()
4352
private val _lock = SynchronizedObject()
53+
private var _coroutinesCleanedUp = false
4454

45-
/** @suppress **/
55+
@Suppress("INVISIBLE_MEMBER")
4656
override fun handleException(context: CoroutineContext, exception: Throwable) {
4757
synchronized(_lock) {
58+
if (_coroutinesCleanedUp) {
59+
handleCoroutineExceptionImpl(context, exception)
60+
return
61+
}
4862
_exceptions += exception
4963
}
5064
}
5165

52-
/** @suppress **/
53-
override val uncaughtExceptions: List<Throwable>
66+
public override val uncaughtExceptions: List<Throwable>
5467
get() = synchronized(_lock) { _exceptions.toList() }
5568

56-
/** @suppress **/
57-
override fun cleanupTestCoroutinesCaptor() {
69+
public override fun cleanupTestCoroutines() {
5870
synchronized(_lock) {
71+
_coroutinesCleanedUp = true
5972
val exception = _exceptions.firstOrNull() ?: return
6073
// log the rest
6174
_exceptions.drop(1).forEach { it.printStackTrace() }

0 commit comments

Comments
 (0)