diff --git a/Sources/Nimble/Utils/AsyncAwait.swift b/Sources/Nimble/Utils/AsyncAwait.swift index c11c7983..724cb113 100644 --- a/Sources/Nimble/Utils/AsyncAwait.swift +++ b/Sources/Nimble/Utils/AsyncAwait.swift @@ -44,7 +44,7 @@ internal enum AsyncPollResult { } } -final class BlockingTask: Sendable { +private final class BlockingTask: Sendable { private nonisolated(unsafe) var finished = false private nonisolated(unsafe) var continuation: CheckedContinuation? = nil let sourceLocation: SourceLocation diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index c6dd95f4..a45da4aa 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -81,6 +81,30 @@ internal enum PollStatus { case incomplete } +private final class LockBox: Sendable { + private nonisolated(unsafe) var value: T + + private let lock = NSLock() + + init(value: T) { + self.value = value + } + + var currentValue: T { + lock.lock() + defer { + lock.unlock() + } + return value + } + + func set(_ newValue: T) { + lock.lock() + value = newValue + lock.unlock() + } +} + func synchronousWaitUntil( timeout: NimbleTimeInterval, fnName: String, @@ -100,23 +124,20 @@ Please use Swift Testing's `confirmation(...)` APIs to accomplish (nearly) the s return guaranteeNotNested(fnName: fnName, sourceLocation: sourceLocation) { let runloop = RunLoop.current - nonisolated(unsafe) var result = PollResult.timedOut - let lock = NSLock() + let resultBox = LockBox(value: PollResult.timedOut) let doneBlock: () -> Void = { let onFinish = { - lock.lock() - defer { lock.unlock() } - if case .completed = result { + if case .completed = resultBox.currentValue { fail("waitUntil(...) expects its completion closure to be only called once", location: sourceLocation) return } + resultBox.set(.completed(())) #if canImport(CoreFoundation) CFRunLoopStop(CFRunLoopGetCurrent()) #else RunLoop.main._stop() #endif - result = .completed(()) } if Thread.isMainThread { onFinish() @@ -127,9 +148,7 @@ Please use Swift Testing's `confirmation(...)` APIs to accomplish (nearly) the s let capture = NMBExceptionCapture( handler: ({ exception in - lock.lock() - defer { lock.unlock() } - result = .raisedException(exception) + resultBox.set(.raisedException(exception)) }), finally: ({ }) ) @@ -137,23 +156,22 @@ Please use Swift Testing's `confirmation(...)` APIs to accomplish (nearly) the s do { try closure(doneBlock) } catch { - lock.lock() - defer { lock.unlock() } - result = .errorThrown(error) + resultBox.set(.errorThrown(error)) } } - if Thread.isMainThread { - runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval)) - } else { - DispatchQueue.main.sync { - _ = runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval)) + let start = Date() + while case .timedOut = resultBox.currentValue, abs(start.timeIntervalSinceNow) < timeout.timeInterval { + if Thread.isMainThread { + runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval)) + } else { + DispatchQueue.main.sync { + _ = runloop.run(mode: .default, before: Date(timeIntervalSinceNow: timeout.timeInterval)) + } } } - lock.lock() - defer { lock.unlock() } - return result + return resultBox.currentValue } } diff --git a/Tests/NimbleTests/AsyncAwaitTest.swift b/Tests/NimbleTests/AsyncAwaitTest.swift index 4f345566..c3e94d72 100644 --- a/Tests/NimbleTests/AsyncAwaitTest.swift +++ b/Tests/NimbleTests/AsyncAwaitTest.swift @@ -298,6 +298,74 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len timer.cancel() } + func testWaitUntilNestedMainQueueAsyncCalls() async { + await waitUntil(timeout: .seconds(1)) { done in + DispatchQueue.main.async { + DispatchQueue.main.async { + DispatchQueue.main.async { + done() + } + } + } + } + } + + func testWaitUntilOperationQueueWithMainUnderlyingQueueAndBarrier() async { + await waitUntil(timeout: .seconds(1)) { done in + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = .main + + let operation = BlockOperation {} + + operationQueue.addOperation(operation) + operationQueue.addBarrierBlock { + done() + } + } + } + + func testNestedMainAsyncWithOperationQueue() async { + await waitUntil(timeout: .seconds(1)) { done in + let indexingQueue = DispatchQueue.main + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = indexingQueue + + indexingQueue.async { + indexingQueue.async { + indexingQueue.async { + let operation = BlockOperation {} + + operationQueue.addOperation(operation) + operationQueue.addBarrierBlock { + done() + } + } + } + } + } + } + + func testNestedBackgroundAsyncWithOperationQueue() async { + await waitUntil(timeout: .seconds(1)) { done in + let indexingQueue = DispatchQueue(label: "test.queue") + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = indexingQueue + + indexingQueue.async { + indexingQueue.async { + indexingQueue.async { + let operation = BlockOperation {} + + operationQueue.addOperation(operation) + operationQueue.addBarrierBlock { + done() + } + } + } + } + } + } + final class ClassUnderTest { var deinitCalled: (() -> Void)? var count = 0 diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index e3abae65..93236393 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -219,6 +219,74 @@ final class PollingTest: XCTestCase { #endif // canImport(Darwin) } + func testWaitUntilNestedMainQueueAsyncCalls() { + waitUntil(timeout: .seconds(1)) { done in + DispatchQueue.main.async { + DispatchQueue.main.async { + DispatchQueue.main.async { + done() + } + } + } + } + } + + func testWaitUntilOperationQueueWithMainUnderlyingQueueAndBarrier() { + waitUntil(timeout: .seconds(1)) { done in + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = .main + + let operation = BlockOperation {} + + operationQueue.addOperation(operation) + operationQueue.addBarrierBlock { + done() + } + } + } + + func testNestedMainAsyncWithOperationQueue() { + waitUntil(timeout: .seconds(1)) { done in + let indexingQueue = DispatchQueue.main + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = indexingQueue + + indexingQueue.async { + indexingQueue.async { + indexingQueue.async { + let operation = BlockOperation {} + + operationQueue.addOperation(operation) + operationQueue.addBarrierBlock { + done() + } + } + } + } + } + } + + func testNestedBackgroundAsyncWithOperationQueue() { + waitUntil(timeout: .seconds(1)) { done in + let indexingQueue = DispatchQueue(label: "test.queue") + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = indexingQueue + + indexingQueue.async { + indexingQueue.async { + indexingQueue.async { + let operation = BlockOperation {} + + operationQueue.addOperation(operation) + operationQueue.addBarrierBlock { + done() + } + } + } + } + } + } + func testWaitUntilAllowsInBackgroundThread() { #if !SWIFT_PACKAGE var executedAsyncBlock: Bool = false