Skip to content

Commit 0eb5698

Browse files
committed
Use a Semaphore to make tests reliable
1 parent 28a6aa1 commit 0eb5698

File tree

1 file changed

+62
-34
lines changed

1 file changed

+62
-34
lines changed

Tests/AsyncQueueTests/AsyncQueueTests.swift

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -77,55 +77,50 @@ final class AsyncQueueTests: XCTestCase {
7777
var systemUnderTest: AsyncQueue? = AsyncQueue()
7878
let counter = Counter()
7979
let expectation = self.expectation(description: #function)
80-
await withThrowingTaskGroup(of: Void.self) { taskGroup in
81-
let foreverSleep = Task {
82-
try await Task.sleep(nanoseconds: UInt64.max)
83-
}
84-
taskGroup.addTask {
85-
try await foreverSleep.value
86-
}
87-
systemUnderTest?.async {
88-
// Make the queue wait.
89-
try? await foreverSleep.value
90-
await counter.incrementAndExpectCount(equals: 1)
91-
}
92-
systemUnderTest?.async {
93-
// This async task should not execute until the sleep is cancelled.
94-
await counter.incrementAndExpectCount(equals: 2)
95-
expectation.fulfill()
96-
}
97-
// Nil out our reference to the queue to show that the enqueued tasks will still complete
98-
systemUnderTest = nil
99-
// Cancel the sleep timer to unlock the remaining enqueued tasks.
100-
foreverSleep.cancel()
101-
102-
await waitForExpectations(timeout: 1.0)
80+
let semaphore = Semaphore()
81+
systemUnderTest?.async {
82+
// Make the queue wait.
83+
await semaphore.wait()
84+
await counter.incrementAndExpectCount(equals: 1)
85+
}
86+
systemUnderTest?.async {
87+
// This async task should not execute until the sleep is cancelled.
88+
await counter.incrementAndExpectCount(equals: 2)
89+
expectation.fulfill()
10390
}
91+
// Nil out our reference to the queue to show that the enqueued tasks will still complete
92+
systemUnderTest = nil
93+
// Signal the semaphore to unlock the remaining enqueued tasks.
94+
await semaphore.signal()
95+
96+
await waitForExpectations(timeout: 1.0)
10497
}
10598

106-
func test_async_doesNotRetainTaskAfterExecution() async {
99+
func test_async_doesNotRetainTaskAfterExecution() async throws {
107100
final class Reference: Sendable {}
108101
final class ReferenceHolder: @unchecked Sendable {
109102
var reference: Reference? = Reference()
110103
}
111104
let referenceHolder = ReferenceHolder()
112105
weak var weakReference = referenceHolder.reference
113-
let expectation = self.expectation(description: #function)
114-
let foreverSleep = Task {
115-
try await Task.sleep(nanoseconds: UInt64.max)
116-
}
106+
let asyncSemaphore = Semaphore()
107+
let syncSemaphore = Semaphore()
117108
systemUnderTest.async { [reference = referenceHolder.reference] in
118-
// Wait for the setup to complete.
119-
try? await foreverSleep.value
109+
// Now that we've started the task and captured the reference, release the syncronous code.
110+
await syncSemaphore.signal()
111+
// Wait for the synchronous setup to complete and the reference to be nil'd out.
112+
await asyncSemaphore.wait()
120113
// Retain the unsafe counter until the task is completed.
121114
_ = reference
122-
expectation.fulfill()
123115
}
116+
// Wait for the asynchronous task to start.
117+
await syncSemaphore.wait()
124118
referenceHolder.reference = nil
125119
XCTAssertNotNil(weakReference)
126-
// Cancel the sleep timer to allow the enqueued task to complete.
127-
foreverSleep.cancel()
128-
await waitForExpectations(timeout: 1.0)
120+
// Allow the enqueued task to complete.
121+
await asyncSemaphore.signal()
122+
// Make sure the task has completed.
123+
await systemUnderTest.await { /* Drain the queue */ }
129124
XCTAssertNil(weakReference)
130125
}
131126

@@ -197,4 +192,37 @@ final class AsyncQueueTests: XCTestCase {
197192
var count = 0
198193
}
199194

195+
// MARK: - Semaphore
196+
197+
private actor Semaphore {
198+
199+
func wait() async {
200+
count -= 1
201+
guard count < 0 else {
202+
// We don't need to wait because count is greater than or equal to zero.
203+
return
204+
}
205+
206+
await withCheckedContinuation { continuation in
207+
continuations.append(continuation)
208+
}
209+
}
210+
211+
func signal() {
212+
count += 1
213+
guard count >= 0 else {
214+
// Continue waiting.
215+
return
216+
}
217+
218+
for continuation in continuations {
219+
continuation.resume()
220+
}
221+
222+
continuations.removeAll()
223+
}
224+
225+
private var continuations = [CheckedContinuation<Void, Never>]()
226+
private var count = 0
227+
}
200228
}

0 commit comments

Comments
 (0)