@@ -16,80 +16,84 @@ import Testing
1616
1717@testable import DispatchAsync
1818
19- @available ( macOS 13 , iOS 16 , tvOS 16 , watchOS 9 , * )
20- @Test ( . timeLimit( . minutes( 1 ) ) )
21- func asyncSemaphoreWaitSignal( ) async throws {
22- let semaphore = AsyncSemaphore ( value: 1 )
23-
24- // First wait should succeed immediately and bring the count to 0
25- await semaphore. wait ( )
26-
27- // Launch a task that tries to wait – it should be suspended until we signal
28- nonisolated ( unsafe) var didEnterCriticalSection = false
29- await withCheckedContinuation { continuation in
30- Task { @Sendable in
31- // Ensure the rest of this test doesn't
32- // proceed until the Task block has started executing
33- continuation. resume ( )
34-
35- await semaphore. wait ( )
36- didEnterCriticalSection = true
37- await semaphore. signal ( )
38- }
39- }
40-
41- // Allow the task a few cycles to reach the initial semaphore.wait()
42- try ? await Task . sleep ( nanoseconds: 1_000 )
43-
44- #expect( !didEnterCriticalSection) // should still be waiting
45-
46- // Now release the semaphore – the waiter should proceed
47- await semaphore. signal ( )
19+ nonisolated ( unsafe) private var sharedPoolCompletionCount = 0
20+
21+ @Suite ( " DispatchGroup Tests " )
22+ class AsyncSemaphoreTests {
23+ @available ( macOS 13 , iOS 16 , tvOS 16 , watchOS 9 , * )
24+ @Test ( . timeLimit( . minutes( 1 ) ) )
25+ func asyncSemaphoreWaitSignal( ) async throws {
26+ let semaphore = AsyncSemaphore ( value: 1 )
27+
28+ // First wait should succeed immediately and bring the count to 0
29+ await semaphore. wait ( )
30+
31+ // Launch a task that tries to wait – it should be suspended until we signal
32+ nonisolated ( unsafe) var didEnterCriticalSection = false
33+ await withCheckedContinuation { continuation in
34+ Task { @Sendable in
35+ // Ensure the rest of this test doesn't
36+ // proceed until the Task block has started executing
37+ continuation. resume ( )
4838
49- // Wait for second signal to fire from inside the task above
50- // There is a timeout on this test, so if there is a problem
51- // we'll either hit the timeout and fail, or didEnterCriticalSection
52- // will be false below
53- await semaphore. wait ( )
54-
55- #expect( didEnterCriticalSection) // waiter must have run
56- }
57-
58- @Test func basicAsyncSemaphoreTest( ) async throws {
59- nonisolated ( unsafe) var sharedPoolCompletionCount = 0
60- sharedPoolCompletionCount = 0 // Reset to 0 for each test run
61- let totalConcurrentPools = 10
39+ await semaphore. wait ( )
40+ didEnterCriticalSection = true
41+ await semaphore. signal ( )
42+ }
43+ }
6244
63- let semaphore = AsyncSemaphore ( value: 1 )
45+ // Allow the task a few cycles to reach the initial semaphore.wait()
46+ try ? await Task . sleep ( nanoseconds: 1_000 )
6447
65- await withTaskGroup ( of: Void . self) { group in
66- for _ in 0 ..< totalConcurrentPools {
67- group. addTask {
68- // Wait for any other pools currently holding the semaphore
69- await semaphore. wait ( )
48+ #expect( !didEnterCriticalSection) // should still be waiting
7049
71- // Only one task should mutate counter at a time
72- //
73- // If there are issues with the semaphore, then
74- // we would expect to grab incorrect values here occasionally,
75- // which would result in an incorrect final completion count.
76- //
77- let existingPoolCompletionCount = sharedPoolCompletionCount
50+ // Now release the semaphore – the waiter should proceed
51+ await semaphore. signal ( )
7852
79- // Add artificial delay to amplify race conditions
80- // Pools started shortly after this "semaphore-locked"
81- // pool starts will run before this line, unless
82- // this pool contains a valid lock.
83- try ? await Task . sleep ( nanoseconds : 100 )
53+ // Wait for second signal to fire from inside the task above
54+ // There is a timeout on this test, so if there is a problem
55+ // we'll either hit the timeout and fail, or didEnterCriticalSection
56+ // will be false below
57+ await semaphore . wait ( )
8458
85- sharedPoolCompletionCount = existingPoolCompletionCount + 1
59+ #expect( didEnterCriticalSection) // waiter must have run
60+ }
8661
87- // When we exit this flow, release our hold on the semaphore
88- await semaphore. signal ( )
62+ @Test func basicAsyncSemaphoreTest( ) async throws {
63+ sharedPoolCompletionCount = 0 // Reset to 0 for each test run
64+ let totalConcurrentPools = 10
65+
66+ let semaphore = AsyncSemaphore ( value: 1 )
67+
68+ await withTaskGroup ( of: Void . self) { group in
69+ for _ in 0 ..< totalConcurrentPools {
70+ group. addTask {
71+ // Wait for any other pools currently holding the semaphore
72+ await semaphore. wait ( )
73+
74+ // Only one task should mutate counter at a time
75+ //
76+ // If there are issues with the semaphore, then
77+ // we would expect to grab incorrect values here occasionally,
78+ // which would result in an incorrect final completion count.
79+ //
80+ let existingPoolCompletionCount = sharedPoolCompletionCount
81+
82+ // Add artificial delay to amplify race conditions
83+ // Pools started shortly after this "semaphore-locked"
84+ // pool starts will run before this line, unless
85+ // this pool contains a valid lock.
86+ try ? await Task . sleep ( nanoseconds: 100 )
87+
88+ sharedPoolCompletionCount = existingPoolCompletionCount + 1
89+
90+ // When we exit this flow, release our hold on the semaphore
91+ await semaphore. signal ( )
92+ }
8993 }
9094 }
91- }
9295
93- // After all tasks are done, counter should be 10
94- #expect( sharedPoolCompletionCount == totalConcurrentPools)
96+ // After all tasks are done, counter should be 10
97+ #expect( sharedPoolCompletionCount == totalConcurrentPools)
98+ }
9599}
0 commit comments