Skip to content

Commit 5ae073d

Browse files
committed
Remove configurable exception handling
1 parent fd9b064 commit 5ae073d

11 files changed

+124
-306
lines changed

kotlinx-coroutines-core/common/README.md

-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ Low-level primitives for finer-grained control of coroutines.
130130

131131
[kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
132132
[kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html
133-
[kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html
134133

135134
<!--- INDEX kotlinx.coroutines.channels -->
136135

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

-6
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public final class kotlinx/coroutines/test/TestCoroutineScheduler$Key : kotlin/c
6969
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;
72-
public abstract fun reportException (Ljava/lang/Throwable;)V
7372
}
7473

7574
public final class kotlinx/coroutines/test/TestCoroutineScopeKt {
@@ -99,11 +98,6 @@ public final class kotlinx/coroutines/test/TestDispatchers {
9998
public static final fun setMain (Lkotlinx/coroutines/Dispatchers;Lkotlinx/coroutines/CoroutineDispatcher;)V
10099
}
101100

102-
public final class kotlinx/coroutines/test/TestExceptionHandlerKt {
103-
public static final fun TestExceptionHandler (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/CoroutineExceptionHandler;
104-
public static synthetic fun TestExceptionHandler$default (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlinx/coroutines/CoroutineExceptionHandler;
105-
}
106-
107101
public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
108102
public abstract fun cleanupTestCoroutines ()V
109103
public abstract fun getUncaughtExceptions ()Ljava/util/List;

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

+21-9
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 {
@@ -205,7 +208,7 @@ public fun runTest(
205208
}
206209
onTimeout(dispatchTimeoutMs) {
207210
try {
208-
testScope.cleanupTestCoroutines()
211+
testScope.cleanup()
209212
} catch (e: UncompletedCoroutinesError) {
210213
// we expect these and will instead throw a more informative exception just below.
211214
}
@@ -215,15 +218,15 @@ public fun runTest(
215218
}
216219
testScope.getCompletionExceptionOrNull()?.let {
217220
try {
218-
testScope.cleanupTestCoroutines()
221+
testScope.cleanup()
219222
} catch (e: UncompletedCoroutinesError) {
220223
// it's normal that some jobs are not completed if the test body has failed, won't clutter the output
221224
} catch (e: Throwable) {
222225
it.addSuppressed(e)
223226
}
224227
throw it
225228
}
226-
testScope.cleanupTestCoroutines()
229+
testScope.cleanup()
227230
}
228231
}
229232

@@ -267,7 +270,7 @@ public fun TestDispatcher.runTest(
267270
runTest(this, dispatchTimeoutMs, block)
268271

269272
/** A coroutine context element indicating that the coroutine is running inside `runTest`. */
270-
private object RunningInRunTest: CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
273+
private object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
271274
override val key: CoroutineContext.Key<*>
272275
get() = this
273276

@@ -280,11 +283,20 @@ private const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
280283

281284
private class TestBodyCoroutine<T>(
282285
private val testScope: TestCoroutineScope,
283-
) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope
284-
{
286+
) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
287+
285288
override val testScheduler get() = testScope.testScheduler
286289

287-
override fun cleanupTestCoroutines() = testScope.cleanupTestCoroutines()
290+
@Deprecated(
291+
"This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
292+
ReplaceWith("this.cleanup()"),
293+
DeprecationLevel.ERROR
294+
)
295+
override fun cleanupTestCoroutines() =
296+
throw UnsupportedOperationException(
297+
"Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
298+
"it will be called at the end of the test in any case."
299+
)
288300

289-
override fun reportException(throwable: Throwable) = testScope.reportException(throwable)
301+
fun cleanup() = testScope.cleanupTestCoroutines()
290302
}

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import kotlin.coroutines.*
1212
* Access uncaught coroutine exceptions captured during test execution.
1313
*/
1414
@Deprecated(
15-
"Consider whether a `TestExceptionHandler` would work instead. If not, please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.",
15+
"Consider whether the default mechanism of handling uncaught exceptions is sufficient. " +
16+
"If not, try writing your own `CoroutineExceptionHandler` and " +
17+
"please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.",
1618
level = DeprecationLevel.WARNING
1719
)
1820
public interface UncaughtExceptionCaptor {
@@ -38,12 +40,12 @@ public interface UncaughtExceptionCaptor {
3840
/**
3941
* An exception handler that captures uncaught exceptions in tests.
4042
*/
41-
@Deprecated("You can use `TestExceptionHandler` to define an exception handler that is linked to " +
42-
"a `TestCoroutineScope`, or define your own `CoroutineExceptionHandler` if you just need to handle uncaught " +
43-
"exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING)
43+
@Deprecated(
44+
"It's better to define one's own `CoroutineExceptionHandler` if you just need to handle '" +
45+
"uncaught exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING
46+
)
4447
public class TestCoroutineExceptionHandler :
45-
AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor
46-
{
48+
AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor {
4749
private val _exceptions = mutableListOf<Throwable>()
4850
private val _lock = SynchronizedObject()
4951
private var _coroutinesCleanedUp = false

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

+86-68
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,18 @@ import kotlin.coroutines.*
1212
* A scope which provides detailed control over the execution of coroutines for tests.
1313
*/
1414
@ExperimentalCoroutinesApi
15-
public sealed interface TestCoroutineScope: CoroutineScope {
15+
public sealed interface TestCoroutineScope : CoroutineScope {
1616
/**
1717
* Called after the test completes.
1818
*
19-
* * It checks that there were no uncaught exceptions reported via [reportException]. If there were any, then the
20-
* first one is thrown, whereas the rest are printed to the standard output or the standard error output
21-
* (see [Throwable.printStackTrace]).
19+
* * It checks that there were no uncaught exceptions caught by its [CoroutineExceptionHandler].
20+
* If there were any, then the first one is thrown, whereas the rest are suppressed by it.
2221
* * It runs the tasks pending in the scheduler at the current time. If there are any uncompleted tasks afterwards,
2322
* it fails with [UncompletedCoroutinesError].
2423
* * It checks whether some new child [Job]s were created but not completed since this [TestCoroutineScope] was
2524
* created. If so, it fails with [UncompletedCoroutinesError].
2625
*
27-
* For backwards compatibility, if the [CoroutineExceptionHandler] is an [UncaughtExceptionCaptor], its
26+
* For backward compatibility, if the [CoroutineExceptionHandler] is an [UncaughtExceptionCaptor], its
2827
* [TestCoroutineExceptionHandler.cleanupTestCoroutines] behavior is performed.
2928
* Likewise, if the [ContinuationInterceptor] is a [DelayController], its [DelayController.cleanupTestCoroutines]
3029
* is called.
@@ -41,35 +40,32 @@ public sealed interface TestCoroutineScope: CoroutineScope {
4140
*/
4241
@ExperimentalCoroutinesApi
4342
public val testScheduler: TestCoroutineScheduler
44-
45-
/**
46-
* Reports an exception so that it is thrown on [cleanupTestCoroutines].
47-
*
48-
* If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by
49-
* it.
50-
*
51-
* @throws IllegalStateException with the [Throwable.cause] set to [throwable] if [cleanupTestCoroutines] was
52-
* already called.
53-
*/
54-
@ExperimentalCoroutinesApi
55-
public fun reportException(throwable: Throwable)
5643
}
5744

5845
private class TestCoroutineScopeImpl(
5946
override val coroutineContext: CoroutineContext
60-
): TestCoroutineScope
61-
{
47+
) : TestCoroutineScope {
6248
private val lock = SynchronizedObject()
6349
private var exceptions = mutableListOf<Throwable>()
6450
private var cleanedUp = false
6551

66-
override fun reportException(throwable: Throwable) {
52+
/**
53+
* Reports an exception so that it is thrown on [cleanupTestCoroutines].
54+
*
55+
* If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by
56+
* it.
57+
*
58+
* Returns `false` if [cleanupTestCoroutines] was already called.
59+
*/
60+
fun reportException(throwable: Throwable): Boolean =
6761
synchronized(lock) {
68-
if (cleanedUp)
69-
throw ExceptionReportAfterCleanup(throwable)
70-
exceptions.add(throwable)
62+
if (cleanedUp) {
63+
false
64+
} else {
65+
exceptions.add(throwable)
66+
true
67+
}
7168
}
72-
}
7369

7470
override val testScheduler: TestCoroutineScheduler
7571
get() = coroutineContext[TestCoroutineScheduler]!!
@@ -111,7 +107,7 @@ private class TestCoroutineScopeImpl(
111107
}
112108
}
113109

114-
internal class ExceptionReportAfterCleanup(cause: Throwable): IllegalStateException(
110+
internal class ExceptionReportAfterCleanup(cause: Throwable) : IllegalStateException(
115111
"Attempting to report an uncaught exception after the test coroutine scope was already cleaned up",
116112
cause
117113
)
@@ -125,10 +121,13 @@ private fun CoroutineContext.activeJobs(): Set<Job> {
125121
*
126122
* [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher].
127123
*/
128-
@Deprecated("This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
129-
"Please use `createTestCoroutineScope` instead.",
130-
ReplaceWith("createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)",
131-
"kotlin.coroutines.EmptyCoroutineContext"),
124+
@Deprecated(
125+
"This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " +
126+
"Please use `createTestCoroutineScope` instead.",
127+
ReplaceWith(
128+
"createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + context)",
129+
"kotlin.coroutines.EmptyCoroutineContext"
130+
),
132131
level = DeprecationLevel.WARNING
133132
)
134133
public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
@@ -143,8 +142,13 @@ public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext)
143142
* * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
144143
* a new one is created, unless a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used.
145144
* * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created.
146-
* * If [context] does not provide a [CoroutineExceptionHandler], [TestCoroutineExceptionHandler] is created
147-
* automatically.
145+
* * A [CoroutineExceptionHandler] is created that makes [TestCoroutineScope.cleanupTestCoroutines] throw if there were
146+
* any uncaught exceptions, or forwards the exceptions further in a platform-specific manner if the cleanup was
147+
* already performed when an exception happened. Passing a [CoroutineExceptionHandler] is illegal, unless it's an
148+
* [UncaughtExceptionCaptor], in which case the behavior is preserved for the time being for backward compatibility.
149+
* If you need to have a specific [CoroutineExceptionHandler], please pass it to [launch] on an already-created
150+
* [TestCoroutineScope] and share your use case at
151+
* [our issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues).
148152
* * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
149153
*
150154
* @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
@@ -174,26 +178,25 @@ public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineCo
174178
}
175179
else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher")
176180
}
177-
val linkedHandler: TestExceptionHandlerContextElement?
178-
val exceptionHandler: CoroutineExceptionHandler
179-
val handlerOwner = Any()
180-
when (val exceptionHandlerInCtx = context[CoroutineExceptionHandler]) {
181+
var scope: TestCoroutineScopeImpl? = null
182+
val exceptionHandler = when (val exceptionHandler = context[CoroutineExceptionHandler]) {
183+
is UncaughtExceptionCaptor -> exceptionHandler
181184
null -> {
182-
linkedHandler = TestExceptionHandlerContextElement(
183-
{ _, throwable -> reportException(throwable) },
184-
null,
185-
handlerOwner)
186-
exceptionHandler = linkedHandler
187-
}
188-
else -> {
189-
linkedHandler = (exceptionHandlerInCtx as? TestExceptionHandlerContextElement
190-
)?.claimOwnershipOrCopy(handlerOwner)
191-
exceptionHandler = linkedHandler ?: exceptionHandlerInCtx
185+
val lock = SynchronizedObject()
186+
CoroutineExceptionHandler { context, throwable ->
187+
val reported = synchronized(lock) {
188+
scope!!.reportException(throwable)
189+
}
190+
if (!reported)
191+
throw throwable // let this exception crash everything
192+
}
192193
}
194+
else -> throw IllegalArgumentException("A CoroutineExceptionHandler was passed to TestCoroutineScope")
193195
}
194196
val job: Job = context[Job] ?: Job()
195-
return TestCoroutineScopeImpl(context + scheduler + dispatcher + exceptionHandler + job)
196-
.also { linkedHandler?.registerTestCoroutineScope(handlerOwner, it) }
197+
return TestCoroutineScopeImpl(context + scheduler + dispatcher + exceptionHandler + job).also {
198+
scope = it
199+
}
197200
}
198201

199202

@@ -219,10 +222,12 @@ public val TestCoroutineScope.currentTime: Long
219222
* @see TestCoroutineScheduler.advanceTimeBy
220223
*/
221224
@ExperimentalCoroutinesApi
222-
@Deprecated("The name of this function is misleading: it not only advances the time, but also runs the tasks " +
223-
"scheduled *at* the ending moment.",
225+
@Deprecated(
226+
"The name of this function is misleading: it not only advances the time, but also runs the tasks " +
227+
"scheduled *at* the ending moment.",
224228
ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"),
225-
DeprecationLevel.WARNING)
229+
DeprecationLevel.WARNING
230+
)
226231
public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit =
227232
when (val controller = coroutineContext.delayController) {
228233
null -> {
@@ -256,34 +261,46 @@ public fun TestCoroutineScope.runCurrent() {
256261
}
257262

258263
@ExperimentalCoroutinesApi
259-
@Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " +
260-
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
261-
"\"paused\", like `StandardTestDispatcher`.",
262-
ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
263-
"kotlin.coroutines.ContinuationInterceptor"),
264-
DeprecationLevel.WARNING)
264+
@Deprecated(
265+
"The test coroutine scope isn't able to pause its dispatchers in the general case. " +
266+
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
267+
"\"paused\", like `StandardTestDispatcher`.",
268+
ReplaceWith(
269+
"(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)",
270+
"kotlin.coroutines.ContinuationInterceptor"
271+
),
272+
DeprecationLevel.WARNING
273+
)
265274
public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) {
266275
delayControllerForPausing.pauseDispatcher(block)
267276
}
268277

269278
@ExperimentalCoroutinesApi
270-
@Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " +
271-
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
279+
@Deprecated(
280+
"The test coroutine scope isn't able to pause its dispatchers in the general case. " +
281+
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
272282
"\"paused\", like `StandardTestDispatcher`.",
273-
ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
274-
"kotlin.coroutines.ContinuationInterceptor"),
275-
level = DeprecationLevel.WARNING)
283+
ReplaceWith(
284+
"(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()",
285+
"kotlin.coroutines.ContinuationInterceptor"
286+
),
287+
level = DeprecationLevel.WARNING
288+
)
276289
public fun TestCoroutineScope.pauseDispatcher() {
277290
delayControllerForPausing.pauseDispatcher()
278291
}
279292

280293
@ExperimentalCoroutinesApi
281-
@Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " +
282-
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
294+
@Deprecated(
295+
"The test coroutine scope isn't able to pause its dispatchers in the general case. " +
296+
"Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " +
283297
"\"paused\", like `StandardTestDispatcher`.",
284-
ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
285-
"kotlin.coroutines.ContinuationInterceptor"),
286-
level = DeprecationLevel.WARNING)
298+
ReplaceWith(
299+
"(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()",
300+
"kotlin.coroutines.ContinuationInterceptor"
301+
),
302+
level = DeprecationLevel.WARNING
303+
)
287304
public fun TestCoroutineScope.resumeDispatcher() {
288305
delayControllerForPausing.resumeDispatcher()
289306
}
@@ -301,7 +318,8 @@ public fun TestCoroutineScope.resumeDispatcher() {
301318
"easily misused. It is only present for backward compatibility and will be removed in the subsequent " +
302319
"releases. If you need to check the list of exceptions, please consider creating your own " +
303320
"`CoroutineExceptionHandler`.",
304-
level = DeprecationLevel.WARNING)
321+
level = DeprecationLevel.WARNING
322+
)
305323
public val TestCoroutineScope.uncaughtExceptions: List<Throwable>
306324
get() = (coroutineContext[CoroutineExceptionHandler] as? UncaughtExceptionCaptor)?.uncaughtExceptions
307325
?: emptyList()
@@ -314,4 +332,4 @@ private val TestCoroutineScope.delayControllerForPausing: DelayController
314332
* Thrown when a test has completed and there are tasks that are not completed or cancelled.
315333
*/
316334
@ExperimentalCoroutinesApi
317-
internal class UncompletedCoroutinesError(message: String): AssertionError(message)
335+
internal class UncompletedCoroutinesError(message: String) : AssertionError(message)

0 commit comments

Comments
 (0)