From 59165d218a8efe65b8d7a92c99d40d05304e785e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 15:43:11 -0500 Subject: [PATCH 01/12] Use `trylock` to eliminate the remaining race condition in `Test.cancel()`. This PR fixes the race condition in `Test.cancel()` that could occur if an unstructured task, created from within a test's task, called `Test.cancel()` at just the right moment. The order of events for the race is: - Unstructured task is created and inherits task-locals including the reference to the test's unsafe current task; - Test's task starts tearing down; - Unstructured task calls `takeUnsafeCurrentTask()` and gets a reference to the unsafe current task; - Test's task finishes tearing down; - Unstructured task calls `UnsafeCurrentTask.cancel()`. The fix is to use `trylock` semantics when cancelling the unsafe current task. If the test's task is still alive, the task is cancelled while the lock is held, which will block the test's task from being torn down as it has a lock-guarded call to clear the unsafe current task reference. If the test's task is no longer alive, the reference is already `nil` by the time the unstructured task acquires the lock and it bails early. If we recursively call `cancel()` (which can happen via the concurrency-level cancellation handler), the `trylock` means we won't acquire the lock a second time, so we won't end up deadlocking or aborting (which is what prevents calling `cancel()` while holding the lock in the current implementation). I hope some part of that made sense. --- Sources/Testing/Support/Locked.swift | 32 ++++++++++ Sources/Testing/Test+Cancellation.swift | 82 ++++++++++++++----------- 2 files changed, 79 insertions(+), 35 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index fac062adb..be7917a18 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -90,6 +90,38 @@ extension Locked { try _storage.mutex.withLock { rawValue in try body(&rawValue) } +#endif + } + + /// Try to acquire the lock and invoke a function while it is held. + /// + /// - Parameters: + /// - body: A closure to invoke while the lock is held. + /// + /// - Returns: Whatever is returned by `body`, or `nil` if the lock could not + /// be acquired. + /// + /// - Throws: Whatever is thrown by `body`. + /// + /// This function can be used to synchronize access to shared data from a + /// synchronous caller. Wherever possible, use actor isolation or other Swift + /// concurrency tools. + func withLockIfAvailable(_ body: (inout T) throws -> sending R) rethrows -> sending R? where R: ~Copyable { +#if SWT_TARGET_OS_APPLE && canImport(os) + nonisolated(unsafe) let result: R? = try _storage.withUnsafeMutablePointers { rawValue, lock in + guard os_unfair_lock_trylock(lock) else { + return nil + } + defer { + os_unfair_lock_unlock(lock) + } + return try body(&rawValue.pointee) + } + return result +#else + try _storage.mutex.withLockIfAvailable { rawValue in + try body(&rawValue) + } #endif } } diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 1ded9359f..25c5a0ffa 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -25,9 +25,8 @@ protocol TestCancellable: Sendable { // MARK: - Tracking the current task -/// A structure describing a reference to a task that is associated with some -/// ``TestCancellable`` value. -private struct _TaskReference: Sendable { +/// A structure that is able to cancel a task. +private struct _TaskCanceller: Sendable { /// The unsafe underlying reference to the associated task. private nonisolated(unsafe) var _unsafeCurrentTask = Locked() @@ -45,25 +44,46 @@ private struct _TaskReference: Sendable { _unsafeCurrentTask = withUnsafeCurrentTask { Locked(rawValue: $0) } } - /// Take this instance's reference to its associated task. - /// - /// - Returns: An `UnsafeCurrentTask` instance, or `nil` if it was already - /// taken or if it was never available. - /// - /// This function consumes the reference to the task. After the first call, - /// subsequent calls on the same instance return `nil`. - func takeUnsafeCurrentTask() -> UnsafeCurrentTask? { + /// Clear this instance's reference to its associated task without first + /// cancelling it. + func clear() { _unsafeCurrentTask.withLock { unsafeCurrentTask in - let result = unsafeCurrentTask unsafeCurrentTask = nil - return result } } + + /// Cancel this instance's associated task and clear the reference to it. + /// + /// - Returns: Whether or not this instance's task was cancelled. + /// + /// After the first call to this function _starts_, subsequent calls on the + /// same instance return `false`. In other words, if another thread calls this + /// function before it has returned (or the same thread calls it recursively), + /// it returns `false` without cancelling the task a second time. + func cancel(with skipInfo: SkipInfo) -> Bool { + // trylock means a recursive call to this function won't ruin our day, nor + // should interleaving locks. + _unsafeCurrentTask.withLockIfAvailable { unsafeCurrentTask in + defer { + unsafeCurrentTask = nil + } + if let unsafeCurrentTask { + // The task is still valid, so we'll cancel it. + $_currentSkipInfo.withValue(skipInfo) { + unsafeCurrentTask.cancel() + } + return true + } + + // The task has already been cancelled and/or cleared. + return false + } ?? false + } } -/// A dictionary of tracked tasks, keyed by types that conform to +/// A dictionary of cancellable tasks keyed by types that conform to /// ``TestCancellable``. -@TaskLocal private var _currentTaskReferences = [ObjectIdentifier: _TaskReference]() +@TaskLocal private var _currentTaskCancellers = [ObjectIdentifier: _TaskCanceller]() /// The instance of ``SkipInfo`` to propagate to children of the current task. /// @@ -87,16 +107,15 @@ extension TestCancellable { /// the current task, test, or test case is cancelled, it records a /// corresponding cancellation event. func withCancellationHandling(_ body: () async throws -> R) async rethrows -> R { - let taskReference = _TaskReference() - var currentTaskReferences = _currentTaskReferences - currentTaskReferences[ObjectIdentifier(Self.self)] = taskReference - return try await $_currentTaskReferences.withValue(currentTaskReferences) { - // Before returning, explicitly clear the stored task. This minimizes - // the potential race condition that can occur if test code creates an - // unstructured task and calls `Test.cancel()` in it after the test body - // has finished. + let taskCanceller = _TaskCanceller() + var currentTaskCancellers = _currentTaskCancellers + currentTaskCancellers[ObjectIdentifier(Self.self)] = taskCanceller + return try await $_currentTaskCancellers.withValue(currentTaskCancellers) { + // Before returning, explicitly clear the stored task so that an + // unstructured task that inherits the task local isn't able to + // accidentally cancel the task after it has been deallocated. defer { - _ = taskReference.takeUnsafeCurrentTask() + taskCanceller.clear() } return try await withTaskCancellationHandler { @@ -121,17 +140,10 @@ extension TestCancellable { /// - testAndTestCase: The test and test case to use when posting an event. /// - skipInfo: Information about the cancellation event. private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) where T: TestCancellable { - if cancellableValue != nil { - // If the current test case is still running, take its task property (which - // signals to subsequent callers that it has been cancelled.) - let task = _currentTaskReferences[ObjectIdentifier(T.self)]?.takeUnsafeCurrentTask() - - // If we just cancelled the current test case's task, post a corresponding - // event with the relevant skip info. - if let task { - $_currentSkipInfo.withValue(skipInfo) { - task.cancel() - } + if cancellableValue != nil, let taskCanceller = _currentTaskCancellers[ObjectIdentifier(T.self)] { + // Try to cancel the task associated with `T`, if any. If we succeed, post a + // corresponding event with the relevant skip info. + if taskCanceller.cancel(with: skipInfo) { Event.post(T.makeCancelledEventKind(with: skipInfo), for: testAndTestCase) } } else { From ee56a0fc7791dd9c44eb1488b6a8177d44809a41 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:11:31 -0500 Subject: [PATCH 02/12] Avoid abort on Linux when the current thread already owns the lock --- Sources/Testing/Support/Locked.swift | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index be7917a18..1e5f2e653 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -30,8 +30,16 @@ struct Locked { private final class _Storage { let mutex: Mutex +#if os(Linux) || os(Android) + // The Linux implementation of Mutex terminates if `_tryLock()` is called on + // the owning thread. (Other platforms just return `false`.) So, on Linux, + // we also track the thread ID of the owner. + let owningThreadID: Atomic +#endif + init(_ rawValue: consuming sending T) { mutex = Mutex(rawValue) + owningThreadID = Atomic(0) } } #endif @@ -88,6 +96,12 @@ extension Locked { return result #else try _storage.mutex.withLock { rawValue in +#if os(Linux) || os(Android) + _storage.owningThreadID.store(gettid(), ordering: .sequentiallyConsistent) + defer { + _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) + } +#endif try body(&rawValue) } #endif @@ -119,7 +133,20 @@ extension Locked { } return result #else +#if os(Linux) || os(Android) + let tid = gettid() + if _storage.owningThreadID.load(ordering: .sequentiallyConsistent) == tid { + // This thread already holds the lock. + return nil + } +#endif try _storage.mutex.withLockIfAvailable { rawValue in +#if os(Linux) || os(Android) + _storage.owningThreadID.store(tid, ordering: .sequentiallyConsistent) + defer { + _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) + } +#endif try body(&rawValue) } #endif From 9d6c9fb53336508956d4a21825e6f95819e29b6f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:24:18 -0500 Subject: [PATCH 03/12] Use pthread_t instead of gettid() since the latter requires _GNU_SOURCE --- Sources/Testing/Support/Locked.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index 1e5f2e653..f96b2c25e 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -34,7 +34,7 @@ struct Locked { // The Linux implementation of Mutex terminates if `_tryLock()` is called on // the owning thread. (Other platforms just return `false`.) So, on Linux, // we also track the thread ID of the owner. - let owningThreadID: Atomic + let owningThreadID: Atomic #endif init(_ rawValue: consuming sending T) { @@ -97,9 +97,9 @@ extension Locked { #else try _storage.mutex.withLock { rawValue in #if os(Linux) || os(Android) - _storage.owningThreadID.store(gettid(), ordering: .sequentiallyConsistent) + _storage.owningThreadID.store(pthread_self(), ordering: .sequentiallyConsistent) defer { - _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) + _storage.owningThreadID.store(nil, ordering: .sequentiallyConsistent) } #endif try body(&rawValue) @@ -134,7 +134,7 @@ extension Locked { return result #else #if os(Linux) || os(Android) - let tid = gettid() + let tid = pthread_self() if _storage.owningThreadID.load(ordering: .sequentiallyConsistent) == tid { // This thread already holds the lock. return nil @@ -144,7 +144,7 @@ extension Locked { #if os(Linux) || os(Android) _storage.owningThreadID.store(tid, ordering: .sequentiallyConsistent) defer { - _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) + _storage.owningThreadID.store(nil, ordering: .sequentiallyConsistent) } #endif try body(&rawValue) From 21255c0af4232d34c7c5a83a7e42fa8fec9a1495 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:24:32 -0500 Subject: [PATCH 04/12] Revert "Use pthread_t instead of gettid() since the latter requires _GNU_SOURCE" This reverts commit 9d6c9fb53336508956d4a21825e6f95819e29b6f. --- Sources/Testing/Support/Locked.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index f96b2c25e..1e5f2e653 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -34,7 +34,7 @@ struct Locked { // The Linux implementation of Mutex terminates if `_tryLock()` is called on // the owning thread. (Other platforms just return `false`.) So, on Linux, // we also track the thread ID of the owner. - let owningThreadID: Atomic + let owningThreadID: Atomic #endif init(_ rawValue: consuming sending T) { @@ -97,9 +97,9 @@ extension Locked { #else try _storage.mutex.withLock { rawValue in #if os(Linux) || os(Android) - _storage.owningThreadID.store(pthread_self(), ordering: .sequentiallyConsistent) + _storage.owningThreadID.store(gettid(), ordering: .sequentiallyConsistent) defer { - _storage.owningThreadID.store(nil, ordering: .sequentiallyConsistent) + _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) } #endif try body(&rawValue) @@ -134,7 +134,7 @@ extension Locked { return result #else #if os(Linux) || os(Android) - let tid = pthread_self() + let tid = gettid() if _storage.owningThreadID.load(ordering: .sequentiallyConsistent) == tid { // This thread already holds the lock. return nil @@ -144,7 +144,7 @@ extension Locked { #if os(Linux) || os(Android) _storage.owningThreadID.store(tid, ordering: .sequentiallyConsistent) defer { - _storage.owningThreadID.store(nil, ordering: .sequentiallyConsistent) + _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) } #endif try body(&rawValue) From e76d0ee4f3b7582be4860fc6581ef37413e65e5e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:26:25 -0500 Subject: [PATCH 05/12] Just declare gettid() --- Sources/_TestingInternals/include/Stubs.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..3c939ace6 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -152,6 +152,14 @@ static int swt_siginfo_t_si_status(const siginfo_t *siginfo) { #endif #endif +#if defined(__linux__) || defined(__ANDROID__) +/// Get the current thread's ID. +/// +/// This function is redeclared here because it is guarded by `_GNU_SOURCE` in +/// the platform headers. +SWT_EXTERN pid_t gettid(void); +#endif + /// Get the value of `EEXIST`. /// /// This function is provided because `EEXIST` is a complex macro in wasi-libc From 1b32337887ed3678a5bcc953149c2d6cc0aaa53c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:27:00 -0500 Subject: [PATCH 06/12] Returns --- Sources/Testing/Support/Locked.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index 1e5f2e653..9287c6a1b 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -102,7 +102,7 @@ extension Locked { _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) } #endif - try body(&rawValue) + return try body(&rawValue) } #endif } @@ -147,7 +147,7 @@ extension Locked { _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) } #endif - try body(&rawValue) + return try body(&rawValue) } #endif } From 8d69ae08e855c317d7f94502f3d04f92d4eedc3b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 21:28:43 +0000 Subject: [PATCH 07/12] Another return --- Sources/Testing/Support/Locked.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index 9287c6a1b..2c7b12d95 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -140,7 +140,7 @@ extension Locked { return nil } #endif - try _storage.mutex.withLockIfAvailable { rawValue in + return try _storage.mutex.withLockIfAvailable { rawValue in #if os(Linux) || os(Android) _storage.owningThreadID.store(tid, ordering: .sequentiallyConsistent) defer { From 26fb71d23b2628473a82a25407f948c121a163e5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:36:15 -0500 Subject: [PATCH 08/12] Just use pthread_mutex_t on Linux until the Swift issue is cleared up --- Sources/Testing/Support/Locked.swift | 68 +++++++++++++++------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index 9287c6a1b..a2be1a06a 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -26,20 +26,20 @@ struct Locked { /// A type providing storage for the underlying lock and wrapped value. #if SWT_TARGET_OS_APPLE && canImport(os) private typealias _Storage = ManagedBuffer +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + private final class _Storage: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { lock in + _ = lock.deinitialize(count: 1) + } + } + } #else private final class _Storage { let mutex: Mutex -#if os(Linux) || os(Android) - // The Linux implementation of Mutex terminates if `_tryLock()` is called on - // the owning thread. (Other platforms just return `false`.) So, on Linux, - // we also track the thread ID of the owner. - let owningThreadID: Atomic -#endif - init(_ rawValue: consuming sending T) { mutex = Mutex(rawValue) - owningThreadID = Atomic(0) } } #endif @@ -57,6 +57,11 @@ extension Locked: RawRepresentable { _storage.withUnsafeMutablePointerToElements { lock in lock.initialize(to: .init()) } +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) as! _Storage + _storage.withUnsafeMutablePointerToElements { lock in + _ = pthread_mutex_init(lock, nil) + } #else nonisolated(unsafe) let rawValue = rawValue _storage = _Storage(rawValue) @@ -85,26 +90,29 @@ extension Locked { /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { + nonisolated(unsafe) let result: R #if SWT_TARGET_OS_APPLE && canImport(os) - nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in + result = try _storage.withUnsafeMutablePointers { rawValue, lock in os_unfair_lock_lock(lock) defer { os_unfair_lock_unlock(lock) } return try body(&rawValue.pointee) } - return result -#else - try _storage.mutex.withLock { rawValue in -#if os(Linux) || os(Android) - _storage.owningThreadID.store(gettid(), ordering: .sequentiallyConsistent) +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + result = try _storage.withUnsafeMutablePointers { rawValue, lock in + pthread_mutex_lock(lock) defer { - _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) + pthread_mutex_unlock(lock) } -#endif + return try body(&rawValue.pointee) + } +#else + result = try _storage.mutex.withLock { rawValue in return try body(&rawValue) } #endif + return result } /// Try to acquire the lock and invoke a function while it is held. @@ -121,8 +129,9 @@ extension Locked { /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. func withLockIfAvailable(_ body: (inout T) throws -> sending R) rethrows -> sending R? where R: ~Copyable { + nonisolated(unsafe) let result: R? #if SWT_TARGET_OS_APPLE && canImport(os) - nonisolated(unsafe) let result: R? = try _storage.withUnsafeMutablePointers { rawValue, lock in + result = try _storage.withUnsafeMutablePointers { rawValue, lock in guard os_unfair_lock_trylock(lock) else { return nil } @@ -131,25 +140,22 @@ extension Locked { } return try body(&rawValue.pointee) } - return result -#else -#if os(Linux) || os(Android) - let tid = gettid() - if _storage.owningThreadID.load(ordering: .sequentiallyConsistent) == tid { - // This thread already holds the lock. - return nil - } -#endif - try _storage.mutex.withLockIfAvailable { rawValue in -#if os(Linux) || os(Android) - _storage.owningThreadID.store(tid, ordering: .sequentiallyConsistent) +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + result = try _storage.withUnsafeMutablePointers { rawValue, lock in + guard 0 == pthread_mutex_trylock(lock) else { + return nil + } defer { - _storage.owningThreadID.store(0, ordering: .sequentiallyConsistent) + pthread_mutex_unlock(lock) } -#endif + return try body(&rawValue.pointee) + } +#else + result = try _storage.mutex.withLockIfAvailable { rawValue in return try body(&rawValue) } #endif + return result } } From a8427f938305b26c494698c64a22046054434422 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 16:38:09 -0500 Subject: [PATCH 09/12] Remove gettid() declaration, don't need it anymore --- Sources/_TestingInternals/include/Stubs.h | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 3c939ace6..636ea9aff 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -152,14 +152,6 @@ static int swt_siginfo_t_si_status(const siginfo_t *siginfo) { #endif #endif -#if defined(__linux__) || defined(__ANDROID__) -/// Get the current thread's ID. -/// -/// This function is redeclared here because it is guarded by `_GNU_SOURCE` in -/// the platform headers. -SWT_EXTERN pid_t gettid(void); -#endif - /// Get the value of `EEXIST`. /// /// This function is provided because `EEXIST` is a complex macro in wasi-libc From c319c99e3cce8b20db74d326dd761ac067e0cfeb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 17:16:19 -0500 Subject: [PATCH 10/12] pthread_mutex_destroy --- Sources/Testing/Support/Locked.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index a2be1a06a..90ebe61c3 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -30,7 +30,7 @@ struct Locked { private final class _Storage: ManagedBuffer { deinit { withUnsafeMutablePointerToElements { lock in - _ = lock.deinitialize(count: 1) + _ = pthread_mutex_destroy(lock) } } } From f194f206f77bc0904251ea5a542eefa2b0614418 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 17:57:32 -0500 Subject: [PATCH 11/12] Make sure we still cancel the current task in the edge case --- Sources/Testing/Test+Cancellation.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 25c5a0ffa..43fd54391 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -142,9 +142,14 @@ extension TestCancellable { private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) where T: TestCancellable { if cancellableValue != nil, let taskCanceller = _currentTaskCancellers[ObjectIdentifier(T.self)] { // Try to cancel the task associated with `T`, if any. If we succeed, post a - // corresponding event with the relevant skip info. + // corresponding event with the relevant skip info. If we fail, we still + // attempt to cancel the current *task* in order to honor our API contract. if taskCanceller.cancel(with: skipInfo) { Event.post(T.makeCancelledEventKind(with: skipInfo), for: testAndTestCase) + } else { + withUnsafeCurrentTask { task in + task?.cancel() + } } } else { // The current task isn't associated with a test/case, so just cancel the From e7a040e669c885c5ffa533fc00cc7ff8823baadd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 12 Nov 2025 08:54:01 -0500 Subject: [PATCH 12/12] Extra `return` --- Sources/Testing/Support/Locked.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index 90ebe61c3..fbebaa063 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -109,7 +109,7 @@ extension Locked { } #else result = try _storage.mutex.withLock { rawValue in - return try body(&rawValue) + try body(&rawValue) } #endif return result