Skip to content

Commit b400036

Browse files
committed
fixes behavior of inverted expectations which can fulfill after timeout
1 parent bfff294 commit b400036

3 files changed

Lines changed: 61 additions & 9 deletions

File tree

Package.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ let package = Package(
2727
swiftSettings: nil,
2828
linkerSettings: [.linkedFramework("XCTest")]
2929
),
30-
3130
.testTarget(
3231
name: "AsyncTestingTests",
3332
dependencies: ["AsyncTesting"],

Sources/AsyncTesting/AsyncExpectation.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,20 @@ public actor AsyncExpectation {
3434
self.isInverted = isInverted
3535
self.expectedFulfillmentCount = expectedFulfillmentCount
3636
}
37-
37+
38+
/// Marks the expectation as having been met.
39+
///
40+
/// It is an error to call this method on an expectation that has already been fulfilled,
41+
/// or when the test case that vended the expectation has already completed.
3842
public func fulfill(file: StaticString = #filePath, line: UInt = #line) {
3943
guard state != .fulfilled else { return }
40-
41-
guard !isInverted else {
42-
XCTFail("Inverted expectation fulfilled: \(expectationDescription)", file: file, line: line)
43-
state = .fulfilled
44-
finish()
44+
45+
if isInverted {
46+
if state != .timedOut {
47+
XCTFail("Inverted expectation fulfilled: \(expectationDescription)", file: file, line: line)
48+
state = .fulfilled
49+
finish()
50+
}
4551
return
4652
}
4753

@@ -70,7 +76,9 @@ public actor AsyncExpectation {
7076

7177
internal func timeOut(file: StaticString = #filePath,
7278
line: UInt = #line) async {
73-
if state != .fulfilled && !isInverted {
79+
if isInverted {
80+
state = .timedOut
81+
} else if state != .fulfilled {
7482
state = .timedOut
7583
XCTFail("Expectation timed out: \(expectationDescription)", file: file, line: line)
7684
}

Tests/AsyncTestingTests/AsyncTestingTests.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
11
import XCTest
22
@testable import AsyncTesting
33

4+
actor AsyncRunner {
5+
typealias VoidNeverContinuation = CheckedContinuation<Void, Never>
6+
private var continuations: [VoidNeverContinuation] = []
7+
8+
public func run(timeout: Double = 5.0) async {
9+
await withTaskCancellationHandler {
10+
Task {
11+
await finish()
12+
}
13+
} operation: {
14+
await withCheckedContinuation {
15+
continuations.append($0)
16+
}
17+
Task {
18+
try await Task.sleep(seconds: timeout)
19+
finish()
20+
}
21+
}
22+
}
23+
24+
public func finish() {
25+
while !continuations.isEmpty {
26+
let continuation = continuations.removeFirst()
27+
continuation.resume(returning: ())
28+
}
29+
}
30+
}
31+
432
final class AsyncExpectationTests: XCTestCase {
533

634
func testDoneExpectation() async throws {
@@ -42,6 +70,23 @@ final class AsyncExpectationTests: XCTestCase {
4270
task.cancel()
4371
try await waitForExpectations([notDone], timeout: delay * 2)
4472
}
73+
74+
func testNotYetDoneAndThenDoneExpectation() async throws {
75+
let delay = 0.01
76+
let notYetDone = asyncExpectation(description: "not yet done", isInverted: true)
77+
let done = asyncExpectation(description: "done")
78+
79+
let task = Task {
80+
await AsyncRunner().run()
81+
XCTAssertTrue(Task.isCancelled)
82+
await notYetDone.fulfill() // will timeout before being called
83+
await done.fulfill() // will be called after cancellation
84+
}
85+
86+
try await waitForExpectations([notYetDone], timeout: delay)
87+
task.cancel()
88+
try await waitForExpectations([done])
89+
}
4590

4691
func testDoneAndNotDoneInvertedExpectation() async throws {
4792
let delay = 0.01
@@ -91,5 +136,5 @@ final class AsyncExpectationTests: XCTestCase {
91136

92137
try await waitForExpectations([one, two, three])
93138
}
94-
139+
95140
}

0 commit comments

Comments
 (0)