blob: e728d73b7fe59d907f826fcc307e06963e03e213 [file] [log] [blame]
Vsevolod Tolstopyatovf2b4e0e2018-05-25 18:58:01 +03001package kotlinx.coroutines.experimental
2
3import org.junit.Test
4import java.io.*
5import java.nio.channels.*
6import kotlin.coroutines.experimental.*
7import kotlin.test.*
8
9class CancellableContinuationExceptionHandlingTest : TestBase() {
10
11 @Test
12 fun testCancellation() = runTest {
13 /*
14 * Continuation cancelled without cause
15 * Continuation itself throws ISE
16 * Result: ISE with suppressed CancellationException
17 */
18 runCancellation(coroutineContext, null, IllegalStateException()) { e ->
19 assertNull(e.cause)
20 val suppressed = e.suppressed()
21 assertTrue(suppressed.size == 1)
22
23 val cancellation = suppressed[0] as CancellationException
24 assertNull(cancellation.cause)
25 assertTrue(cancellation.suppressed().isEmpty())
26 }
27 }
28
29 @Test
30 fun testCancellationWithException() = runTest {
31 /*
32 * Continuation cancelled with IOE
33 * Continuation itself throws ISE
34 * Result: ISE with suppressed CancellationException(IOE)
35 */
36 val cancellationCause = IOException()
37 runCancellation(coroutineContext, cancellationCause, IllegalStateException()) { e ->
38 assertNull(e.cause)
39 val suppressed = e.suppressed()
40 assertTrue(suppressed.size == 1)
41
42 val cancellation = suppressed[0] as CancellationException
43 assertSame(cancellation.cause, cancellationCause)
44 assertTrue(cancellationCause.suppressed().isEmpty())
45 }
46 }
47
48 @Test
49 fun testSameException() = runTest {
50 /*
51 * Continuation cancelled with ISE
52 * Continuation itself throws the same ISE
53 * Result: ISE
54 */
55 val cancellationCause = IllegalStateException()
56 runCancellation(coroutineContext, cancellationCause, cancellationCause) { e ->
57 assertNull(e.cause)
58 val suppressed = e.suppressed()
59 assertTrue(suppressed.isEmpty())
60 }
61 }
62
63 @Test
64 fun testSameCancellation() = runTest {
65 /*
66 * Continuation cancelled with CancellationException
67 * Continuation itself throws the same CE
68 * Result: CE
69 */
70 val cancellationCause = CancellationException()
71 runCancellation(coroutineContext, cancellationCause, cancellationCause) { e ->
72 assertNull(e.cause)
73 assertSame(e, cancellationCause)
74 val suppressed = e.suppressed()
75 assertTrue(suppressed.isEmpty())
76 }
77 }
78
79 @Test
80 fun testSameCancellationWithException() = runTest {
81 /*
82 * Continuation cancelled with CancellationException(IOE)
83 * Continuation itself throws the same IOE
84 * Result: IOE
85 */
86 val cancellationCause = CancellationException()
87 val exception = IOException()
88 cancellationCause.initCause(exception)
89 runCancellation(coroutineContext, cancellationCause, exception) { e ->
90 assertNull(e.cause)
91 assertSame(exception, e)
92 assertTrue(e.suppressed().isEmpty())
93 }
94 }
95
96 @Test
97 fun testConflictingCancellation() = runTest {
98 /*
99 * Continuation cancelled with ISE
100 * Continuation itself throws CE(IOE)
101 * Result: CE(IOE) with suppressed JCE(ISE)
102 */
103 val cancellationCause = IllegalStateException()
104 val thrown = CancellationException()
105 thrown.initCause(IOException())
106 runCancellation(coroutineContext, cancellationCause, thrown) { e ->
107 assertSame(thrown, e)
108 assertEquals(1, thrown.suppressed().size)
109
110 val suppressed = thrown.suppressed()[0]
111 assertTrue(suppressed is JobCancellationException)
112 assertTrue(suppressed.cause is IllegalStateException)
113 }
114 }
115
116 @Test
117 fun testConflictingCancellation2() = runTest {
118 /*
119 * Continuation cancelled with ISE
120 * Continuation itself throws CE
121 * Result: CE with suppressed JCE(ISE)
122 */
123 val cancellationCause = IllegalStateException()
124 val thrown = CancellationException()
125 runCancellation(coroutineContext, cancellationCause, thrown) { e ->
126 assertSame(thrown, e)
127 assertEquals(1, thrown.suppressed().size)
128
129 val suppressed = thrown.suppressed()[0]
130 assertTrue(suppressed is JobCancellationException)
131 assertTrue(suppressed.cause is IllegalStateException)
132
133 }
134 }
135
136 @Test
137 fun testConflictingCancellation3() = runTest {
138 /*
139 * Continuation cancelled with CE
140 * Continuation itself throws CE
141 * Result: CE
142 */
143 val cancellationCause = CancellationException()
144 val thrown = CancellationException()
145 runCancellation(coroutineContext, cancellationCause, thrown) { e ->
146 assertSame(thrown, e)
147 assertNull(e.cause)
148 assertTrue(e.suppressed().isEmpty())
149 }
150 }
151
152 @Test
153 fun testConflictingCancellationWithSameException() = runTest {
154 val cancellationCause = IllegalStateException()
155 val thrown = CancellationException()
156 /*
157 * Continuation cancelled with ISE
158 * Continuation itself throws CE with the same ISE as a cause
159 * Result: CE(ISE)
160 */
161 thrown.initCause(cancellationCause)
162 runCancellation(coroutineContext, cancellationCause, thrown) { e ->
163 assertSame(thrown, e)
164 assertSame(cancellationCause, e.cause)
165 assertEquals(0, thrown.suppressed().size)
166
167 }
168 }
169
170 @Test
171 fun testThrowingCancellation() = runTest {
172 val thrown = CancellationException()
173 runThrowingContinuation(coroutineContext, thrown) { e ->
174 assertSame(thrown, e)
175 }
176 }
177
178 @Test
179 fun testThrowingCancellationWithCause() = runTest {
180 val thrown = CancellationException()
181 thrown.initCause(IOException())
182 runThrowingContinuation(coroutineContext, thrown) { e ->
183 assertSame(thrown, e)
184 }
185 }
186
187 @Test
188 fun testCancel() = runTest {
189 runOnlyCancellation(coroutineContext, null) { e ->
190 assertNull(e.cause)
191 assertTrue(e.suppressed().isEmpty())
192 }
193 }
194
195 @Test
196 fun testCancelWithCause() = runTest {
197 val cause = IOException()
198 runOnlyCancellation(coroutineContext, cause) { e ->
199 assertSame(cause, e.cause)
200 assertTrue(e.suppressed().isEmpty())
201 }
202 }
203
204 @Test
205 fun testCancelWithCancellationException() = runTest {
206 val cause = CancellationException()
207 runThrowingContinuation(coroutineContext, cause) { e ->
208 assertSame(cause, e)
209 assertNull(e.cause)
210 assertTrue(e.suppressed().isEmpty())
211 }
212 }
213
214 @Test
215 fun testMultipleCancellations() = runTest {
216 var continuation: Continuation<Unit>? = null
217
218 val job = launch(coroutineContext) {
219 try {
220 expect(2)
221 suspendCancellableCoroutine<Unit> { c ->
222 continuation = c
223 }
224 } catch (e: IOException) {
225 expect(3)
226 }
227 }
228
229 expect(1)
230 yield()
231 continuation!!.resumeWithException(IOException())
232 yield()
233 assertFailsWith<IllegalStateException> { continuation!!.resumeWithException(ClosedChannelException()) }
234 try {
235 job.join()
236 } finally {
237 finish(4)
238 }
239 }
240
241 @Test
242 fun testResumeAndCancel() = runTest {
243 var continuation: Continuation<Unit>? = null
244
245 val job = launch(coroutineContext) {
246 expect(2)
247 suspendCancellableCoroutine<Unit> { c ->
248 continuation = c
249 }
250 expect(3)
251 }
252
253 expect(1)
254 yield()
255 continuation!!.resume(Unit)
256 job.join()
257 assertFailsWith<IllegalStateException> { continuation!!.resumeWithException(ClosedChannelException()) }
258 finish(4)
259 }
260
261 private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
262 val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
263 return object : CoroutineDispatcher() {
264 override fun dispatch(context: CoroutineContext, block: Runnable) {
265 dispatcher.dispatch(context, block)
266 }
267 }
268 }
269
270 private suspend inline fun <reified T : Exception> CoroutineScope.runCancellation(
271 coroutineContext: CoroutineContext,
272 cancellationCause: Exception?,
273 thrownException: T, exceptionChecker: (T) -> Unit
274 ) {
275
276 expect(1)
277 val job = Job()
278 job.cancel(cancellationCause)
279
280 try {
281 withContext(wrapperDispatcher(coroutineContext) + job, CoroutineStart.ATOMIC) {
282 require(isActive)
283 expect(2)
284 throw thrownException
285 }
286 } catch (e: Exception) {
287 assertTrue(e is T)
288 exceptionChecker(e as T)
289 finish(3)
290 return
291 }
292
293 fail()
294 }
295
296 private suspend inline fun <reified T : Exception> CoroutineScope.runThrowingContinuation(
297 coroutineContext: CoroutineContext,
298 thrownException: T, exceptionChecker: (T) -> Unit
299 ) {
300
301 expect(1)
302 try {
303 withContext(wrapperDispatcher(coroutineContext), CoroutineStart.ATOMIC) {
304 require(isActive)
305 expect(2)
306 throw thrownException
307 }
308 } catch (e: Exception) {
309 assertTrue(e is T)
310 exceptionChecker(e as T)
311 finish(3)
312 return
313 }
314
315 fail()
316 }
317
318 private suspend inline fun CoroutineScope.runOnlyCancellation(
319 coroutineContext: CoroutineContext,
320 cancellationCause: Exception?, exceptionChecker: (CancellationException) -> Unit
321 ) {
322
323 expect(1)
324 val job = Job()
325 job.cancel(cancellationCause)
326 try {
327 withContext(wrapperDispatcher(coroutineContext) + job, CoroutineStart.ATOMIC) {
328 require(isActive)
329 expect(2)
330 }
331 } catch (e: Exception) {
332 assertTrue(e is CancellationException)
333 exceptionChecker(e as CancellationException)
334 finish(3)
335 return
336 }
337
338 fail()
339 }
340}
341
342// Workaround to run tests on JDK 8, this test is excluded from jdk16Test task
343fun Throwable.suppressed(): Array<Throwable> {
344 val method = this::class.java.getMethod("getSuppressed") ?: error("This test can only be run using JDK 1.8")
345 return method.invoke(this) as Array<Throwable>
346}