diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Semaphore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Semaphore.xcscheme
index ed39087..0a11c2c 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Semaphore.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Semaphore.xcscheme
@@ -40,8 +40,15 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES"
+ shouldUseLaunchSchemeArgsEnv = "NO"
codeCoverageEnabled = "YES">
+
+
+
+
= 0 {
- unlock()
+ defer { unlock() }
// All code paths check for cancellation
- try Task.checkCancellation()
+ if Task.isCancelled {
+ value += 1
+ throw CancellationError()
+ }
return
}
diff --git a/Tests/SemaphoreTests/AsyncSemaphoreTests.swift b/Tests/SemaphoreTests/AsyncSemaphoreTests.swift
index cf9665f..4d2d200 100644
--- a/Tests/SemaphoreTests/AsyncSemaphoreTests.swift
+++ b/Tests/SemaphoreTests/AsyncSemaphoreTests.swift
@@ -3,6 +3,12 @@ import XCTest
@testable import Semaphore
final class AsyncSemaphoreTests: XCTestCase {
+
+ override func setUp() {
+ super.setUp()
+ // Don't continue after completeWithin(nanoseconds:) causes a XCTFail
+ continueAfterFailure = false
+ }
func testSignalWithoutSuspendedTasks() async {
// Check DispatchSemaphore behavior
@@ -104,7 +110,7 @@ final class AsyncSemaphoreTests: XCTestCase {
let ex1 = expectation(description: "wait")
ex1.isInverted = true
let ex2 = expectation(description: "woken")
- Task {
+ let task = Task {
await sem.wait()
ex1.fulfill()
ex2.fulfill()
@@ -115,6 +121,7 @@ final class AsyncSemaphoreTests: XCTestCase {
// When a signal occurs, then the suspended task is resumed.
sem.signal()
+ await task.value
wait(for: [ex2], timeout: 0.5)
}
}
@@ -134,6 +141,7 @@ final class AsyncSemaphoreTests: XCTestCase {
}
try await Task.sleep(nanoseconds: 100_000_000)
task.cancel()
+ await task.value
wait(for: [ex], timeout: 1)
}
@@ -157,6 +165,7 @@ final class AsyncSemaphoreTests: XCTestCase {
ex.fulfill()
}
task.cancel()
+ await task.value
wait(for: [ex], timeout: 5)
}
@@ -173,7 +182,7 @@ final class AsyncSemaphoreTests: XCTestCase {
let ex1 = expectation(description: "wait")
ex1.isInverted = true
let ex2 = expectation(description: "woken")
- Task {
+ let taskTwo = Task {
await sem.wait()
ex1.fulfill()
ex2.fulfill()
@@ -184,6 +193,7 @@ final class AsyncSemaphoreTests: XCTestCase {
// When a signal occurs, then the suspended task is resumed.
sem.signal()
+ await taskTwo.value
wait(for: [ex2], timeout: 0.5)
}
@@ -205,7 +215,7 @@ final class AsyncSemaphoreTests: XCTestCase {
let ex1 = expectation(description: "wait")
ex1.isInverted = true
let ex2 = expectation(description: "woken")
- Task {
+ let taskTwo = Task {
await sem.wait()
ex1.fulfill()
ex2.fulfill()
@@ -216,9 +226,46 @@ final class AsyncSemaphoreTests: XCTestCase {
// When a signal occurs, then the suspended task is resumed.
sem.signal()
+ await taskTwo.value
wait(for: [ex2], timeout: 0.5)
}
-
+
+ func test_that_cancellation_before_suspension_increments_the_semaphore_two() async {
+ await completeWithin(nanoseconds: NSEC_PER_SEC * 2) {
+ let sem = AsyncSemaphore(value: 1)
+ let task = Task {
+ while !Task.isCancelled {
+ await Task.yield()
+ }
+ try await sem.waitUnlessCancelled()
+ }
+ task.cancel()
+ try? await task.value
+ await sem.wait()
+ }
+ }
+
+ func test_that_cancellation_while_suspended_increments_the_semaphore_two() async {
+ await completeWithin(nanoseconds: NSEC_PER_SEC * 2) {
+ let sem = AsyncSemaphore(value: 0)
+ let running = Atomic(false)
+ let task = Task {
+ running.mutate { $0 = true }
+ try await sem.waitUnlessCancelled()
+ while !Task.isCancelled {
+ await Task.yield()
+ }
+ }
+ while !running.value {
+ await Task.yield()
+ }
+ task.cancel()
+ try? await task.value
+ sem.signal()
+ await sem.wait()
+ }
+ }
+
// Test that semaphore can limit the number of concurrent executions of
// an actor method.
func test_semaphore_as_a_resource_limiter_on_actor_method() async {
@@ -395,3 +442,50 @@ final class AsyncSemaphoreTests: XCTestCase {
}
}
}
+
+/// Helper to complete a test within some amount of time or fail.
+/// XCTestExpectation don't work, when using LIBDISPATCH_COOPERATIVE_POOL_STRICT=1 as environment variable
+/// or e.g. running tests on an iOS simulator as the wait(for:timeout:) blocks the pool.
+/// Which means after the await no further async work can execute to fulfill any expectation that wait
+/// is waiting for.
+func completeWithin(nanoseconds nanosecondsDeadline: UInt64,
+ file: StaticString = #filePath,
+ line: UInt = #line,
+ work: () async throws -> Void) async rethrows {
+ let checkDeadlineTask = Task {
+ try await Task.sleep(nanoseconds: nanosecondsDeadline)
+ try Task.checkCancellation()
+ XCTFail("Test timed out.", file: file, line: line)
+ }
+ try await work()
+ checkDeadlineTask.cancel()
+}
+
+final class Atomic: @unchecked Sendable {
+ private var lock = NSRecursiveLock()
+ private var _value: A
+
+ public init(_ value: A) {
+ _value = value
+ }
+
+ public var value: A {
+ synced {
+ _value
+ }
+ }
+
+ public func mutate(_ transform: (inout A) -> Void) {
+ synced {
+ transform(&self._value)
+ }
+ }
+
+ private func synced(_ action: () throws -> Result) rethrows -> Result {
+ lock.lock()
+ defer {
+ lock.unlock()
+ }
+ return try action()
+ }
+}