@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 package kotlinx.coroutines import kotlinx.coroutines.testing.* import kotlinx.coroutines.channels.* import kotlin.coroutines.* import kotlin.test.* /** * Systematically tests that various builders cancel parent on failure. */ class ParentCancellationTest : TestBase() { @Test fun testJobChild() = runTest { testParentCancellation(expectUnhandled = false) { fail -> val child = Job(coroutineContext[Job]) CoroutineScope(coroutineContext + child).fail() } } @Test fun testSupervisorJobChild() = runTest { testParentCancellation(expectParentActive = true, expectUnhandled = true) { fail -> val child = SupervisorJob(coroutineContext[Job]) CoroutineScope(coroutineContext + child).fail() } } @Test fun testCompletableDeferredChild() = runTest { testParentCancellation { fail -> val child = CompletableDeferred(coroutineContext[Job]) CoroutineScope(coroutineContext + child).fail() } } @Test fun testLaunchChild() = runTest { testParentCancellation(runsInScopeContext = true) { fail -> launch { fail() } } } @Test fun testAsyncChild() = runTest { testParentCancellation(runsInScopeContext = true) { fail -> async { fail() } } } @Test fun testProduceChild() = runTest { testParentCancellation(runsInScopeContext = true) { fail -> produce { fail() } } } @Test fun testBroadcastChild() = runTest { testParentCancellation(runsInScopeContext = true) { fail -> broadcast { fail() }.openSubscription() } } @Test fun testSupervisorChild() = runTest { testParentCancellation(expectParentActive = true, expectUnhandled = true, runsInScopeContext = true) { fail -> supervisorScope { fail() } } } @Test fun testCoroutineScopeChild() = runTest { testParentCancellation(expectParentActive = true, expectRethrows = true, runsInScopeContext = true) { fail -> coroutineScope { fail() } } } @Test fun testWithContextChild() = runTest { testParentCancellation(expectParentActive = true, expectRethrows = true, runsInScopeContext = true) { fail -> withContext(CoroutineName("fail")) { fail() } } } @Test fun testWithTimeoutChild() = runTest { testParentCancellation(expectParentActive = true, expectRethrows = true, runsInScopeContext = true) { fail -> withTimeout(1000) { fail() } } } private suspend fun CoroutineScope.testParentCancellation( expectParentActive: Boolean = false, expectRethrows: Boolean = false, expectUnhandled: Boolean = false, runsInScopeContext: Boolean = false, child: suspend CoroutineScope.(block: suspend CoroutineScope.() -> Unit) -> Unit ) { testWithException( expectParentActive, expectRethrows, expectUnhandled, runsInScopeContext, TestException(), child ) testWithException( true, expectRethrows, false, runsInScopeContext, CancellationException("Test"), child ) } private suspend fun CoroutineScope.testWithException( expectParentActive: Boolean, expectRethrows: Boolean, expectUnhandled: Boolean, runsInScopeContext: Boolean, throwException: Throwable, child: suspend CoroutineScope.(block: suspend CoroutineScope.() -> Unit) -> Unit ) { reset() expect(1) val parent = CompletableDeferred() // parent that handles exception (!) val scope = CoroutineScope(coroutineContext + parent) try { scope.child { // launch failing grandchild var unhandledException: Throwable? = null val handler = CoroutineExceptionHandler { _, e -> unhandledException = e } val grandchild = launch(handler) { throw throwException } grandchild.join() when { !expectParentActive && runsInScopeContext -> expectUnreached() expectUnhandled -> assertSame(throwException, unhandledException) else -> assertNull(unhandledException) } } if (expectRethrows && throwException !is CancellationException) { expectUnreached() } else { expect(2) } } catch (e: Throwable) { if (expectRethrows) { expect(2) assertSame(throwException, e) } else { expectUnreached() } } if (expectParentActive) { assertTrue(parent.isActive) } else { parent.join() assertFalse(parent.isActive) assertTrue(parent.isCancelled) } finish(3) } }