Skip to content

Commit bef199c

Browse files
committed
Suppress excessive exceptions in cleanup
1 parent f158e36 commit bef199c

File tree

2 files changed

+62
-16
lines changed

2 files changed

+62
-16
lines changed

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

+27-16
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ public interface TestCoroutineScope: CoroutineScope {
4747
/**
4848
* Reports an exception so that it is thrown on [cleanupTestCoroutines].
4949
*
50-
* If several exceptions are reported, only the first one will be thrown, and the other ones will be printed to the
51-
* console.
50+
* If several exceptions are reported, only the first one will be thrown, and the other ones will be suppressed by
51+
* it.
5252
*
5353
* @throws IllegalStateException with the [Throwable.cause] set to [throwable] if [cleanupTestCoroutines] was
5454
* already called.
@@ -85,7 +85,11 @@ private class TestCoroutineScopeImpl(
8585
val delayController = coroutineContext.delayController
8686
var hasUncompletedJobs = false
8787
if (delayController != null) {
88-
delayController.cleanupTestCoroutines()
88+
try {
89+
delayController.cleanupTestCoroutines()
90+
} catch (e: UncompletedCoroutinesError) {
91+
hasUncompletedJobs = true
92+
}
8993
} else {
9094
testScheduler.runCurrent()
9195
if (!testScheduler.isIdle()) {
@@ -98,20 +102,27 @@ private class TestCoroutineScopeImpl(
98102
cleanedUp = true
99103
}
100104
exceptions.apply {
101-
drop(1).forEach { it.printStackTrace() }
102-
singleOrNull()?.let { throw it }
103-
}
104-
if (hasUncompletedJobs) {
105-
throw UncompletedCoroutinesError(
106-
"Unfinished coroutines during teardown. Ensure all coroutines are" +
107-
" completed or cancelled by your test."
108-
)
105+
firstOrNull()?.let {
106+
val toThrow = it
107+
drop(1).forEach { toThrow.addSuppressed(it) }
108+
ownJob?.completeExceptionally(toThrow)
109+
throw toThrow
110+
}
109111
}
110-
val jobs = coroutineContext.activeJobs()
111-
if ((jobs - initialJobs).isNotEmpty()) {
112-
val exception = UncompletedCoroutinesError("Test finished with active jobs: $jobs")
113-
ownJob?.completeExceptionally(exception)
114-
throw exception
112+
try {
113+
if (hasUncompletedJobs) {
114+
throw UncompletedCoroutinesError(
115+
"Unfinished coroutines during teardown. Ensure all coroutines are" +
116+
" completed or cancelled by your test."
117+
)
118+
}
119+
val jobs = coroutineContext.activeJobs()
120+
if ((jobs - initialJobs).isNotEmpty()) {
121+
throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
122+
}
123+
} catch (e: UncompletedCoroutinesError) {
124+
ownJob?.completeExceptionally(e)
125+
throw e
115126
}
116127
ownJob?.complete()
117128
}

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

+35
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ class TestCoroutineScopeTest {
107107
assertTrue(handlerCalled)
108108
}
109109

110+
/** Tests that the coroutine scope completes its job with an exception. */
111+
@Test
112+
fun testCompletesOwnJobOnFailure() {
113+
val scope = createTestCoroutineScope()
114+
val exception = TestException("x")
115+
var caughtException: Throwable? = null
116+
scope.reportException(exception)
117+
scope.coroutineContext.job.invokeOnCompletion {
118+
caughtException = it
119+
}
120+
assertFailsWith<TestException> {
121+
scope.cleanupTestCoroutines()
122+
}
123+
assertSame(exception, caughtException)
124+
}
125+
110126
/** Tests that the coroutine scope completes its job if the job was not passed to it as an argument. */
111127
@Test
112128
fun testDoesNotCompleteGivenJob() {
@@ -169,6 +185,25 @@ class TestCoroutineScopeTest {
169185
}
170186
}
171187

188+
/** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */
189+
@Test
190+
fun testSuppressedExceptions() {
191+
createTestCoroutineScope().apply {
192+
reportException(TestException("x"))
193+
reportException(TestException("y"))
194+
reportException(TestException("z"))
195+
try {
196+
cleanupTestCoroutines()
197+
fail("should not be reached")
198+
} catch (e: TestException) {
199+
assertEquals("x", e.message)
200+
assertEquals(2, e.suppressedExceptions.size)
201+
assertEquals("y", e.suppressedExceptions[0].message)
202+
assertEquals("z", e.suppressedExceptions[1].message)
203+
}
204+
}
205+
}
206+
172207
companion object {
173208
internal val invalidContexts = listOf(
174209
Dispatchers.Default, // not a [TestDispatcher]

0 commit comments

Comments
 (0)