5
5
package kotlinx.coroutines.reactor
6
6
7
7
import kotlinx.coroutines.*
8
+ import kotlinx.coroutines.CancellationException
8
9
import kotlinx.coroutines.flow.*
9
10
import kotlinx.coroutines.reactive.*
10
11
import org.junit.*
@@ -14,13 +15,15 @@ import reactor.core.publisher.*
14
15
import reactor.util.context.*
15
16
import java.time.*
16
17
import java.time.Duration.*
18
+ import java.util.concurrent.*
17
19
import java.util.function.*
18
20
import kotlin.test.*
19
21
20
22
class MonoTest : TestBase () {
21
23
@Before
22
24
fun setup () {
23
25
ignoreLostThreads(" timer-" , " parallel-" )
26
+ Hooks .onErrorDropped { expectUnreached() }
24
27
}
25
28
26
29
@Test
@@ -286,17 +289,57 @@ class MonoTest : TestBase() {
286
289
}
287
290
}
288
291
292
+ /* * Test that cancelling a [mono] due to a timeout does throw an exception. */
289
293
@Test
290
294
fun testTimeout () {
291
295
val mono = mono {
292
296
withTimeout(1 ) { delay(100 ) }
293
297
}
294
- mono.doOnSubscribe { expect(1 ) }
298
+ try {
299
+ mono.doOnSubscribe { expect(1 ) }
300
+ .doOnNext { expectUnreached() }
301
+ .doOnSuccess { expectUnreached() }
302
+ .doOnError { expect(2 ) }
303
+ .doOnCancel { expectUnreached() }
304
+ .block()
305
+ } catch (e: CancellationException ) {
306
+ expect(3 )
307
+ }
308
+ finish(4 )
309
+ }
310
+
311
+ /* * Test that when the reason for cancellation of a [mono] is that the downstream doesn't want its results anymore,
312
+ * this is considered normal behavior and exceptions are not propagated. */
313
+ @Test
314
+ fun testDownstreamCancellationDoesNotThrow () = runTest {
315
+ /* * Attach a hook that handles exceptions from publishers that are known to be disposed of. We don't expect it
316
+ * to be fired in this case, as the reason for the publisher in this test to accept an exception is simply
317
+ * cancellation from the downstream. */
318
+ Hooks .onOperatorError(" testDownstreamCancellationDoesNotThrow" ) { t, a ->
319
+ expectUnreached()
320
+ t
321
+ }
322
+ /* * A Mono that doesn't emit a value and instead waits indefinitely. */
323
+ val mono = mono { expect(3 ); delay(Long .MAX_VALUE ) }
324
+ .doOnSubscribe { expect(2 ) }
295
325
.doOnNext { expectUnreached() }
296
- .doOnSuccess { expect( 2 ) }
326
+ .doOnSuccess { expectUnreached( ) }
297
327
.doOnError { expectUnreached() }
298
- .doOnCancel { expectUnreached() }
299
- .block()
300
- finish(3 )
328
+ .doOnCancel { expect(4 ) }
329
+ /* * There are no guarantees about the execution context in which the cancellation handler will run, but we have
330
+ * to somehow make sure that [Hooks.resetOnOperatorError] occurs after that, as otherwise, the test will pass
331
+ * successfully even for an incorrect implementation. This contraption seems to ensure that the cancellation
332
+ * handler does complete before [finish] is called. */
333
+ newSingleThreadContext(" testDownstreamCancellationDoesNotThrow" ).use { pool ->
334
+ val job = launch(pool, start = CoroutineStart .UNDISPATCHED ) {
335
+ expect(1 )
336
+ mono.await()
337
+ }
338
+ launch(pool) {
339
+ job.cancelAndJoin()
340
+ }
341
+ }.join()
342
+ finish(5 )
343
+ Hooks .resetOnOperatorError(" testDownstreamCancellationDoesNotThrow" )
301
344
}
302
345
}
0 commit comments