Add CompletableFuture.await() helper for Kotlin clients

Test it by porting several tests from FutureTest.java to Kotlin and
using Kotlin idioms for awaiting and cancellation.
This commit is contained in:
Jordan Rose
2025-06-05 12:34:15 -07:00
committed by GitHub
parent ef87636ed0
commit f40d20a72f
6 changed files with 221 additions and 1 deletions

View File

@@ -1,7 +1,8 @@
import groovy.json.JsonSlurper
plugins {
id 'com.android.library' version '8.9.0'
id 'com.android.library'
id 'kotlin-android'
id 'maven-publish'
id 'signing'
}
@@ -43,6 +44,10 @@ android {
srcDir '../client/src/test/java'
srcDir '../shared/test/java'
}
kotlin {
srcDir '../client/src/test/kotlin'
srcDir '../shared/test/kotlin'
}
resources {
srcDir '../client/src/test/resources'
}
@@ -110,9 +115,12 @@ File findRustlsPlatformVerifierClasses() {
dependencies {
implementation files(findRustlsPlatformVerifierClasses())
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'com.googlecode.json-simple:json-simple:1.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
androidTestImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
api project(':client')
}

View File

@@ -7,6 +7,10 @@ plugins {
id "com.diffplug.spotless" version "6.20.0"
id "io.github.gradle-nexus.publish-plugin" version "2.0.0"
id "org.jetbrains.kotlin.jvm" version "2.1.0"
// These plugins need to be loaded together, so we must declare them up front.
id 'com.android.library' version "8.9.0" apply false
id 'org.jetbrains.kotlin.android' version "2.1.0" apply false
}
allprojects {

View File

@@ -55,6 +55,9 @@ sourceSets {
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'com.googlecode.json-simple:json-simple:1.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
testImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
}
test {

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.libsignal.internal
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Awaits for completion of this CompletableFuture without blocking a thread.
*
* This suspending function is cancellable. If the coroutine is cancelled while
* this function is suspended, the future will be cancelled as well.
*
* @return The result value of the CompletableFuture
* @throws Exception if the CompletableFuture completed exceptionally
* @throws CancellationException if the coroutine was cancelled
*/
suspend fun <T> CompletableFuture<T>.await(): T = suspendCancellableCoroutine { c ->
// From https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellable-continuation/
val future = this
future.whenComplete { result, throwable ->
if (throwable != null) {
// Resume continuation with an exception if an external source failed
c.resumeWithException(throwable)
} else {
// Resume continuation with a value if it was computed
c.resume(result)
}
}
// Cancel the computation if the continuation itself was cancelled because a caller of 'await' is cancelled
c.invokeOnCancellation {
future.cancel(true)
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.libsignal.internal
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import java.util.concurrent.CancellationException
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class AsyncTests {
private var ioRuntime: Long = 0
@BeforeTest
fun initIoRuntime() {
ioRuntime = NativeTesting.TESTING_NonSuspendingBackgroundThreadRuntime_New()
}
@AfterTest
fun destroyIoRuntime() {
NativeTesting.TESTING_NonSuspendingBackgroundThreadRuntime_Destroy(ioRuntime)
ioRuntime = 0
}
@Test
fun testSuccessFromRust() = runTest {
val result = NativeTesting.TESTING_FutureSuccess(ioRuntime, 21).await()
assertEquals(42, result)
}
@Test
fun testFailureFromRust() = runTest {
assertFailsWith<IllegalArgumentException> {
NativeTesting.TESTING_FutureFailure(ioRuntime, 21).await()
}
}
@Test
fun testFutureOnlyCompletesByCancellation() = runTest(timeout = 5.seconds) {
val context = TokioAsyncContext()
val counter =
object : NativeHandleGuard.SimpleOwner(
NativeTesting.TESTING_FutureCancellationCounter_Create(0),
) {
override fun release(nativeHandle: Long) {
NativeTesting.TestingFutureCancellationCounter_Destroy(nativeHandle)
}
}
val testFuture =
context
.guardedMap { nativeContextHandle: Long ->
counter.guardedMap { counterHandle: Long ->
NativeTesting.TESTING_FutureIncrementOnCancel(
nativeContextHandle,
counterHandle,
)
}
}
.makeCancelable(context)
assertFailsWith<TimeoutCancellationException> {
withTimeout(20.milliseconds) { testFuture.await() }
}
assertTrue(testFuture.isCancelled)
assertTrue(testFuture.isDone)
assertFailsWith<CancellationException> { testFuture.await() }
// Hangs if the count never gets incremented.
context
.guardedMap { nativeContextHandle: Long ->
counter.guardedMap { counterHandle: Long ->
NativeTesting.TESTING_FutureCancellationCounter_WaitForCount(
nativeContextHandle,
counterHandle,
1,
)
}
}
.await()
}
}

View File

@@ -3354,6 +3354,25 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="e762b8c45690ae8a6a35df584f54be9c9da65885e61a905426aeafca5937e1ce" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-test" version="2.1.0">
<artifact name="kotlin-test-2.1.0-all.jar">
<sha256 value="ba813d25e9ebe67d750b98920694ad42bf9a80c12e6d25066d1c8e974ad89656" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-test-2.1.0.jar">
<sha256 value="6f9818fa182de3c68d19418997a7d5d9f4d7dc10be7ff203dec80e3a3d6238e6" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-test-2.1.0.module">
<sha256 value="f6e3ef22058e1bfa7d6fe42a352ec7449e8c6bb885812f571495b095a0b72176" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-test-junit" version="2.1.0">
<artifact name="kotlin-test-junit-2.1.0.jar">
<sha256 value="4fed5ed01c6ff2fad07a517b986938db5eeb8891a363609077a5257662fbcca5" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-test-junit-2.1.0.module">
<sha256 value="b44c2639a826c721026cf89b8a5082f535457d81f389949829681847a8cf793f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-tooling-core" version="2.1.0">
<artifact name="kotlin-tooling-core-2.1.0.jar">
<sha256 value="4176c612098cb92df38a485ff8b10aaa24abb400f610d48f5088aeb07c8002c8" origin="Generated by Gradle"/>
@@ -3386,6 +3405,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="e329322576dc1ca8cbe20a3afe4fd1f5724da3b95349b5538cd03b184ff971c2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.android" name="org.jetbrains.kotlin.android.gradle.plugin" version="2.1.0">
<artifact name="org.jetbrains.kotlin.android.gradle.plugin-2.1.0.pom">
<sha256 value="96e007b3ecb22cc6d9617e414484539fe5b77b28e6659c8c916b4fddf8961ced" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.jvm" name="org.jetbrains.kotlin.jvm.gradle.plugin" version="2.1.0">
<artifact name="org.jetbrains.kotlin.jvm.gradle.plugin-2.1.0.pom">
<sha256 value="df8026dca84edbee58086307b72bd1cb58cc4b2324989d832e2201997e5c5ff7" origin="Generated by Gradle"/>
@@ -3396,6 +3420,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5c876ef7e1ee07dd30721c43b46ead8eda23ce2b22260e70f933cbe004e80607" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="atomicfu" version="0.23.1">
<artifact name="atomicfu-0.23.1.module">
<sha256 value="3e891fe636b55108192100fcf38b1a39bcd1c2533e23c462fc07644eeafcb20f" origin="Generated by Gradle"/>
</artifact>
<artifact name="atomicfu-metadata-0.23.1.jar">
<sha256 value="7db8660ebe4b91bb478edb3616c4e3a50ba59c07dca517d1e1284c03fe86ac57" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="atomicfu-jvm" version="0.22.0">
<artifact name="atomicfu-jvm-0.22.0.jar">
<sha256 value="2da073727f3ab5e5584e74c12e11519c908ae2dfaf6aeb25ded42b6682297882" origin="Generated by Gradle"/>
@@ -3404,6 +3436,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="35070f923ce69f87c6f90e5305720e2704409b69a2374492ac45be70075ee49a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.10.2">
<artifact name="kotlinx-coroutines-android-1.10.2.jar">
<sha256 value="e713f1f874244115a07571065cffa0f24f5e78300e9720fea16de3af1d75fd41" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-android-1.10.2.module">
<sha256 value="092fe38103eec62e94540ca0cd61039ef8f7d8e46694ec033be1f63f0ea2013d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.10.2">
<artifact name="kotlinx-coroutines-bom-1.10.2.pom">
<sha256 value="faf0c6538e53ddc0499a63664d8e763c216580b2e18e722ccbdf1b431a6afe26" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.6.4">
<artifact name="kotlinx-coroutines-bom-1.6.4.pom">
<sha256 value="ab2614855fba66aa8a42514dbe3d5a884315ffe1ed63f5932e710a8006245ce1" origin="Generated by Gradle"/>
@@ -3414,6 +3459,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="4e5d1900e6379ef3f5970d04a8f30529adc82f859e8cc107c21ce8149ef666c4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.10.2">
<artifact name="kotlinx-coroutines-core-1.10.2.module">
<sha256 value="8fe254177e711a7cd18a3c06d8242fce945f41c2cca13dc19b33ae42a5435016" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-core-metadata-1.10.2.jar">
<sha256 value="319b653009d49c70982f98df29cc84fc7025b092cb0571c8e7532e3ad4366dae" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.6.4">
<artifact name="kotlinx-coroutines-core-1.6.4.module">
<sha256 value="a6eed4a1835588e7c84fcd7b0475fce9a7b3444c870ebc797b88ba64ccf4576b" origin="Generated by Gradle"/>
@@ -3424,6 +3477,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="7fb162396594ec28e1b6a4411b457949a7670f5e12019176774e1fd6b9471bbf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.10.2">
<artifact name="kotlinx-coroutines-core-jvm-1.10.2.jar">
<sha256 value="5ca175b38df331fd64155b35cd8cae1251fa9ee369709b36d42e0a288ccce3fd" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-core-jvm-1.10.2.module">
<sha256 value="e9e4a74b4dbfe0f5ebeed88d49f3546c3ec3089419b20e5250403135c2c64c53" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.6.4">
<artifact name="kotlinx-coroutines-core-jvm-1.6.4.jar">
<sha256 value="c24c8bb27bb320c4a93871501a7e5e0c61607638907b197aef675513d4c820be" origin="Generated by Gradle"/>
@@ -3440,6 +3501,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="34d6ee99b76ac062b51555b4a70be18349fe5566da79a190614f171c80b6538e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-test" version="1.10.2">
<artifact name="kotlinx-coroutines-test-1.10.2.module">
<sha256 value="422072cee3b69f68d5b1503bb6651be78946b04c39284a1cc026cce8bdf1f806" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-test-metadata-1.10.2.jar">
<sha256 value="fadbe04fbda7a27728770d8eaecbdec0a9b0b29693c20cbea77655d783d8bd78" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-test-jvm" version="1.10.2">
<artifact name="kotlinx-coroutines-test-jvm-1.10.2.jar">
<sha256 value="590a549f8c1db590c9d98a8a20424a1f581a34162a369e6a6bd884ce7d36d3d7" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-test-jvm-1.10.2.module">
<sha256 value="56b20817cc51ad88bdb59c01216b09897cd4fa698d517bb477d92a972a7a1aaf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-metadata-jvm" version="0.6.0">
<artifact name="kotlinx-metadata-jvm-0.6.0.jar">
<sha256 value="a20b73b2b30ba6e08a5ffc990b3db9abd0649e42c79ff5da38d22040a3284068" origin="Generated by Gradle"/>