@@ -16,80 +16,84 @@ import Testing
16
16
17
17
@testable import DispatchAsync
18
18
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 ( )
48
38
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
+ }
62
44
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 )
64
47
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
70
49
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 ( )
78
52
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 ( )
84
58
85
- sharedPoolCompletionCount = existingPoolCompletionCount + 1
59
+ #expect( didEnterCriticalSection) // waiter must have run
60
+ }
86
61
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
+ }
89
93
}
90
94
}
91
- }
92
95
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
+ }
95
99
}
0 commit comments