Skip to content

Commit 3d1f6e7

Browse files
Tolerate both CancellationError and URLError in CancellationTests (#45)
### Motivation When cancelling a Swift concurrency task during a streaming request then the error returned might be `URLError` with `.cancelled` code or `CancellationError`. The tests tried to be smart and expect just one of these depending on which stage of the request we were at, but there are still some races, and this test fails very rarely because a `CancellationError` was thrown instead of a `URLError`. ### Modifications This patch updates the test to tolerate both kinds of error at this stage of the request. ### Result The test will pass if the error is either `URLError` with `.cancelled` or `CancellationError`, and continue to fail if there is any other kind of error or no error. ### Test Plan Existing tests, which failed when run repeatedly for 1k runs, now pass when run for 10k runs.
1 parent aac0a82 commit 3d1f6e7

File tree

3 files changed

+19
-16
lines changed

3 files changed

+19
-16
lines changed

Tests/OpenAPIURLSessionTests/NIOAsyncHTTP1TestServer.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ final class AsyncTestHTTP1Server {
5959
for try await connectionChannel in inbound {
6060
group.addTask {
6161
do {
62-
debug("Sevrer handling new connection")
62+
debug("Server handling new connection")
6363
try await connectionHandler(connectionChannel)
6464
debug("Server done handling connection")
6565
} catch { debug("Server error handling connection: \(error)") }

Tests/OpenAPIURLSessionTests/TaskCancellationTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,11 @@ func testTaskCancelled(_ cancellationPoint: CancellationPoint, transport: URLSes
150150
await XCTAssertThrowsError(try await task.value) { error in XCTAssertTrue(error is CancellationError) }
151151
case .beforeSendingRequestBody, .partwayThroughSendingRequestBody:
152152
await XCTAssertThrowsError(try await task.value) { error in
153-
guard let urlError = error as? URLError else {
154-
XCTFail()
155-
return
153+
switch error {
154+
case is CancellationError: break
155+
case is URLError: XCTAssertEqual((error as! URLError).code, .cancelled)
156+
default: XCTFail("Unexpected error: \(error)")
156157
}
157-
XCTAssertEqual(urlError.code, .cancelled)
158158
}
159159
case .beforeConsumingResponseBody, .partwayThroughConsumingResponseBody, .afterConsumingResponseBody:
160160
try await task.value

Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/MockAsyncSequence.swift

+14-11
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,47 @@ struct MockAsyncSequence<Element>: AsyncSequence, Sendable where Element: Sendab
2020
var elementsToVend: [Element]
2121
private let _elementsVended: LockedValueBox<[Element]>
2222
var elementsVended: [Element] { _elementsVended.withValue { $0 } }
23-
private let semaphore: DispatchSemaphore?
23+
private let gateOpeningsStream: AsyncStream<Void>
24+
private let gateOpeningsContinuation: AsyncStream<Void>.Continuation
2425

2526
init(elementsToVend: [Element], gatingProduction: Bool) {
2627
self.elementsToVend = elementsToVend
2728
self._elementsVended = LockedValueBox([])
28-
self.semaphore = gatingProduction ? DispatchSemaphore(value: 0) : nil
29+
(self.gateOpeningsStream, self.gateOpeningsContinuation) = AsyncStream.makeStream(of: Void.self)
30+
if !gatingProduction { openGate() }
2931
}
3032

31-
func openGate(for count: Int) { for _ in 0..<count { semaphore?.signal() } }
33+
func openGate(for count: Int) { for _ in 0..<count { self.gateOpeningsContinuation.yield() } }
3234

3335
func openGate() {
3436
openGate(for: elementsToVend.count + 1) // + 1 for the nil
3537
}
3638

3739
func makeAsyncIterator() -> AsyncIterator {
38-
AsyncIterator(elementsToVend: elementsToVend[...], semaphore: semaphore, elementsVended: _elementsVended)
40+
AsyncIterator(
41+
elementsToVend: elementsToVend[...],
42+
gateOpenings: gateOpeningsStream.makeAsyncIterator(),
43+
elementsVended: _elementsVended
44+
)
3945
}
4046

4147
final class AsyncIterator: AsyncIteratorProtocol {
4248
var elementsToVend: ArraySlice<Element>
43-
var semaphore: DispatchSemaphore?
49+
var gateOpenings: AsyncStream<Void>.Iterator
4450
var elementsVended: LockedValueBox<[Element]>
4551

4652
init(
4753
elementsToVend: ArraySlice<Element>,
48-
semaphore: DispatchSemaphore?,
54+
gateOpenings: AsyncStream<Void>.Iterator,
4955
elementsVended: LockedValueBox<[Element]>
5056
) {
5157
self.elementsToVend = elementsToVend
52-
self.semaphore = semaphore
58+
self.gateOpenings = gateOpenings
5359
self.elementsVended = elementsVended
5460
}
5561

5662
func next() async throws -> Element? {
57-
await withCheckedContinuation { continuation in
58-
semaphore?.wait()
59-
continuation.resume()
60-
}
63+
guard await gateOpenings.next() != nil else { throw CancellationError() }
6164
guard let element = elementsToVend.popFirst() else { return nil }
6265
elementsVended.withValue { $0.append(element) }
6366
return element

0 commit comments

Comments
 (0)