Skip to content

Commit c6bb523

Browse files
authored
Precondition fulfillment awaited before deinit (#13)
1 parent ea53725 commit c6bb523

File tree

3 files changed

+80
-6
lines changed

3 files changed

+80
-6
lines changed

Sources/TestingExpectation/Expectation.swift

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,56 @@ import Testing
2525
public actor Expectation {
2626
// MARK: Initialization
2727

28+
/// An expected outcome in an asynchronous test.
29+
/// - Parameters:
30+
/// - expectedCount: The number of times `fulfill()` must be called before the expectation is completely fulfilled.
31+
/// - requireAwaitingFulfillment: Controls whether `deinit` requires that a created expectation has its fulfillment awaited.
2832
public init(
29-
expectedCount: UInt = 1
33+
expectedCount: UInt = 1,
34+
requireAwaitingFulfillment: Bool = true,
35+
filePath: String = #filePath,
36+
fileID: String = #fileID,
37+
line: Int = #line,
38+
column: Int = #column
3039
) {
3140
self.init(
3241
expectedCount: expectedCount,
3342
expect: { fulfilledWithExpectedCount, comment, sourceLocation in
3443
#expect(fulfilledWithExpectedCount, comment, sourceLocation: sourceLocation)
35-
}
44+
},
45+
precondition: requireAwaitingFulfillment ? Swift.precondition : nil,
46+
filePath: filePath,
47+
fileID: fileID,
48+
line: line,
49+
column: column
3650
)
3751
}
3852

3953
init(
4054
expectedCount: UInt,
41-
expect: @escaping (Bool, Comment?, SourceLocation) -> Void
55+
expect: @escaping @Sendable (Bool, Comment?, SourceLocation) -> Void,
56+
precondition: (@Sendable (@autoclosure () -> Bool, @autoclosure () -> String, StaticString, UInt) -> Void)? = nil,
57+
filePath: String = #filePath,
58+
fileID: String = #fileID,
59+
line: Int = #line,
60+
column: Int = #column
4261
) {
4362
self.expectedCount = expectedCount
4463
self.expect = expect
64+
self.precondition = precondition
65+
createdSourceLocation = .init(
66+
fileID: fileID,
67+
filePath: filePath,
68+
line: line,
69+
column: column
70+
)
71+
}
72+
73+
deinit {
74+
let fulfillmentAwaited = fulfillmentAwaited
75+
if let precondition {
76+
precondition(fulfillmentAwaited, "Expectation created at \(createdSourceLocation) was never awaited", #file, #line)
77+
}
4578
}
4679

4780
// MARK: Public
@@ -53,6 +86,7 @@ public actor Expectation {
5386
line: Int = #line,
5487
column: Int = #column
5588
) async {
89+
fulfillmentAwaited = true
5690
guard !isComplete else { return }
5791
let wait = Task {
5892
try await Task.sleep(for: duration)
@@ -93,8 +127,12 @@ public actor Expectation {
93127
expectedCount <= fulfillCount
94128
}
95129

130+
private var fulfillmentAwaited = false
131+
96132
private let expectedCount: UInt
97-
private let expect: (Bool, Comment?, SourceLocation) -> Void
133+
private let expect: @Sendable (Bool, Comment?, SourceLocation) -> Void
134+
private let precondition: (@Sendable (@autoclosure () -> Bool, @autoclosure () -> String, StaticString, UInt) -> Void)?
135+
private let createdSourceLocation: SourceLocation
98136

99137
private func _fulfill(
100138
filePath: String,

TestingExpectation.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'TestingExpectation'
3-
s.version = '0.1.3'
3+
s.version = '0.1.4'
44
s.license = 'MIT'
55
s.summary = 'Create an asynchronous expectation in Swift Testing'
66
s.homepage = 'https://github.com/dfed/swift-testing-expectation'

Tests/TestingExpectationTests/ExpectationTests.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ struct ExpectationTests {
7777
@MainActor // Global actor ensures Task ordering.
7878
@Test
7979
func fulfillment_waitsForFulfillment() async {
80-
let systemUnderTest = Expectation(expectedCount: 1)
80+
let systemUnderTest = Expectation(expectedCount: 1, requireAwaitingFulfillment: false)
8181
var hasFulfilled = false
8282
let wait = Task {
8383
await systemUnderTest.fulfillment(within: .seconds(10))
@@ -103,4 +103,40 @@ struct ExpectationTests {
103103
await systemUnderTest.fulfillment(within: .zero)
104104
}
105105
}
106+
107+
@Test
108+
func deinit_triggersTrueExpectationWhenNotAwaited() async {
109+
let expect: @Sendable (Bool, Comment?, SourceLocation) -> Void = { _, _, _ in }
110+
expect(true, nil, #_sourceLocation) // Force code coverage to cover the empty closure.
111+
await confirmation { confirmation in
112+
let unmanagedSystemUnderTest = Unmanaged.passRetained(Expectation(
113+
expectedCount: 0,
114+
expect: expect,
115+
precondition: { condition, message, _, _ in
116+
_ = message() // Force code coverage to cover the creation of the message.
117+
#expect(condition())
118+
confirmation()
119+
}
120+
))
121+
await unmanagedSystemUnderTest.takeUnretainedValue().fulfillment(within: .zero)
122+
unmanagedSystemUnderTest.release() // Force the system under test to deinit.
123+
}
124+
}
125+
126+
@Test
127+
func deinit_triggersFalseExpectationWhenNotAwaited() async {
128+
let expect: @Sendable (Bool, Comment?, SourceLocation) -> Void = { _, _, _ in }
129+
expect(true, nil, #_sourceLocation) // Force code coverage to cover the empty closure.
130+
await confirmation { confirmation in
131+
_ = Expectation(
132+
expectedCount: 1,
133+
expect: expect,
134+
precondition: { condition, message, _, _ in
135+
_ = message() // Force code coverage to cover the creation of the message.
136+
#expect(!condition())
137+
confirmation()
138+
}
139+
)
140+
}
141+
}
106142
}

0 commit comments

Comments
 (0)