diff --git a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt index 6d1fab3d69..9590dc0724 100644 --- a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt +++ b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt @@ -385,9 +385,24 @@ private class JobListenableFuture(private val jobToCancel: Job): ListenableFu // this Future hasn't itself been successfully cancelled, the Future will return // isCancelled() == false. This is the only discovered way to reconcile the two different // cancellation contracts. - return auxFuture.isCancelled || (isDone && Uninterruptibles.getUninterruptibly(auxFuture) is Cancelled) + return auxFuture.isCancelled || auxFuture.completedWithCancellation } + /** + * Helper for [isCancelled] that takes into account that + * our auxiliary future can complete with [Cancelled] instance. + */ + private val SettableFuture<*>.completedWithCancellation: Boolean + get() = isDone && try { + Uninterruptibles.getUninterruptibly(this) is Cancelled + } catch (e: CancellationException) { + true + } catch (t: Throwable) { + // In theory appart from CancellationException, getUninterruptibly can only + // throw ExecutionException, but to be safe we catch Throwable here. + false + } + /** * Waits for [auxFuture] to complete by blocking, then uses its `result` * to get the `T` value `this` [ListenableFuture] is pointing to or throw a [CancellationException]. diff --git a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt index dc2d99d7f7..2211295b76 100644 --- a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt +++ b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt @@ -680,6 +680,33 @@ class ListenableFutureTest : TestBase() { finish(5) } + @Test + fun testFutureCompletedExceptionally() = runTest { + val testException = TestException() + // NonCancellable to not propagate error to this scope. + val future = future(context = NonCancellable) { + throw testException + } + yield() + assertTrue(future.isDone) + assertFalse(future.isCancelled) + val thrown = assertFailsWith { future.get() } + assertEquals(testException, thrown.cause) + } + + @Test + fun testAsListenableFutureCompletedExceptionally() = runTest { + val testException = TestException() + val deferred = CompletableDeferred().apply { + completeExceptionally(testException) + } + val asListenableFuture = deferred.asListenableFuture() + assertTrue(asListenableFuture.isDone) + assertFalse(asListenableFuture.isCancelled) + val thrown = assertFailsWith { asListenableFuture.get() } + assertEquals(testException, thrown.cause) + } + private inline fun ListenableFuture<*>.checkFutureException() { val e = assertFailsWith { get() } val cause = e.cause!!