Skip to content

Commit b59a075

Browse files
committed
Add CancellableQueueTests for 100% test coverage
Test cancelTasks() behavior across all queue types: - FIFOQueue (with isolated and throwing variants) - ActorQueue - MainActor queue Each queue type tests four scenarios: - doesNotCancelCompletedTask - cancelsCurrentlyExecutingTask - cancelsCurrentlyExecutingAndPendingTasks - doesNotCancelFutureTasks
1 parent bd485f5 commit b59a075

File tree

1 file changed

+353
-0
lines changed

1 file changed

+353
-0
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2025 Dan Federman
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
import Foundation
24+
import Testing
25+
26+
@testable import AsyncQueue
27+
28+
struct CancellableQueueTests {
29+
// MARK: FIFOQueue Tests
30+
31+
@Test
32+
func cancelTasks_fifoQueue_doesNotCancelCompletedTask() async throws {
33+
let systemUnderTest = CancellableQueue(underlyingQueue: FIFOQueue())
34+
let expectation = Expectation()
35+
36+
// Create a task that completes immediately
37+
let task = Task(on: systemUnderTest) {
38+
expectation.fulfill()
39+
try doWork()
40+
}
41+
42+
// Wait for the task to complete
43+
try await task.value
44+
45+
// Now cancel tasks - should have no effect since task already completed
46+
systemUnderTest.cancelTasks()
47+
48+
await expectation.fulfillment(withinSeconds: 30)
49+
}
50+
51+
@Test
52+
func cancelTasks_fifoQueue_cancelsCurrentlyExecutingTask() async {
53+
let systemUnderTest = CancellableQueue(underlyingQueue: FIFOQueue())
54+
let semaphore = Semaphore()
55+
let expectation = Expectation()
56+
57+
// Create a task that waits on a semaphore
58+
Task(on: systemUnderTest) {
59+
await semaphore.wait()
60+
#expect(Task.isCancelled)
61+
expectation.fulfill()
62+
}
63+
64+
// Give the task time to start executing and reach the semaphore
65+
try? await Task.sleep(nanoseconds: 10_000_000)
66+
67+
// Cancel all tasks
68+
systemUnderTest.cancelTasks()
69+
70+
// Signal the semaphore to let the task continue
71+
await semaphore.signal()
72+
73+
await expectation.fulfillment(withinSeconds: 30)
74+
}
75+
76+
@Test
77+
func cancelTasks_fifoQueue_cancelsCurrentlyExecutingAndPendingTasks() async {
78+
let systemUnderTest = CancellableQueue(underlyingQueue: FIFOQueue())
79+
let semaphore = Semaphore()
80+
let counter = Counter()
81+
let expectation = Expectation(expectedCount: 3)
82+
83+
// Create a task that waits on a semaphore (will be executing)
84+
Task(on: systemUnderTest, isolatedTo: counter) { _ in
85+
await semaphore.wait()
86+
#expect(Task.isCancelled)
87+
expectation.fulfill()
88+
}
89+
90+
// Create pending tasks that won't start until the first task completes
91+
Task(on: systemUnderTest, isolatedTo: counter) { _ in
92+
#expect(Task.isCancelled)
93+
expectation.fulfill()
94+
}
95+
96+
Task(on: systemUnderTest, isolatedTo: counter) { _ in
97+
#expect(Task.isCancelled)
98+
expectation.fulfill()
99+
}
100+
101+
// Give the first task time to start executing
102+
try? await Task.sleep(nanoseconds: 10_000_000)
103+
104+
// Cancel all tasks
105+
systemUnderTest.cancelTasks()
106+
107+
// Signal the semaphore to let tasks continue
108+
await semaphore.signal()
109+
110+
await expectation.fulfillment(withinSeconds: 30)
111+
}
112+
113+
@Test
114+
func cancelTasks_fifoQueue_doesNotCancelFutureTasks() async throws {
115+
let systemUnderTest = CancellableQueue(underlyingQueue: FIFOQueue())
116+
let counter = Counter()
117+
let expectation = Expectation()
118+
119+
// Cancel tasks before creating any
120+
systemUnderTest.cancelTasks()
121+
122+
// Create a task after cancellation - it should NOT be cancelled
123+
let task = Task(on: systemUnderTest, isolatedTo: counter) { _ in
124+
#expect(!Task.isCancelled)
125+
expectation.fulfill()
126+
try doWork()
127+
}
128+
129+
try await task.value
130+
await expectation.fulfillment(withinSeconds: 30)
131+
}
132+
133+
// MARK: ActorQueue Tests
134+
135+
@Test
136+
func cancelTasks_actorQueue_doesNotCancelCompletedTask() async throws {
137+
let actorQueue = ActorQueue<Counter>()
138+
let counter = Counter()
139+
actorQueue.adoptExecutionContext(of: counter)
140+
let systemUnderTest = CancellableQueue(underlyingQueue: actorQueue)
141+
let expectation = Expectation()
142+
143+
// Create a task that completes immediately
144+
let task = Task(on: systemUnderTest) { _ in
145+
expectation.fulfill()
146+
try doWork()
147+
}
148+
149+
// Wait for the task to complete
150+
try await task.value
151+
152+
// Now cancel tasks - should have no effect since task already completed
153+
systemUnderTest.cancelTasks()
154+
155+
await expectation.fulfillment(withinSeconds: 30)
156+
}
157+
158+
@Test
159+
func cancelTasks_actorQueue_cancelsCurrentlyExecutingTask() async {
160+
let actorQueue = ActorQueue<Counter>()
161+
let counter = Counter()
162+
actorQueue.adoptExecutionContext(of: counter)
163+
let systemUnderTest = CancellableQueue(underlyingQueue: actorQueue)
164+
let semaphore = Semaphore()
165+
let expectation = Expectation()
166+
167+
// Create a task that waits on a semaphore
168+
Task(on: systemUnderTest) { _ in
169+
await semaphore.wait()
170+
#expect(Task.isCancelled)
171+
expectation.fulfill()
172+
}
173+
174+
// Give the task time to start executing and reach the semaphore
175+
try? await Task.sleep(nanoseconds: 10_000_000)
176+
177+
// Cancel all tasks
178+
systemUnderTest.cancelTasks()
179+
180+
// Signal the semaphore to let the task continue
181+
await semaphore.signal()
182+
183+
await expectation.fulfillment(withinSeconds: 30)
184+
}
185+
186+
@Test
187+
func cancelTasks_actorQueue_cancelsCurrentlyExecutingAndPendingTasks() async {
188+
let actorQueue = ActorQueue<Counter>()
189+
let counter = Counter()
190+
actorQueue.adoptExecutionContext(of: counter)
191+
let systemUnderTest = CancellableQueue(underlyingQueue: actorQueue)
192+
let semaphore = Semaphore()
193+
let expectation = Expectation(expectedCount: 3)
194+
195+
// Create a task that waits on a semaphore (will be executing)
196+
Task(on: systemUnderTest) { _ in
197+
await semaphore.wait()
198+
#expect(Task.isCancelled)
199+
expectation.fulfill()
200+
}
201+
202+
// Create pending tasks that won't start until the first task suspends
203+
Task(on: systemUnderTest) { _ in
204+
#expect(Task.isCancelled)
205+
expectation.fulfill()
206+
}
207+
208+
Task(on: systemUnderTest) { _ in
209+
#expect(Task.isCancelled)
210+
expectation.fulfill()
211+
}
212+
213+
// Give the first task time to start executing
214+
try? await Task.sleep(nanoseconds: 10_000_000)
215+
216+
// Cancel all tasks
217+
systemUnderTest.cancelTasks()
218+
219+
// Signal the semaphore to let tasks continue
220+
await semaphore.signal()
221+
222+
await expectation.fulfillment(withinSeconds: 30)
223+
}
224+
225+
@Test
226+
func cancelTasks_actorQueue_doesNotCancelFutureTasks() async throws {
227+
let actorQueue = ActorQueue<Counter>()
228+
let counter = Counter()
229+
actorQueue.adoptExecutionContext(of: counter)
230+
let systemUnderTest = CancellableQueue(underlyingQueue: actorQueue)
231+
let expectation = Expectation()
232+
233+
// Cancel tasks before creating any
234+
systemUnderTest.cancelTasks()
235+
236+
// Create a task after cancellation - it should NOT be cancelled
237+
let task = Task(on: systemUnderTest) { _ in
238+
#expect(!Task.isCancelled)
239+
expectation.fulfill()
240+
try doWork()
241+
}
242+
243+
try await task.value
244+
await expectation.fulfillment(withinSeconds: 30)
245+
}
246+
247+
// MARK: MainActor Queue Tests
248+
249+
@Test
250+
func cancelTasks_mainActorQueue_doesNotCancelCompletedTask() async throws {
251+
let systemUnderTest = CancellableQueue(underlyingQueue: MainActor.queue)
252+
let expectation = Expectation()
253+
254+
// Create a task that completes immediately
255+
let task = Task(on: systemUnderTest) {
256+
expectation.fulfill()
257+
try doWork()
258+
}
259+
260+
// Wait for the task to complete
261+
try await task.value
262+
263+
// Now cancel tasks - should have no effect since task already completed
264+
systemUnderTest.cancelTasks()
265+
266+
await expectation.fulfillment(withinSeconds: 30)
267+
}
268+
269+
@Test
270+
func cancelTasks_mainActorQueue_cancelsCurrentlyExecutingTask() async {
271+
let systemUnderTest = CancellableQueue(underlyingQueue: MainActor.queue)
272+
let semaphore = Semaphore()
273+
let expectation = Expectation()
274+
275+
// Create a task that waits on a semaphore
276+
Task(on: systemUnderTest) {
277+
await semaphore.wait()
278+
#expect(Task.isCancelled)
279+
expectation.fulfill()
280+
}
281+
282+
// Give the task time to start executing and reach the semaphore
283+
try? await Task.sleep(nanoseconds: 10_000_000)
284+
285+
// Cancel all tasks
286+
systemUnderTest.cancelTasks()
287+
288+
// Signal the semaphore to let the task continue
289+
await semaphore.signal()
290+
291+
await expectation.fulfillment(withinSeconds: 30)
292+
}
293+
294+
@Test
295+
func cancelTasks_mainActorQueue_cancelsCurrentlyExecutingAndPendingTasks() async {
296+
let systemUnderTest = CancellableQueue(underlyingQueue: MainActor.queue)
297+
let semaphore = Semaphore()
298+
let expectation = Expectation(expectedCount: 3)
299+
300+
// Create a task that waits on a semaphore (will be executing)
301+
Task(on: systemUnderTest) {
302+
await semaphore.wait()
303+
#expect(Task.isCancelled)
304+
expectation.fulfill()
305+
}
306+
307+
// Create pending tasks that won't start until the first task suspends
308+
Task(on: systemUnderTest) {
309+
#expect(Task.isCancelled)
310+
expectation.fulfill()
311+
}
312+
313+
Task(on: systemUnderTest) {
314+
#expect(Task.isCancelled)
315+
expectation.fulfill()
316+
}
317+
318+
// Give the first task time to start executing
319+
try? await Task.sleep(nanoseconds: 10_000_000)
320+
321+
// Cancel all tasks
322+
systemUnderTest.cancelTasks()
323+
324+
// Signal the semaphore to let tasks continue
325+
await semaphore.signal()
326+
327+
await expectation.fulfillment(withinSeconds: 30)
328+
}
329+
330+
@Test
331+
func cancelTasks_mainActorQueue_doesNotCancelFutureTasks() async throws {
332+
let systemUnderTest = CancellableQueue(underlyingQueue: MainActor.queue)
333+
let expectation = Expectation()
334+
335+
// Cancel tasks before creating any
336+
systemUnderTest.cancelTasks()
337+
338+
// Create a task after cancellation - it should NOT be cancelled
339+
let task = Task(on: systemUnderTest) {
340+
#expect(!Task.isCancelled)
341+
expectation.fulfill()
342+
try doWork()
343+
}
344+
345+
try await task.value
346+
await expectation.fulfillment(withinSeconds: 30)
347+
}
348+
349+
// MARK: Private
350+
351+
@Sendable
352+
private func doWork() throws {}
353+
}

0 commit comments

Comments
 (0)