From 65b7cab952ca64ee79fdc5b5e623a64e564a8372 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Tue, 3 Nov 2026 16:04:57 +0100 Subject: [PATCH 1/2] JNI: cancelling an async function's future cancels the Swift task The thunk for an exported `async` function kept its Task only in a local, and the Java method returned a derived future (`thenApply`), whose cancellation doesn't reach its source. Cancelling the returned future left the Swift work running. - SwiftKitCore's `CompletableFutureCancellation` forwards cancellation from the returned future to the source future, and cancels/releases a Swift task handle when the source future completes. - SwiftJavaRuntimeSupport's `_cancelTaskWhenFutureIsCancelled` registers the task with the future the native call received. - In `completableFuture` mode the generated Java forwards cancellation and the Swift thunk registers the task. `legacyFuture` mode is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../MySwiftLibrary/AsyncCancellation.swift | 37 +++++++++ .../java/com/example/swift/AsyncTest.java | 18 ++++- ...ISwift2JavaGenerator+JavaTranslation.swift | 29 ++++--- ...wift2JavaGenerator+NativeTranslation.swift | 16 +++- .../CompletableFutureAwait.swift | 55 ++++++++++++++ .../core/CompletableFutureCancellation.java | 55 ++++++++++++++ .../JNI/JNIActorTests.swift | 12 +-- .../JNI/JNIAsyncCancellationTests.swift | 76 +++++++++++++++++++ .../JNI/JNIAsyncTests.swift | 32 +++++--- .../JNI/JNIDistributedActorTests.swift | 4 +- 10 files changed, 303 insertions(+), 31 deletions(-) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCancellation.swift create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureCancellation.java create mode 100644 Tests/JExtractSwiftTests/JNI/JNIAsyncCancellationTests.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCancellation.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCancellation.swift new file mode 100644 index 000000000..eaabac058 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCancellation.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Synchronization + +/// Cancelling the Java future returned for an `async` function cancels the Swift task. +public final class CancellableWork: Sendable { + private let cancelled = Mutex(false) + + public init() {} + + /// Sleeps until cancelled; returns whether it was. + public func waitUntilCancelled() async -> Bool { + do { + try await Task.sleep(for: .seconds(30)) + return false + } catch { + cancelled.withLock { $0 = true } + return true + } + } + + public func wasCancelled() -> Bool { + cancelled.withLock { $0 } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java index 4ff86284d..34bf8ea24 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -87,4 +87,20 @@ void asyncString() throws Exception { Future future = MySwiftLibrary.asyncString("hey"); assertEquals("hey", future.get()); } -} \ No newline at end of file + + @Test + void cancellingTheFutureCancelsTheSwiftTask() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + CancellableWork work = CancellableWork.init(arena); + var future = work.waitUntilCancelled(); + Thread.sleep(100); + assertTrue(future.cancel(true)); + + long deadline = System.currentTimeMillis() + 5_000; + while (!work.wasCancelled() && System.currentTimeMillis() < deadline) { + Thread.sleep(20); + } + assertTrue(work.wasCancelled()); + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 65f0f0f58..bea4777ab 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -741,21 +741,29 @@ extension JNISwift2JavaGenerator { ) let result = translatedFunctionSignature.result + let convertedFuture = JavaNativeConversionStep.method( + .constant("future$"), + function: "thenApply", + arguments: [ + .lambda( + args: ["futureResult$"], + body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$") + ) + ] + ) translatedFunctionSignature.result = TranslatedResult( javaType: translatedFutureType, nativeJavaType: .void, annotations: result.annotations, outParameters: result.outParameters + [futureOutParameter], - conversion: .method( - .constant("future$"), - function: "thenApply", - arguments: [ - .lambda( - args: ["futureResult$"], - body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$") - ) - ] - ) + // Cancelling the returned (derived) future must reach `future$`, which the Swift task watches. + conversion: mode == .completableFuture + ? .method( + .constant("org.swift.swiftkit.core.CompletableFutureCancellation"), + function: "forwardCancellation", + arguments: [convertedFuture, .constant("future$")] + ) + : convertedFuture ) // Update native function @@ -765,6 +773,7 @@ extension JNISwift2JavaGenerator { isThrowing: originalFunctionSignature.isThrowing || originalFunctionSignature.isImplicitlyThrowing, completeMethodID: completeMethodID, completeExceptionallyMethodID: completeExceptionallyMethodID, + cancelsTaskWithFuture: mode == .completableFuture, ) nativeFunctionSignature.result.javaType = .void nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index c3b4c580a..3e6cb2223 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1465,7 +1465,8 @@ extension JNISwift2JavaGenerator { nativeFunctionSignature: NativeFunctionSignature, isThrowing: Bool, completeMethodID: String, - completeExceptionallyMethodID: String + completeExceptionallyMethodID: String, + cancelsTaskWithFuture: Bool ) /// `{ (args) -> return body }` @@ -1968,7 +1969,8 @@ extension JNISwift2JavaGenerator { let nativeFunctionSignature, let isThrowing, let completeMethodID, - let completeExceptionallyMethodID + let completeExceptionallyMethodID, + let cancelsTaskWithFuture ): var globalRefs: [String] = ["globalFuture"] @@ -2101,6 +2103,16 @@ extension JNISwift2JavaGenerator { } } + if cancelsTaskWithFuture { + // Cancelling the Java future cancels the task. Registered with the call's own (local) + // reference, which stays valid even if an immediate task already finished. + printer.printIfBlock("let task") { printer in + printer.print( + "SwiftJavaRuntimeSupport._cancelTaskWhenFutureIsCancelled(result_future, task: task, environment: environment)" + ) + } + } + return "" case .closure(let args, let body): diff --git a/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift b/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift index cc595d5cd..fc717c177 100644 --- a/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift +++ b/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift @@ -102,3 +102,58 @@ public func Java_org_swift_swiftkit_core_CompletableFutureContinuation__00024res ) ) } + +// ==== ----------------------------------------------------------------------- +// MARK: Cancelling the Swift task behind an exported `async` function + +@JavaClass("org.swift.swiftkit.core.CompletableFutureCancellation") +class JavaCompletableFutureCancellation: JavaObject {} + +extension JavaClass { + @JavaStaticMethod + func cancelTaskWhenCancelled(_ future: JavaCompletableFuture?, _ task: Int64) +} + +private final class SwiftTaskHandle: @unchecked Sendable { + let task: Task + + init(_ task: Task) { + self.task = task + } +} + +/// Cancels `task` when the Java `future` it completes is cancelled. +/// +/// Used by generated code for `async` functions exported to Java. Pass the future the native call +/// received, which stays valid for the duration of the call even if the task has already finished. +public func _cancelTaskWhenFutureIsCancelled( + _ future: jobject?, + task: Task, + environment: JNIEnvironment +) { + guard let future else { return } + let handle = Unmanaged.passRetained(SwiftTaskHandle(task)).toOpaque() + let cancellationClass = try! JavaClass(environment: environment) + cancellationClass.cancelTaskWhenCancelled( + JavaCompletableFuture(javaThis: future, environment: environment), + Int64(Int(bitPattern: handle)) + ) +} + +@_cdecl("Java_org_swift_swiftkit_core_CompletableFutureCancellation__00024cancelTask") +public func Java_org_swift_swiftkit_core_CompletableFutureCancellation__00024cancelTask( + environment: UnsafeMutablePointer!, + thisClass: jclass, + task: jlong +) { + Unmanaged.fromOpaque(UnsafeRawPointer(bitPattern: Int(task))!).takeUnretainedValue().task.cancel() +} + +@_cdecl("Java_org_swift_swiftkit_core_CompletableFutureCancellation__00024releaseTask") +public func Java_org_swift_swiftkit_core_CompletableFutureCancellation__00024releaseTask( + environment: UnsafeMutablePointer!, + thisClass: jclass, + task: jlong +) { + Unmanaged.fromOpaque(UnsafeRawPointer(bitPattern: Int(task))!).release() +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureCancellation.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureCancellation.java new file mode 100644 index 000000000..84204754a --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureCancellation.java @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import java.util.concurrent.CompletableFuture; + +/** + * Propagates cancellation of the {@link CompletableFuture} returned for an {@code async} Swift + * function to the Swift {@code Task} running it. + */ +public final class CompletableFutureCancellation { + private CompletableFutureCancellation() {} + + /** + * Cancels {@code source} when {@code result}, a future derived from it, is cancelled. + * Cancelling a derived future (e.g. from {@code thenApply}) does not otherwise reach its source. + */ + public static CompletableFuture forwardCancellation(CompletableFuture result, CompletableFuture source) { + result.whenComplete((value, error) -> { + if (result.isCancelled()) { + source.cancel(true); + } + }); + return result; + } + + /** + * Cancels the Swift task at {@code task} if {@code future} is cancelled, and releases the task + * handle once {@code future} completes in any way. + */ + public static void cancelTaskWhenCancelled(CompletableFuture future, long task) { + future.whenComplete((value, error) -> { + if (future.isCancelled()) { + $cancelTask(task); + } + $releaseTask(task); + }); + } + + private static native void $cancelTask(long task); + + private static native void $releaseTask(long task); +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift index aca720352..e798ea3cf 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift @@ -45,10 +45,10 @@ struct JNIActor1MethodsTests { public java.util.concurrent.CompletableFuture hello() { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); K.$hello(this.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ @@ -93,10 +93,10 @@ struct JNIActor1MethodsTests { public java.util.concurrent.CompletableFuture hi() { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); K.$hi(this.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ @@ -237,10 +237,10 @@ struct JNIActor1MethodsTests { public java.util.concurrent.CompletableFuture concurrentlyAsync() { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); K.$concurrentlyAsync(this.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncCancellationTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncCancellationTests.swift new file mode 100644 index 000000000..22c57a5e8 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncCancellationTests.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +/// Cancelling the future returned for an exported `async` function cancels the Swift task. +@Suite +struct JNIAsyncCancellationTests { + @Test + func returnedFutureForwardsCancellation() throws { + try assertOutput( + input: "public func work() async -> Int64 { 0 }", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> {", + "), future$);", + ] + ) + } + + @Test + func taskIsRegisteredWithTheFuture() throws { + try assertOutput( + input: "public func work() async -> Int64 { 0 }", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + if let task { + SwiftJavaRuntimeSupport._cancelTaskWhenFutureIsCancelled(result_future, task: task, environment: environment) + } + """ + ] + ) + } + + @Test + func legacyFutureModeIsUnchanged() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + try assertOutput( + input: "public func work() async -> Int64 { 0 }", + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [], + notExpectedChunks: ["_cancelTaskWhenFutureIsCancelled"] + ) + try assertOutput( + input: "public func work() async -> Int64 { 0 }", + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [], + notExpectedChunks: ["forwardCancellation"] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index 866c542d4..5f145086b 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -37,10 +37,10 @@ struct JNIAsyncTests { public static java.util.concurrent.CompletableFuture asyncVoid() { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); SwiftModule.$asyncVoid(future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ @@ -89,6 +89,9 @@ struct JNIAsyncTests { _ = environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)]) } } + if let task { + SwiftJavaRuntimeSupport._cancelTaskWhenFutureIsCancelled(result_future, task: task, environment: environment) + } } """ ] @@ -113,10 +116,10 @@ struct JNIAsyncTests { public static java.util.concurrent.CompletableFuture async() { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); SwiftModule.$async(future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ @@ -179,6 +182,9 @@ struct JNIAsyncTests { } } } + if let task { + SwiftJavaRuntimeSupport._cancelTaskWhenFutureIsCancelled(result_future, task: task, environment: environment) + } } """ ] @@ -203,10 +209,10 @@ struct JNIAsyncTests { public static java.util.concurrent.CompletableFuture async(long i) { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); SwiftModule.$async(i, future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ @@ -257,6 +263,9 @@ struct JNIAsyncTests { _ = environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) } } + if let task { + SwiftJavaRuntimeSupport._cancelTaskWhenFutureIsCancelled(result_future, task: task, environment: environment) + } return } """ @@ -286,10 +295,10 @@ struct JNIAsyncTests { public static java.util.concurrent.CompletableFuture async(MyClass c, SwiftArena swiftArena) { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); SwiftModule.$async(c.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena); } - ); + ), future$); } """, """ @@ -356,6 +365,9 @@ struct JNIAsyncTests { _ = environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) } } + if let task { + SwiftJavaRuntimeSupport._cancelTaskWhenFutureIsCancelled(result_future, task: task, environment: environment) + } return } """ @@ -377,10 +389,10 @@ struct JNIAsyncTests { public static java.util.concurrent.CompletableFuture async(java.lang.String s) { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); SwiftModule.$async(s, future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift index 6b16459de..02783a639 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift @@ -36,10 +36,10 @@ struct JNIDistributedActorTests { public java.util.concurrent.CompletableFuture hi() { java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); D.$hi(this.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { + return org.swift.swiftkit.core.CompletableFutureCancellation.forwardCancellation(future$.thenApply((futureResult$) -> { return futureResult$; } - ); + ), future$); } """, """ From aafa76a5839c374d1e5fc3f8160ed5f907abd1c0 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Tue, 3 Nov 2026 16:04:57 +0100 Subject: [PATCH 2/2] Samples: cancelling a Kotlin coroutine cancels the Swift task it awaits Co-Authored-By: Claude Opus 5.5 --- .../KotlinSampleLibrary/Cancellation.swift | 36 +++++++++++++++++++ .../com/example/kotlin/CancellationTest.kt | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Cancellation.swift create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/CancellationTest.kt diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Cancellation.swift b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Cancellation.swift new file mode 100644 index 000000000..831f158a0 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Cancellation.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Synchronization + +/// Long-running async work: cancelling the Kotlin coroutine awaiting it cancels the Swift task. +public final class Download: Sendable { + private let cancelled = Mutex(false) + + public init() {} + + public func run() async -> Bool { + do { + try await Task.sleep(for: .seconds(30)) + return false + } catch { + cancelled.withLock { $0 = true } + return true + } + } + + public func wasCancelled() -> Bool { + cancelled.withLock { $0 } + } +} diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/CancellationTest.kt b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/CancellationTest.kt new file mode 100644 index 000000000..71d2d02b1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/CancellationTest.kt @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.kotlin + +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class CancellationTest { + @Test + fun cancellingTheCoroutineCancelsTheSwiftTask() = runBlocking { + val download = Download.init() + // await() cancels the future when its coroutine is cancelled, here by the timeout. + val result = withTimeoutOrNull(100) { download.run().await() } + assertNull(result) + + val deadline = System.currentTimeMillis() + 5_000 + while (!download.wasCancelled() && System.currentTimeMillis() < deadline) Thread.sleep(20) + assertTrue(download.wasCancelled()) + } +}