Skip to content

Commit b93b829

Browse files
authored
Merge pull request #727 from Quick/8.x-waitUntil-race-condition-fix
[8.x] Wait until race condition fix
2 parents b02b00b + c7db238 commit b93b829

File tree

3 files changed

+46
-3
lines changed

3 files changed

+46
-3
lines changed

Sources/Nimble/Utils/Await.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,11 +301,19 @@ internal class Awaiter {
301301
let timeoutSource = createTimerSource(timeoutQueue)
302302
var completionCount = 0
303303
let trigger = AwaitTrigger(timeoutSource: timeoutSource, actionSource: nil) {
304-
try closure {
304+
try closure { result in
305305
completionCount += 1
306306
if completionCount < 2 {
307-
if promise.resolveResult(.completed($0)) {
308-
CFRunLoopStop(CFRunLoopGetMain())
307+
func completeBlock() {
308+
if promise.resolveResult(.completed(result)) {
309+
CFRunLoopStop(CFRunLoopGetMain())
310+
}
311+
}
312+
313+
if Thread.isMainThread {
314+
completeBlock()
315+
} else {
316+
DispatchQueue.main.async { completeBlock() }
309317
}
310318
} else {
311319
fail("waitUntil(..) expects its completion closure to be only called once",

Tests/NimbleTests/AsynchronousTest.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Dispatch
2+
import CoreFoundation
23
import Foundation
34
import XCTest
45
import Nimble
@@ -165,6 +166,39 @@ final class AsyncTest: XCTestCase, XCTestCaseProvider {
165166
}
166167
}
167168

169+
func testWaitUntilDoesNotCompleteBeforeRunLoopIsWaiting() {
170+
// This verifies the fix for a race condition in which `done()` is
171+
// called asynchronously on a background thread after the main thread checks
172+
// for completion, but prior to `RunLoop.current.run(mode:before:)` being called.
173+
// This race condition resulted in the RunLoop locking up.
174+
var failed = false
175+
176+
let timeoutQueue = DispatchQueue(label: "Nimble.waitUntilTest.timeout", qos: .background)
177+
let timer = DispatchSource.makeTimerSource(flags: .strict, queue: timeoutQueue)
178+
timer.schedule(
179+
deadline: DispatchTime.now() + 5,
180+
repeating: .never,
181+
leeway: DispatchTimeInterval.milliseconds(1)
182+
)
183+
timer.setEventHandler {
184+
failed = true
185+
fail("Timed out: Main RunLoop stalled.")
186+
CFRunLoopStop(CFRunLoopGetMain())
187+
}
188+
timer.resume()
189+
190+
for index in 0..<100 {
191+
if failed { break }
192+
waitUntil(line: UInt(index)) { done in
193+
DispatchQueue(label: "Nimble.waitUntilTest.\(index)").async {
194+
done()
195+
}
196+
}
197+
}
198+
199+
timer.cancel()
200+
}
201+
168202
func testWaitUntilMustBeInMainThread() {
169203
#if !SWIFT_PACKAGE
170204
var executedAsyncBlock: Bool = false

Tests/NimbleTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extension AsyncTest {
2020
("testToEventuallyPositiveMatches", testToEventuallyPositiveMatches),
2121
("testToEventuallyWithCustomDefaultTimeout", testToEventuallyWithCustomDefaultTimeout),
2222
("testWaitUntilDetectsStalledMainThreadActivity", testWaitUntilDetectsStalledMainThreadActivity),
23+
("testWaitUntilDoesNotCompleteBeforeRunLoopIsWaiting", testWaitUntilDoesNotCompleteBeforeRunLoopIsWaiting),
2324
("testWaitUntilErrorsIfDoneIsCalledMultipleTimes", testWaitUntilErrorsIfDoneIsCalledMultipleTimes),
2425
("testWaitUntilMustBeInMainThread", testWaitUntilMustBeInMainThread),
2526
("testWaitUntilNegativeMatches", testWaitUntilNegativeMatches),

0 commit comments

Comments
 (0)