blob: a95a3da37952dee5566a76d31cb69da0b935a10d [file] [log] [blame]
/*
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.exceptions
import kotlinx.coroutines.*
import org.junit.Test
import org.junit.runner.*
import org.junit.runners.*
import kotlin.coroutines.*
import kotlin.test.*
@RunWith(Parameterized::class)
class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() {
enum class Mode { WITH_CONTEXT, ASYNC_AWAIT }
companion object {
@Parameterized.Parameters(name = "mode={0}")
@JvmStatic
fun params(): Collection<Array<Any>> = Mode.values().map { arrayOf<Any>(it) }
}
@Test
fun testCancellation() = runTest {
/*
* context cancelled without cause
* code itself throws TE2
* Result: TE2
*/
runCancellation(null, TestException2()) { e ->
assertTrue(e is TestException2)
assertNull(e.cause)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testCancellationWithException() = runTest {
/*
* context cancelled with TCE
* block itself throws TE2
* Result: TE (CancellationException is always ignored)
*/
val cancellationCause = TestCancellationException()
runCancellation(cancellationCause, TestException2()) { e ->
assertTrue(e is TestException2)
assertNull(e.cause)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testSameException() = runTest {
/*
* context cancelled with TCE
* block itself throws the same TCE
* Result: TCE
*/
val cancellationCause = TestCancellationException()
runCancellation(cancellationCause, cancellationCause) { e ->
assertTrue(e is TestCancellationException)
assertNull(e.cause)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testSameCancellation() = runTest {
/*
* context cancelled with TestCancellationException
* block itself throws the same TCE
* Result: TCE
*/
val cancellationCause = TestCancellationException()
runCancellation(cancellationCause, cancellationCause) { e ->
assertSame(e, cancellationCause)
assertNull(e.cause)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testSameCancellationWithException() = runTest {
/*
* context cancelled with CancellationException(TE)
* block itself throws the same TE
* Result: TE
*/
val cancellationCause = CancellationException()
val exception = TestException()
cancellationCause.initCause(exception)
runCancellation(cancellationCause, exception) { e ->
assertSame(exception, e)
assertNull(e.cause)
assertTrue(e.suppressed.isEmpty())
}
}
@Test
fun testConflictingCancellation() = runTest {
/*
* context cancelled with TCE
* block itself throws CE(TE)
* Result: TE (because cancellation exception is always ignored and not handled)
*/
val cancellationCause = TestCancellationException()
val thrown = CancellationException()
thrown.initCause(TestException())
runCancellation(cancellationCause, thrown) { e ->
assertSame(cancellationCause, e)
assertTrue(e.suppressed.isEmpty())
}
}
@Test
fun testConflictingCancellation2() = runTest {
/*
* context cancelled with TE
* block itself throws CE
* Result: TE
*/
val cancellationCause = TestCancellationException()
val thrown = CancellationException()
runCancellation(cancellationCause, thrown) { e ->
assertSame(cancellationCause, e)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testConflictingCancellation3() = runTest {
/*
* context cancelled with TCE
* block itself throws TCE
* Result: TCE
*/
val cancellationCause = TestCancellationException()
val thrown = TestCancellationException()
runCancellation(cancellationCause, thrown) { e ->
assertSame(cancellationCause, e)
assertNull(e.cause)
assertTrue(e.suppressed.isEmpty())
}
}
@Test
fun testThrowingCancellation() = runTest {
val thrown = TestCancellationException()
runThrowing(thrown) { e ->
assertSame(thrown, e)
}
}
@Test
fun testThrowingCancellationWithCause() = runTest {
// Exception are never unwrapped, so if CE(TE) is thrown then it is the cancellation cause
val thrown = TestCancellationException()
thrown.initCause(TestException())
runThrowing(thrown) { e ->
assertSame(thrown, e)
}
}
@Test
fun testCancel() = runTest {
runOnlyCancellation(null) { e ->
val cause = e.cause as JobCancellationException // shall be recovered JCE
assertNull(cause.cause)
assertTrue(e.suppressed.isEmpty())
assertTrue(cause.suppressed.isEmpty())
}
}
@Test
fun testCancelWithCause() = runTest {
val cause = TestCancellationException()
runOnlyCancellation(cause) { e ->
assertSame(cause, e)
assertTrue(e.suppressed.isEmpty())
}
}
@Test
fun testCancelWithCancellationException() = runTest {
val cause = TestCancellationException()
runThrowing(cause) { e ->
assertSame(cause, e)
assertNull(e.cause)
assertTrue(e.suppressed.isEmpty())
}
}
private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
return object : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatcher.dispatch(context, block)
}
}
}
private suspend fun runCancellation(
cancellationCause: CancellationException?,
thrownException: Throwable,
exceptionChecker: (Throwable) -> Unit
) {
expect(1)
try {
withCtx(wrapperDispatcher(coroutineContext)) { job ->
require(isActive) // not cancelled yet
job.cancel(cancellationCause)
require(!isActive) // now cancelled
expect(2)
throw thrownException
}
} catch (e: Throwable) {
exceptionChecker(e)
finish(3)
return
}
fail()
}
private suspend fun runThrowing(
thrownException: Throwable,
exceptionChecker: (Throwable) -> Unit
) {
expect(1)
try {
withCtx(wrapperDispatcher(coroutineContext).minusKey(Job)) {
require(isActive)
expect(2)
throw thrownException
}
} catch (e: Throwable) {
exceptionChecker(e)
finish(3)
return
}
fail()
}
private suspend fun withCtx(context: CoroutineContext, job: Job = Job(), block: suspend CoroutineScope.(Job) -> Nothing) {
when (mode) {
Mode.WITH_CONTEXT -> withContext(context + job) {
block(job)
}
Mode.ASYNC_AWAIT -> CoroutineScope(coroutineContext).async(context + job) {
block(job)
}.await()
}
}
private suspend fun runOnlyCancellation(
cancellationCause: CancellationException?,
exceptionChecker: (Throwable) -> Unit
) {
expect(1)
val job = Job()
try {
withContext(wrapperDispatcher(coroutineContext) + job) {
require(isActive) // still active
job.cancel(cancellationCause)
require(!isActive) // is already cancelled
expect(2)
}
} catch (e: Throwable) {
exceptionChecker(e)
finish(3)
return
}
fail()
}
}