mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-25 17:25:18 +02:00
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:
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user