blob: 73522c083783670a1c56d67ab9b1b64dc2fc674c [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.experimental.exceptions
import kotlinx.coroutines.experimental.*
import org.junit.Test
import java.io.*
import java.nio.channels.*
import kotlin.coroutines.experimental.*
import kotlin.test.*
class CancellableContinuationExceptionHandlingTest : TestBase() {
@Test
fun testCancellation() = runTest {
/*
* Continuation cancelled without cause
* Continuation itself throws ISE
* Result: ISE with suppressed CancellationException
*/
runCancellation(coroutineContext, null, IllegalStateException()) { e ->
assertNull(e.cause)
val suppressed = e.suppressed()
assertTrue(suppressed.size == 1)
val cancellation = suppressed[0] as CancellationException
assertNull(cancellation.cause)
assertTrue(cancellation.suppressed().isEmpty())
}
}
@Test
fun testCancellationWithException() = runTest {
/*
* Continuation cancelled with IOE
* Continuation itself throws ISE
* Result: ISE with suppressed CancellationException(IOE)
*/
val cancellationCause = IOException()
runCancellation(coroutineContext, cancellationCause, IllegalStateException()) { e ->
assertNull(e.cause)
val suppressed = e.suppressed()
assertTrue(suppressed.size == 1)
val cancellation = suppressed[0] as CancellationException
assertSame(cancellation.cause, cancellationCause)
assertTrue(cancellationCause.suppressed().isEmpty())
}
}
@Test
fun testSameException() = runTest {
/*
* Continuation cancelled with ISE
* Continuation itself throws the same ISE
* Result: ISE
*/
val cancellationCause = IllegalStateException()
runCancellation(coroutineContext, cancellationCause, cancellationCause) { e ->
assertNull(e.cause)
val suppressed = e.suppressed()
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testSameCancellation() = runTest {
/*
* Continuation cancelled with CancellationException
* Continuation itself throws the same CE
* Result: CE
*/
val cancellationCause = CancellationException()
runCancellation(coroutineContext, cancellationCause, cancellationCause) { e ->
assertNull(e.cause)
assertSame(e, cancellationCause)
val suppressed = e.suppressed()
assertTrue(suppressed.isEmpty())
}
}
@Test
fun testSameCancellationWithException() = runTest {
/*
* Continuation cancelled with CancellationException(IOE)
* Continuation itself throws the same IOE
* Result: IOE
*/
val cancellationCause = CancellationException()
val exception = IOException()
cancellationCause.initCause(exception)
runCancellation(coroutineContext, cancellationCause, exception) { e ->
assertNull(e.cause)
assertSame(exception, e)
assertTrue(e.suppressed().isEmpty())
}
}
@Test
fun testConflictingCancellation() = runTest {
/*
* Continuation cancelled with ISE
* Continuation itself throws CE(IOE)
* Result: CE(IOE) with suppressed JCE(ISE)
*/
val cancellationCause = IllegalStateException()
val thrown = CancellationException()
thrown.initCause(IOException())
runCancellation(coroutineContext, cancellationCause, thrown) { e ->
assertSame(thrown, e)
assertEquals(1, thrown.suppressed().size)
val suppressed = thrown.suppressed()[0]
assertTrue(suppressed is JobCancellationException)
assertTrue(suppressed.cause is IllegalStateException)
}
}
@Test
fun testConflictingCancellation2() = runTest {
/*
* Continuation cancelled with ISE
* Continuation itself throws CE
* Result: CE with suppressed JCE(ISE)
*/
val cancellationCause = IllegalStateException()
val thrown = CancellationException()
runCancellation(coroutineContext, cancellationCause, thrown) { e ->
assertSame(thrown, e)
assertEquals(1, thrown.suppressed().size)
val suppressed = thrown.suppressed()[0]
assertTrue(suppressed is JobCancellationException)
assertTrue(suppressed.cause is IllegalStateException)
}
}
@Test
fun testConflictingCancellation3() = runTest {
/*
* Continuation cancelled with CE
* Continuation itself throws CE
* Result: CE
*/
val cancellationCause = CancellationException()
val thrown = CancellationException()
runCancellation(coroutineContext, cancellationCause, thrown) { e ->
assertSame(thrown, e)
assertNull(e.cause)
assertTrue(e.suppressed().isEmpty())
}
}
@Test
fun testConflictingCancellationWithSameException() = runTest {
val cancellationCause = IllegalStateException()
val thrown = CancellationException()
/*
* Continuation cancelled with ISE
* Continuation itself throws CE with the same ISE as a cause
* Result: CE(ISE)
*/
thrown.initCause(cancellationCause)
runCancellation(coroutineContext, cancellationCause, thrown) { e ->
assertSame(thrown, e)
assertSame(cancellationCause, e.cause)
assertEquals(0, thrown.suppressed().size)
}
}
@Test
fun testThrowingCancellation() = runTest {
val thrown = CancellationException()
runThrowingContinuation(coroutineContext, thrown) { e ->
assertSame(thrown, e)
}
}
@Test
fun testThrowingCancellationWithCause() = runTest {
val thrown = CancellationException()
thrown.initCause(IOException())
runThrowingContinuation(coroutineContext, thrown) { e ->
assertSame(thrown, e)
}
}
@Test
fun testCancel() = runTest {
runOnlyCancellation(coroutineContext, null) { e ->
assertNull(e.cause)
assertTrue(e.suppressed().isEmpty())
}
}
@Test
fun testCancelWithCause() = runTest {
val cause = IOException()
runOnlyCancellation(coroutineContext, cause) { e ->
assertSame(cause, e.cause)
assertTrue(e.suppressed().isEmpty())
}
}
@Test
fun testCancelWithCancellationException() = runTest {
val cause = CancellationException()
runThrowingContinuation(coroutineContext, 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 inline fun <reified T : Exception> CoroutineScope.runCancellation(
coroutineContext: CoroutineContext,
cancellationCause: Exception?,
thrownException: T, exceptionChecker: (T) -> Unit
) {
expect(1)
val job = Job()
job.cancel(cancellationCause)
try {
withContext(wrapperDispatcher(coroutineContext) + job, CoroutineStart.ATOMIC) {
require(isActive)
expect(2)
throw thrownException
}
} catch (e: Exception) {
assertTrue(e is T)
exceptionChecker(e as T)
finish(3)
return
}
fail()
}
private suspend inline fun <reified T : Exception> CoroutineScope.runThrowingContinuation(
coroutineContext: CoroutineContext,
thrownException: T, exceptionChecker: (T) -> Unit
) {
expect(1)
try {
withContext(wrapperDispatcher(coroutineContext), CoroutineStart.ATOMIC) {
require(isActive)
expect(2)
throw thrownException
}
} catch (e: Exception) {
assertTrue(e is T)
exceptionChecker(e as T)
finish(3)
return
}
fail()
}
private suspend inline fun CoroutineScope.runOnlyCancellation(
coroutineContext: CoroutineContext,
cancellationCause: Exception?, exceptionChecker: (CancellationException) -> Unit
) {
expect(1)
val job = Job()
job.cancel(cancellationCause)
try {
withContext(wrapperDispatcher(coroutineContext) + job, CoroutineStart.ATOMIC) {
require(isActive)
expect(2)
}
} catch (e: Exception) {
assertTrue(e is CancellationException)
exceptionChecker(e as CancellationException)
finish(3)
return
}
fail()
}
}