From 8d8b7e5d5dfecd08135f80789d30892e3a36550e Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Tue, 9 Jun 2026 08:08:44 -0700 Subject: [PATCH] Deflake queue-deallocation tests by awaiting the enqueued task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four `*_executesAfterQueueIsDeallocated` tests confirmed the final enqueued task ran by waiting on `Expectation.fulfillment(withinSeconds: 30)`. That is a wall-clock race: the expectation pits a `Task.sleep(30s)` against `fulfill()`, and `fulfill()` itself hops through an extra detached task. Under the `-test-iterations 100 -run-tests-until-failure` stress harness on a slow, contended simulator (visionOS), the cooperative pool can be starved long enough that the sleep wins before the queued work is scheduled, producing a false failure. `Task(on:)` captures only its `Delivery`/`Semaphore` — never the `FIFOQueue` — so holding the returned handle does not retain the queue, and the `weak queue == nil` assertion still holds. Capture the last enqueued task and `await` its value instead. This removes the wall-clock race entirely and is a strictly stronger check: it waits for the work to actually complete rather than for a side-channel flag, while the `Counter` assertions still verify ordering. The tests now finish in ~1ms regardless of runner speed. Co-Authored-By: Claude Opus 4.8 (1M context) --- Tests/AsyncQueueTests/FIFOQueueTests.swift | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Tests/AsyncQueueTests/FIFOQueueTests.swift b/Tests/AsyncQueueTests/FIFOQueueTests.swift index 24fa9f1..6224807 100644 --- a/Tests/AsyncQueueTests/FIFOQueueTests.swift +++ b/Tests/AsyncQueueTests/FIFOQueueTests.swift @@ -265,17 +265,15 @@ struct FIFOQueueTests { func task_executesAfterQueueIsDeallocated() async throws { var systemUnderTest: FIFOQueue? = FIFOQueue() let counter = Counter() - let expectation = Expectation() let semaphore = Semaphore() try Task(on: #require(systemUnderTest)) { // Make the queue wait. await semaphore.wait() await counter.incrementAndExpectCount(equals: 1) } - try Task(on: #require(systemUnderTest)) { + let lastTask = try Task(on: #require(systemUnderTest)) { // This async task should not execute until the semaphore is released. await counter.incrementAndExpectCount(equals: 2) - expectation.fulfill() } weak var queue = systemUnderTest // Nil out our reference to the queue to show that the enqueued tasks will still complete @@ -284,24 +282,23 @@ struct FIFOQueueTests { // Signal the semaphore to unlock the remaining enqueued tasks. await semaphore.signal() - await expectation.fulfillment(withinSeconds: 30) + // Await the last enqueued task to deterministically confirm the enqueued work ran after the queue was deallocated. + await lastTask.value } @Test func taskIsolatedTo_executesAfterQueueIsDeallocated() async throws { var systemUnderTest: FIFOQueue? = FIFOQueue() let counter = Counter() - let expectation = Expectation() let semaphore = Semaphore() try Task(on: #require(systemUnderTest), isolatedTo: counter) { counter in // Make the queue wait. await semaphore.wait() counter.incrementAndExpectCount(equals: 1) } - try Task(on: #require(systemUnderTest), isolatedTo: counter) { counter in + let lastTask = try Task(on: #require(systemUnderTest), isolatedTo: counter) { counter in // This async task should not execute until the semaphore is released. counter.incrementAndExpectCount(equals: 2) - expectation.fulfill() } weak var queue = systemUnderTest // Nil out our reference to the queue to show that the enqueued tasks will still complete @@ -310,14 +307,14 @@ struct FIFOQueueTests { // Signal the semaphore to unlock the remaining enqueued tasks. await semaphore.signal() - await expectation.fulfillment(withinSeconds: 30) + // Await the last enqueued task to deterministically confirm the enqueued work ran after the queue was deallocated. + await lastTask.value } @Test func throwingTask_executesAfterQueueIsDeallocated() async throws { var systemUnderTest: FIFOQueue? = FIFOQueue() let counter = Counter() - let expectation = Expectation() let semaphore = Semaphore() try Task(on: #require(systemUnderTest)) { // Make the queue wait. @@ -325,10 +322,9 @@ struct FIFOQueueTests { await counter.incrementAndExpectCount(equals: 1) try doWork() } - try Task(on: #require(systemUnderTest)) { + let lastTask = try Task(on: #require(systemUnderTest)) { // This async task should not execute until the semaphore is released. await counter.incrementAndExpectCount(equals: 2) - expectation.fulfill() try doWork() } weak var queue = systemUnderTest @@ -338,14 +334,14 @@ struct FIFOQueueTests { // Signal the semaphore to unlock the remaining enqueued tasks. await semaphore.signal() - await expectation.fulfillment(withinSeconds: 30) + // Await the last enqueued task to deterministically confirm the enqueued work ran after the queue was deallocated. + try await lastTask.value } @Test func throwingTaskIsolatedTo_executesAfterQueueIsDeallocated() async throws { var systemUnderTest: FIFOQueue? = FIFOQueue() let counter = Counter() - let expectation = Expectation() let semaphore = Semaphore() try Task(on: #require(systemUnderTest), isolatedTo: counter) { counter in // Make the queue wait. @@ -353,10 +349,9 @@ struct FIFOQueueTests { counter.incrementAndExpectCount(equals: 1) try doWork() } - try Task(on: #require(systemUnderTest), isolatedTo: counter) { counter in + let lastTask = try Task(on: #require(systemUnderTest), isolatedTo: counter) { counter in // This async task should not execute until the semaphore is released. counter.incrementAndExpectCount(equals: 2) - expectation.fulfill() try doWork() } weak var queue = systemUnderTest @@ -366,7 +361,8 @@ struct FIFOQueueTests { // Signal the semaphore to unlock the remaining enqueued tasks. await semaphore.signal() - await expectation.fulfillment(withinSeconds: 30) + // Await the last enqueued task to deterministically confirm the enqueued work ran after the queue was deallocated. + try await lastTask.value } @Test