Skip to content

Commit b559168

Browse files
committed
test: Add port of libdispatch/tests/dispatch_group.c test
# Conflicts: # Tests/DispatchAsyncTests/DispatchGroupTests.swift
1 parent 3dca480 commit b559168

File tree

1 file changed

+162
-89
lines changed

1 file changed

+162
-89
lines changed

Tests/DispatchAsyncTests/DispatchGroupTests.swift

Lines changed: 162 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -13,111 +13,184 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
@_spi(DispatchAsync) import DispatchAsync
16+
import func Foundation.sin
17+
#if !os(WASI)
18+
import class Foundation.Thread
19+
#endif
1620
import Testing
1721

1822
private typealias DispatchGroup = DispatchAsync.DispatchGroup
1923

20-
@Test
21-
func dispatchGroupOrderCleanliness() async throws {
22-
// Repeating this 100 times to help rule out
23-
// edge cases that only show up some of the time
24-
for index in 0 ..< 100 {
25-
Task {
26-
actor Result {
27-
private(set) var value = ""
28-
29-
func append(value: String) {
30-
self.value.append(value)
24+
@Suite("DispatchGroup Tests")
25+
struct DispatchGroupTests {
26+
@Test
27+
func dispatchGroupOrderCleanliness() async throws {
28+
// Repeating this 1000 times to help rule out
29+
// edge cases that only show up some of the time
30+
for iteration in 0 ..< 1000 {
31+
Task {
32+
actor Result {
33+
private(set) var value = ""
34+
35+
func append(value: String) {
36+
self.value.append(value)
37+
}
3138
}
32-
}
3339

34-
let result = Result()
40+
let result = Result()
3541

36-
let group = DispatchGroup()
37-
await result.append(value: "|🔵\(index)")
42+
let group = DispatchGroup()
43+
await result.append(value: "|🔵\(iteration)")
3844

39-
group.enter()
40-
Task {
41-
await result.append(value: "🟣/")
42-
group.leave()
43-
}
45+
group.enter()
46+
Task {
47+
await result.append(value: "🟣/")
48+
group.leave()
49+
}
4450

45-
group.enter()
46-
Task {
47-
await result.append(value: "🟣^")
48-
group.leave()
49-
}
51+
group.enter()
52+
Task {
53+
await result.append(value: "🟣^")
54+
group.leave()
55+
}
5056

51-
group.enter()
52-
Task {
53-
await result.append(value: "🟣\\")
54-
group.leave()
57+
group.enter()
58+
Task {
59+
await result.append(value: "🟣\\")
60+
group.leave()
61+
}
62+
63+
await withCheckedContinuation { continuation in
64+
group.notify(queue: .main) {
65+
Task {
66+
await result.append(value: "🟢\(iteration)=")
67+
continuation.resume()
68+
}
69+
}
70+
}
71+
72+
let finalValue = await result.value
73+
74+
/// NOTE: If you need to visually debug issues, you can uncomment
75+
/// the following to watch a visual representation of the group ordering.
76+
///
77+
/// In general, you'll see something like the following printed over and over
78+
/// to the console:
79+
///
80+
/// ```
81+
/// |🔵42🟣/🟣^🟣\🟢42=
82+
/// ```
83+
///
84+
/// What you should observe:
85+
///
86+
/// - The index number be the same at the beginning and end of each line, and it
87+
/// should always increment by one.
88+
/// - The 🔵 should always be first, and the 🟢 should always be last for each line.
89+
/// - There should always be 3 🟣's in between the 🔵 and 🟢.
90+
/// - The ordering of the 🟣 can be random, and that is fine.
91+
///
92+
/// For example, for of the following are valid outputs:
93+
///
94+
/// ```
95+
/// // GOOD
96+
/// |🔵42🟣/🟣^🟣\🟢42=
97+
/// ```
98+
///
99+
/// ```
100+
/// // GOOD
101+
/// |🔵42🟣/🟣\🟣^🟢42=
102+
/// ```
103+
///
104+
/// But the following would not be valid:
105+
///
106+
/// ```
107+
/// // BAD!
108+
/// |🔵43🟣/🟣^🟣\🟢43=
109+
/// |🔵42🟣/🟣^🟣\🟢42=
110+
/// |🔵44🟣/🟣^🟣\🟢44=
111+
/// ```
112+
///
113+
/// ```
114+
/// // BAD!
115+
/// |🔵42🟣/🟣^🟢42🟣\=
116+
/// ```
117+
///
118+
119+
// NOTE: Uncomment to use troubleshooting method above:
120+
// print(finalValue)
121+
122+
#expect(finalValue.prefix(1) == "|")
123+
#expect(finalValue.count { $0 == "🟣" } == 3)
124+
#expect(finalValue.count { $0 == "🟢" } == 1)
125+
#expect(finalValue.lastIndex(of: "🟣")! < finalValue.firstIndex(of: "🟢")!)
126+
#expect(finalValue.suffix(1) == "=")
55127
}
128+
}
129+
}
130+
131+
/// Swift port of libdispatch/tests/dispatch_group.c
132+
/// The original C test stresses `dispatch_group_wait` by enqueuing a bunch of
133+
/// math-heavy blocks on a global queue, then waiting for them to finish with a
134+
/// timeout. It also verifies that `notify` is invoked exactly once.
135+
@Test(.timeLimit(.minutes(1)))
136+
func dispatchGroupStress() async throws {
137+
let iterations = 1000
138+
// We use a separate concurrent queue rather than the global queue to avoid interference issues
139+
// with other tests running in parallel
140+
let workQueue = DispatchQueue(attributes: .concurrent)
141+
let group = DispatchGroup()
56142

57-
await withCheckedContinuation { continuation in
58-
group.notify(queue: .main) {
59-
Task {
60-
await result.append(value: "🟢\(index)=")
61-
continuation.resume()
143+
let isolationQueue = DispatchQueue(label: "isolationQueue")
144+
nonisolated(unsafe) var counter = 0
145+
146+
for _ in 0 ..< iterations {
147+
group.enter()
148+
workQueue.async {
149+
// We alternate between two options for workload. One is a simple
150+
// math function, the other is a thread sleep.
151+
//
152+
// Alternating between those two approaches provides variance to
153+
// increases failure chances if there are race conditions subject to timing
154+
// and load.
155+
if Bool.random() {
156+
#if !os(WASI)
157+
Thread.sleep(forTimeInterval: 0.00001) // 10_000 nanoseconds
158+
#endif
159+
} else {
160+
// A small math workload similar to the original C test which used
161+
// sin(random()). We iterate a couple thousand times to keep the CPU
162+
// busy long enough for the group scheduling to matter.
163+
var x = Double.random(in: 0.0 ... Double.pi)
164+
for _ in 0 ..< 2_000 {
165+
x = sin(x)
62166
}
63167
}
168+
169+
isolationQueue.async {
170+
counter += 1
171+
group.leave()
172+
}
64173
}
174+
}
175+
176+
// NOTE: The test has a 1 minute time limit that will time out. In
177+
// the original code, this timeout was 5 seconds, but currently
178+
// the shortest timeout Swift Testing provides is 1 minute.
179+
await group.wait()
65180

66-
let finalValue = await result.value
67-
68-
/// NOTE: If you need to visually debug issues, you can uncomment
69-
/// the following to watch a visual representation of the group ordering.
70-
///
71-
/// In general, you'll see something like the following printed over and over
72-
/// to the console:
73-
///
74-
/// ```
75-
/// |🔵42🟣/🟣^🟣\🟢42=
76-
/// ```
77-
///
78-
/// What you should observe:
79-
///
80-
/// - The index number be the same at the beginning and end of each line, and it
81-
/// should always increment by one.
82-
/// - The 🔵 should always be first, and the 🟢 should always be last for each line.
83-
/// - There should always be 3 🟣's in between the 🔵 and 🟢.
84-
/// - The ordering of the 🟣 can be random, and that is fine.
85-
///
86-
/// For example, for of the following are valid outputs:
87-
///
88-
/// ```
89-
/// // GOOD
90-
/// |🔵42🟣/🟣^🟣\🟢42=
91-
/// ```
92-
///
93-
/// ```
94-
/// // GOOD
95-
/// |🔵42🟣/🟣\🟣^🟢42=
96-
/// ```
97-
///
98-
/// But the following would not be valid:
99-
///
100-
/// ```
101-
/// // BAD!
102-
/// |🔵43🟣/🟣^🟣\🟢43=
103-
/// |🔵42🟣/🟣^🟣\🟢42=
104-
/// |🔵44🟣/🟣^🟣\🟢44=
105-
/// ```
106-
///
107-
/// ```
108-
/// // BAD!
109-
/// |🔵42🟣/🟣^🟢42🟣\=
110-
/// ```
111-
///
112-
113-
// Uncomment to use troubleshooting method above:
114-
// print(finalValue)
115-
116-
#expect(finalValue.prefix(1) == "|")
117-
#expect(finalValue.count { $0 == "🟣" } == 3)
118-
#expect(finalValue.count { $0 == "🟢" } == 1)
119-
#expect(finalValue.lastIndex(of: "🟣")! < finalValue.firstIndex(of: "🟢")!)
120-
#expect(finalValue.suffix(1) == "=")
181+
// Verify notify fires exactly once.
182+
nonisolated(unsafe) var notifyHits = 0
183+
await withCheckedContinuation { k in
184+
group.notify(queue: .main) {
185+
notifyHits += 1
186+
k.resume()
187+
}
121188
}
189+
#expect(notifyHits == 1)
190+
191+
let finalCount = counter
192+
#expect(finalCount == iterations)
122193
}
123194
}
195+
196+

0 commit comments

Comments
 (0)