Skip to content

Commit f265cda

Browse files
committed
Fix waitUntil, which was broken in the new implementation of polling expectations
This one is wild, because apparently, RunLoop.run(mode:before:) wasn't actually blocking for the amount of time specified. It was basically spinning the runloop once, then returning. This change now continuously spins the runloop until either the timeout passes, or a result is set.
1 parent 5a0cc37 commit f265cda

File tree

4 files changed

+175
-21
lines changed

4 files changed

+175
-21
lines changed

Sources/Nimble/Utils/AsyncAwait.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal enum AsyncPollResult<T> {
4444
}
4545
}
4646

47-
final class BlockingTask: Sendable {
47+
private final class BlockingTask: Sendable {
4848
private nonisolated(unsafe) var finished = false
4949
private nonisolated(unsafe) var continuation: CheckedContinuation<Void, Never>? = nil
5050
let sourceLocation: SourceLocation

Sources/Nimble/Utils/PollAwait.swift

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,30 @@ internal enum PollStatus {
8181
case incomplete
8282
}
8383

84+
private final class LockBox<T>: Sendable {
85+
private nonisolated(unsafe) var value: T
86+
87+
private let lock = NSLock()
88+
89+
init(value: T) {
90+
self.value = value
91+
}
92+
93+
var currentValue: T {
94+
lock.lock()
95+
defer {
96+
lock.unlock()
97+
}
98+
return value
99+
}
100+
101+
func set(_ newValue: T) {
102+
lock.lock()
103+
value = newValue
104+
lock.unlock()
105+
}
106+
}
107+
84108
func synchronousWaitUntil(
85109
timeout: NimbleTimeInterval,
86110
fnName: String,
@@ -100,23 +124,20 @@ Please use Swift Testing's `confirmation(...)` APIs to accomplish (nearly) the s
100124
return guaranteeNotNested(fnName: fnName, sourceLocation: sourceLocation) {
101125
let runloop = RunLoop.current
102126

103-
nonisolated(unsafe) var result = PollResult<Void>.timedOut
104-
let lock = NSLock()
127+
let resultBox = LockBox(value: PollResult<Void>.timedOut)
105128

106129
let doneBlock: () -> Void = {
107130
let onFinish = {
108-
lock.lock()
109-
defer { lock.unlock() }
110-
if case .completed = result {
131+
if case .completed = resultBox.currentValue {
111132
fail("waitUntil(...) expects its completion closure to be only called once", location: sourceLocation)
112133
return
113134
}
135+
resultBox.set(.completed(()))
114136
#if canImport(CoreFoundation)
115137
CFRunLoopStop(CFRunLoopGetCurrent())
116138
#else
117139
RunLoop.main._stop()
118140
#endif
119-
result = .completed(())
120141
}
121142
if Thread.isMainThread {
122143
onFinish()
@@ -127,33 +148,30 @@ Please use Swift Testing's `confirmation(...)` APIs to accomplish (nearly) the s
127148

128149
let capture = NMBExceptionCapture(
129150
handler: ({ exception in
130-
lock.lock()
131-
defer { lock.unlock() }
132-
result = .raisedException(exception)
151+
resultBox.set(.raisedException(exception))
133152
}),
134153
finally: ({ })
135154
)
136155
capture.tryBlock {
137156
do {
138157
try closure(doneBlock)
139158
} catch {
140-
lock.lock()
141-
defer { lock.unlock() }
142-
result = .errorThrown(error)
159+
resultBox.set(.errorThrown(error))
143160
}
144161
}
145162

146-
if Thread.isMainThread {
147-
runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval))
148-
} else {
149-
DispatchQueue.main.sync {
150-
_ = runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval))
163+
let start = Date()
164+
while case .timedOut = resultBox.currentValue, abs(start.timeIntervalSinceNow) < timeout.timeInterval {
165+
if Thread.isMainThread {
166+
runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval))
167+
} else {
168+
DispatchQueue.main.sync {
169+
_ = runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval))
170+
}
151171
}
152172
}
153173

154-
lock.lock()
155-
defer { lock.unlock() }
156-
return result
174+
return resultBox.currentValue
157175
}
158176
}
159177

Tests/NimbleTests/AsyncAwaitTest.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,74 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len
298298
timer.cancel()
299299
}
300300

301+
func testWaitUntilNestedMainQueueAsyncCalls() async {
302+
await waitUntil(timeout: .seconds(1)) { done in
303+
DispatchQueue.main.async {
304+
DispatchQueue.main.async {
305+
DispatchQueue.main.async {
306+
done()
307+
}
308+
}
309+
}
310+
}
311+
}
312+
313+
func testWaitUntilOperationQueueWithMainUnderlyingQueueAndBarrier() async {
314+
await waitUntil(timeout: .seconds(1)) { done in
315+
let operationQueue = OperationQueue()
316+
operationQueue.underlyingQueue = .main
317+
318+
let operation = BlockOperation {}
319+
320+
operationQueue.addOperation(operation)
321+
operationQueue.addBarrierBlock {
322+
done()
323+
}
324+
}
325+
}
326+
327+
func testNestedMainAsyncWithOperationQueue() async {
328+
await waitUntil(timeout: .seconds(1)) { done in
329+
let indexingQueue = DispatchQueue.main
330+
let operationQueue = OperationQueue()
331+
operationQueue.underlyingQueue = indexingQueue
332+
333+
indexingQueue.async {
334+
indexingQueue.async {
335+
indexingQueue.async {
336+
let operation = BlockOperation {}
337+
338+
operationQueue.addOperation(operation)
339+
operationQueue.addBarrierBlock {
340+
done()
341+
}
342+
}
343+
}
344+
}
345+
}
346+
}
347+
348+
func testNestedBackgroundAsyncWithOperationQueue() async {
349+
await waitUntil(timeout: .seconds(1)) { done in
350+
let indexingQueue = DispatchQueue(label: "test.queue")
351+
let operationQueue = OperationQueue()
352+
operationQueue.underlyingQueue = indexingQueue
353+
354+
indexingQueue.async {
355+
indexingQueue.async {
356+
indexingQueue.async {
357+
let operation = BlockOperation {}
358+
359+
operationQueue.addOperation(operation)
360+
operationQueue.addBarrierBlock {
361+
done()
362+
}
363+
}
364+
}
365+
}
366+
}
367+
}
368+
301369
final class ClassUnderTest {
302370
var deinitCalled: (() -> Void)?
303371
var count = 0

Tests/NimbleTests/PollingTest.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,74 @@ final class PollingTest: XCTestCase {
219219
#endif // canImport(Darwin)
220220
}
221221

222+
func testWaitUntilNestedMainQueueAsyncCalls() {
223+
waitUntil(timeout: .seconds(1)) { done in
224+
DispatchQueue.main.async {
225+
DispatchQueue.main.async {
226+
DispatchQueue.main.async {
227+
done()
228+
}
229+
}
230+
}
231+
}
232+
}
233+
234+
func testWaitUntilOperationQueueWithMainUnderlyingQueueAndBarrier() {
235+
waitUntil(timeout: .seconds(1)) { done in
236+
let operationQueue = OperationQueue()
237+
operationQueue.underlyingQueue = .main
238+
239+
let operation = BlockOperation {}
240+
241+
operationQueue.addOperation(operation)
242+
operationQueue.addBarrierBlock {
243+
done()
244+
}
245+
}
246+
}
247+
248+
func testNestedMainAsyncWithOperationQueue() {
249+
waitUntil(timeout: .seconds(1)) { done in
250+
let indexingQueue = DispatchQueue.main
251+
let operationQueue = OperationQueue()
252+
operationQueue.underlyingQueue = indexingQueue
253+
254+
indexingQueue.async {
255+
indexingQueue.async {
256+
indexingQueue.async {
257+
let operation = BlockOperation {}
258+
259+
operationQueue.addOperation(operation)
260+
operationQueue.addBarrierBlock {
261+
done()
262+
}
263+
}
264+
}
265+
}
266+
}
267+
}
268+
269+
func testNestedBackgroundAsyncWithOperationQueue() {
270+
waitUntil(timeout: .seconds(1)) { done in
271+
let indexingQueue = DispatchQueue(label: "test.queue")
272+
let operationQueue = OperationQueue()
273+
operationQueue.underlyingQueue = indexingQueue
274+
275+
indexingQueue.async {
276+
indexingQueue.async {
277+
indexingQueue.async {
278+
let operation = BlockOperation {}
279+
280+
operationQueue.addOperation(operation)
281+
operationQueue.addBarrierBlock {
282+
done()
283+
}
284+
}
285+
}
286+
}
287+
}
288+
}
289+
222290
func testWaitUntilAllowsInBackgroundThread() {
223291
#if !SWIFT_PACKAGE
224292
var executedAsyncBlock: Bool = false

0 commit comments

Comments
 (0)