Skip to content

Commit

Permalink
Update the waitUntil DSL to be sendable... ish
Browse files Browse the repository at this point in the history
Still more to do here. Will likely to require further refactoring
  • Loading branch information
younata committed Oct 14, 2024
1 parent 5cd0493 commit b504403
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 37 deletions.
2 changes: 2 additions & 0 deletions Sources/Nimble/AsyncExpression.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

/// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure
private final class MemoizedClosure<T>: Sendable {
enum State {
Expand Down
8 changes: 4 additions & 4 deletions Sources/Nimble/DSL+AsyncAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public func waitUntil(
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
action: @escaping (@escaping () -> Void) async -> Void
action: sending @escaping (@escaping @Sendable () -> Void) async -> Void
) async {
await throwableUntil(
timeout: timeout,
Expand All @@ -116,7 +116,7 @@ public func waitUntil(
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
action: @escaping (@escaping () -> Void) -> Void
action: sending @escaping (@escaping @Sendable () -> Void) -> Void
) async {
await throwableUntil(
timeout: timeout,
Expand All @@ -134,12 +134,12 @@ private enum ErrorResult {
private func throwableUntil(
timeout: NimbleTimeInterval,
sourceLocation: SourceLocation,
action: @escaping (@escaping () -> Void) async throws -> Void) async {
action: sending @escaping (@escaping @Sendable () -> Void) async throws -> Void) async {
let leeway = timeout.divided
let result = await performBlock(
timeoutInterval: timeout,
leeway: leeway,
sourceLocation: sourceLocation) { @MainActor (done: @escaping (ErrorResult) -> Void) async throws -> Void in
sourceLocation: sourceLocation) { @MainActor (done: @escaping @Sendable (ErrorResult) -> Void) async throws -> Void in
do {
try await action {
done(.none)
Expand Down
13 changes: 6 additions & 7 deletions Sources/Nimble/DSL+Wait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class NMBWait: NSObject {
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
action: @escaping (@escaping () -> Void) -> Void) {
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
// Convert TimeInterval to NimbleTimeInterval
until(timeout: timeout.nimbleInterval, file: file, line: line, action: action)
}
Expand All @@ -35,7 +35,7 @@ public class NMBWait: NSObject {
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
action: @escaping (@escaping () -> Void) -> Void) {
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
return throwableUntil(timeout: timeout, file: file, line: line) { done in
action(done)
}
Expand All @@ -48,10 +48,10 @@ public class NMBWait: NSObject {
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
action: @escaping (@escaping () -> Void) throws -> Void) {
action: sending @escaping (@escaping @Sendable () -> Void) throws -> Void) {
let awaiter = NimbleEnvironment.activeInstance.awaiter
let leeway = timeout.divided
let result = awaiter.performBlock(file: file, line: line) { (done: @escaping (ErrorResult) -> Void) throws -> Void in
let result = awaiter.performBlock(file: file, line: line) { (done: @escaping @Sendable (ErrorResult) -> Void) throws -> Void in
DispatchQueue.main.async {
let capture = NMBExceptionCapture(
handler: ({ exception in
Expand Down Expand Up @@ -110,8 +110,7 @@ public class NMBWait: NSObject {
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
action: @escaping (@escaping () -> Void) -> Void) {
until(timeout: .seconds(1), fileID: fileID, file: file, line: line, column: column, action: action)
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
}
#else
public class func until(
Expand All @@ -138,7 +137,7 @@ internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTime
/// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function
/// is executing. Any attempts to touch the run loop may cause non-deterministic behavior.
@available(*, noasync, message: "the sync variant of `waitUntil` does not work in async contexts. Use the async variant as a drop-in replacement")
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) {
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
NMBWait.until(timeout: timeout, fileID: fileID, file: file, line: line, column: column, action: action)
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Nimble/Utils/AsyncAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ private func runAwaitTrigger<T>(
timeoutInterval: NimbleTimeInterval,
leeway: NimbleTimeInterval,
sourceLocation: SourceLocation,
_ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void
_ closure: @escaping (@escaping @Sendable (T) -> Void) async throws -> Void
) async -> AsyncPollResult<T> {
let timeoutQueue = awaiter.timeoutQueue
let completionCount = Box(value: 0)
Expand Down Expand Up @@ -315,7 +315,7 @@ internal func performBlock<T>(
timeoutInterval: NimbleTimeInterval,
leeway: NimbleTimeInterval,
sourceLocation: SourceLocation,
_ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void
_ closure: @escaping (@escaping @Sendable (T) -> Void) async throws -> Void
) async -> AsyncPollResult<T> {
await runAwaitTrigger(
awaiter: NimbleEnvironment.activeInstance.awaiter,
Expand Down
51 changes: 27 additions & 24 deletions Sources/Nimble/Utils/PollAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal final class AssertionWaitLock: WaitLock, @unchecked Sendable {
}
}

internal enum PollResult<T> {
internal enum PollResult<T: Sendable>: Sendable {
/// Incomplete indicates None (aka - this value hasn't been fulfilled yet)
case incomplete
/// TimedOut indicates the result reached its defined timeout limit before returning
Expand Down Expand Up @@ -104,9 +104,9 @@ internal enum PollStatus {

/// Holds the resulting value from an asynchronous expectation.
/// This class is thread-safe at receiving a "response" to this promise.
internal final class AwaitPromise<T> {
private(set) internal var asyncResult: PollResult<T> = .incomplete
private var signal: DispatchSemaphore
internal final class AwaitPromise<T: Sendable>: Sendable {
nonisolated(unsafe) private(set) internal var asyncResult: PollResult<T> = .incomplete
private let signal: DispatchSemaphore

init() {
signal = DispatchSemaphore(value: 1)
Expand Down Expand Up @@ -142,7 +142,7 @@ internal struct PollAwaitTrigger {
///
/// This factory stores all the state for an async expectation so that Await doesn't
/// doesn't have to manage it.
internal class AwaitPromiseBuilder<T> {
internal class AwaitPromiseBuilder<T: Sendable> {
let awaiter: Awaiter
let waitLock: WaitLock
let trigger: PollAwaitTrigger
Expand Down Expand Up @@ -313,36 +313,39 @@ internal class Awaiter {
return DispatchSource.makeTimerSource(flags: .strict, queue: queue)
}

func performBlock<T>(
func performBlock<T: Sendable>(
file: FileString,
line: UInt,
_ closure: @escaping (@escaping (T) -> Void) throws -> Void
_ closure: sending @escaping (@escaping @Sendable (T) -> Void) throws -> Void
) -> AwaitPromiseBuilder<T> {
let promise = AwaitPromise<T>()
let timeoutSource = createTimerSource(timeoutQueue)
var completionCount = 0
nonisolated(unsafe) var completionCount = 0
let lock = NSRecursiveLock()
let trigger = PollAwaitTrigger(timeoutSource: timeoutSource, actionSource: nil) {
try closure { result in
completionCount += 1
if completionCount < 2 {
func completeBlock() {
if promise.resolveResult(.completed(result)) {
#if canImport(CoreFoundation)
CFRunLoopStop(CFRunLoopGetMain())
#else
RunLoop.main._stop()
#endif
lock.withLock {
completionCount += 1
if completionCount < 2 {
@Sendable func completeBlock() {
if promise.resolveResult(.completed(result)) {
#if canImport(CoreFoundation)
CFRunLoopStop(CFRunLoopGetMain())
#else
RunLoop.main._stop()
#endif
}
}
}

if Thread.isMainThread {
completeBlock()
if Thread.isMainThread {
completeBlock()
} else {
DispatchQueue.main.async { completeBlock() }
}
} else {
DispatchQueue.main.async { completeBlock() }
fail("waitUntil(..) expects its completion closure to be only called once",
file: file, line: line)
}
} else {
fail("waitUntil(..) expects its completion closure to be only called once",
file: file, line: line)
}
}
}
Expand Down

0 comments on commit b504403

Please sign in to comment.