Skip to content

Commit

Permalink
Eliminate concurrency warnings in polling expectations
Browse files Browse the repository at this point in the history
  • Loading branch information
younata committed Jul 18, 2024
1 parent 99c5a39 commit 73b8963
Show file tree
Hide file tree
Showing 26 changed files with 173 additions and 135 deletions.
29 changes: 22 additions & 7 deletions Sources/Nimble/Adapters/NMBExpectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,42 @@ private func from(objcMatcher: NMBMatcher) -> Matcher<NSObject> {
}

// Equivalent to Expectation, but for Nimble's Objective-C interface
public class NMBExpectation: NSObject {
internal let _actualBlock: () -> NSObject?
internal var _negative: Bool
public final class NMBExpectation: NSObject, Sendable {
internal let _actualBlock: @Sendable () -> NSObject?
internal let _negative: Bool
internal let _file: FileString
internal let _line: UInt
internal var _timeout: NimbleTimeInterval = .seconds(1)
internal let _timeout: NimbleTimeInterval

@objc public init(actualBlock: @escaping () -> NSObject?, negative: Bool, file: FileString, line: UInt) {
@objc public init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt) {
self._actualBlock = actualBlock
self._negative = negative
self._file = file
self._line = line
self._timeout = .seconds(1)
}

private init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt, timeout: NimbleTimeInterval) {
self._actualBlock = actualBlock
self._negative = negative
self._file = file
self._line = line
self._timeout = timeout
}

private var expectValue: SyncExpectation<NSObject> {
return expect(file: _file, line: _line, self._actualBlock() as NSObject?)
}

@objc public var withTimeout: (TimeInterval) -> NMBExpectation {
return { timeout in self._timeout = timeout.nimbleInterval
return self
return { timeout in
NMBExpectation(
actualBlock: self._actualBlock,
negative: self._negative,
file: self._file,
line: self._line,
timeout: timeout.nimbleInterval
)
}
}

Expand Down
14 changes: 8 additions & 6 deletions Sources/Nimble/AsyncExpression.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure
private final class MemoizedClosure<T>: Sendable {
private final class MemoizedClosure<T: Sendable>: Sendable {
enum State {
case notStarted
case inProgress
Expand All @@ -11,17 +11,17 @@ private final class MemoizedClosure<T>: Sendable {
nonisolated(unsafe) private var _continuations = [CheckedContinuation<T, Error>]()
nonisolated(unsafe) private var _task: Task<Void, Never>?

nonisolated(unsafe) let closure: () async throws -> sending T
let closure: @Sendable () async throws -> T

init(_ closure: @escaping () async throws -> sending T) {
init(_ closure: @escaping @Sendable () async throws -> T) {
self.closure = closure
}

deinit {
_task?.cancel()
}

@Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> sending T {
@Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> T {
if withoutCaching {
try await closure()
} else {
Expand Down Expand Up @@ -64,7 +64,9 @@ private final class MemoizedClosure<T>: Sendable {

// Memoizes the given closure, only calling the passed
// closure once; even if repeat calls to the returned closure
private func memoizedClosure<T>(_ closure: sending @escaping () async throws -> sending T) -> @Sendable (Bool) async throws -> sending T {
private func memoizedClosure<T: Sendable>(
_ closure: sending @escaping @Sendable () async throws -> T
) -> @Sendable (Bool) async throws -> T {
let memoized = MemoizedClosure(closure)
return memoized.callAsFunction(_:)
}
Expand All @@ -80,7 +82,7 @@ private func memoizedClosure<T>(_ closure: sending @escaping () async throws ->
///
/// This provides a common consumable API for matchers to utilize to allow
/// Nimble to change internals to how the captured closure is managed.
public struct AsyncExpression<Value> {
public actor AsyncExpression<Value: Sendable> {
internal let _expression: @Sendable (Bool) async throws -> sending Value?
internal let _withoutCaching: Bool
public let location: SourceLocation
Expand Down
19 changes: 9 additions & 10 deletions Sources/Nimble/DSL+AsyncAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ public func expecta(file: FileString = #file, line: UInt = #line, _ expression:
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: sending @escaping (@escaping @Sendable () -> Void) async -> Void) async {
await throwableUntil(timeout: timeout) { done in
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping @Sendable (@escaping @Sendable () -> Void) async -> Void) async {
await throwableUntil(timeout: timeout, sourceLocation: SourceLocation(file: file, line: line)) { done in
await action(done)
}
}
Expand All @@ -100,8 +100,8 @@ public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fil
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) async {
await throwableUntil(timeout: timeout, file: file, line: line) { done in
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) async {
await throwableUntil(timeout: timeout, sourceLocation: SourceLocation(file: file, line: line)) { done in
action(done)
}
}
Expand All @@ -113,14 +113,13 @@ private enum ErrorResult {

private func throwableUntil(
timeout: NimbleTimeInterval,
file: FileString = #file,
line: UInt = #line,
action: sending @escaping (@escaping @Sendable () -> Void) async throws -> Void) async {
sourceLocation: SourceLocation,
action: @escaping @Sendable (@escaping @Sendable () -> Void) async throws -> Void) async {
let leeway = timeout.divided
let result = await performBlock(
timeoutInterval: timeout,
leeway: leeway,
file: file, line: line) { @MainActor (done: @escaping @Sendable (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 All @@ -134,9 +133,9 @@ private func throwableUntil(
case .incomplete: internalError("Reached .incomplete state for waitUntil(...).")
case .blockedRunLoop:
fail(blockedRunLoopErrorMessageFor("-waitUntil()", leeway: leeway),
file: file, line: line)
file: sourceLocation.file, line: sourceLocation.line)
case .timedOut:
fail("Waited more than \(timeout.description)", file: file, line: line)
fail("Waited more than \(timeout.description)", file: sourceLocation.file, line: sourceLocation.line)
case let .errorThrown(error):
fail("Unexpected error thrown: \(error)")
case .completed(.error(let error)):
Expand Down
24 changes: 12 additions & 12 deletions Sources/Nimble/DSL+Require.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public func require<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure @escaping () throws -> sending T?
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
) -> SyncRequirement<T> {
return SyncRequirement(
expression: Expression(
Expand All @@ -26,7 +26,7 @@ public func require<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending T)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
) -> SyncRequirement<T> {
return SyncRequirement(
expression: Expression(
Expand All @@ -45,7 +45,7 @@ public func require<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending T?)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
) -> SyncRequirement<T> {
return SyncRequirement(
expression: Expression(
Expand All @@ -64,7 +64,7 @@ public func require(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending Void)
_ expression: @autoclosure () -> (@Sendable () throws -> Void)
) -> SyncRequirement<Void> {
return SyncRequirement(
expression: Expression(
Expand All @@ -85,7 +85,7 @@ public func requires<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure @escaping () throws -> sending T?
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
) -> SyncRequirement<T> {
return SyncRequirement(
expression: Expression(
Expand All @@ -106,7 +106,7 @@ public func requires<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending T)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
) -> SyncRequirement<T> {
return SyncRequirement(
expression: Expression(
Expand All @@ -127,7 +127,7 @@ public func requires<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending T?)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
) -> SyncRequirement<T> {
return SyncRequirement(
expression: Expression(
Expand All @@ -148,7 +148,7 @@ public func requires(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending Void)
_ expression: @autoclosure () -> (@Sendable () throws -> sending Void)
) -> SyncRequirement<Void> {
return SyncRequirement(
expression: Expression(
Expand Down Expand Up @@ -260,7 +260,7 @@ public func unwrap<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure @escaping () throws -> sending T?
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
) throws -> T {
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
}
Expand All @@ -275,7 +275,7 @@ public func unwrap<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending T?)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
) throws -> T {
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
}
Expand All @@ -290,7 +290,7 @@ public func unwraps<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure @escaping () throws -> sending T?
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
) throws -> T {
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
}
Expand All @@ -305,7 +305,7 @@ public func unwraps<T>(
file: FileString = #file,
line: UInt = #line,
customError: Error? = nil,
_ expression: @autoclosure () -> sending (() throws -> sending T?)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
) throws -> T {
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
}
Expand Down
20 changes: 14 additions & 6 deletions Sources/Nimble/DSL+Wait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class NMBWait: NSObject {
timeout: TimeInterval,
file: FileString = #file,
line: UInt = #line,
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
// Convert TimeInterval to NimbleTimeInterval
until(timeout: timeout.nimbleInterval, file: file, line: line, action: action)
}
Expand All @@ -31,7 +31,7 @@ public class NMBWait: NSObject {
timeout: NimbleTimeInterval,
file: FileString = #file,
line: UInt = #line,
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
return throwableUntil(timeout: timeout, file: file, line: line) { done in
action(done)
}
Expand All @@ -42,9 +42,10 @@ public class NMBWait: NSObject {
timeout: NimbleTimeInterval,
file: FileString = #file,
line: UInt = #line,
action: sending @escaping (@escaping @Sendable () -> Void) throws -> Void) {
action: @escaping @Sendable (@escaping @Sendable () -> Void) throws -> Void) {
let awaiter = NimbleEnvironment.activeInstance.awaiter
let leeway = timeout.divided
let location = SourceLocation(file: file, line: line)
let result = awaiter.performBlock(file: file, line: line) { (done: @escaping @Sendable (ErrorResult) -> Void) throws -> Void in
DispatchQueue.main.async {
let capture = NMBExceptionCapture(
Expand All @@ -63,7 +64,9 @@ public class NMBWait: NSObject {
}
}
}
}.timeout(timeout, forcefullyAbortTimeout: leeway).wait("waitUntil(...)", file: file, line: line)
}
.timeout(timeout, forcefullyAbortTimeout: leeway)
.wait("waitUntil(...)", sourceLocation: location)

switch result {
case .incomplete: internalError("Reached .incomplete state for waitUntil(...).")
Expand All @@ -90,7 +93,7 @@ public class NMBWait: NSObject {
public class func until(
_ file: FileString = #file,
line: UInt = #line,
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
until(timeout: .seconds(1), file: file, line: line, action: action)
}
#else
Expand All @@ -116,7 +119,12 @@ 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, file: FileString = #file, line: UInt = #line, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
public func waitUntil(
timeout: NimbleTimeInterval = PollingDefaults.timeout,
file: FileString = #file,
line: UInt = #line,
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void
) {
NMBWait.until(timeout: timeout, file: file, line: line, action: action)
}

Expand Down
16 changes: 8 additions & 8 deletions Sources/Nimble/DSL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
public func expect<T>(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure @escaping () throws -> sending T?
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
) -> SyncExpectation<T> {
return SyncExpectation(
expression: Expression(
Expand All @@ -15,7 +15,7 @@ public func expect<T>(
public func expect<T>(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure () -> sending (() throws -> sending T)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
) -> SyncExpectation<T> {
return SyncExpectation(
expression: Expression(
Expand All @@ -28,7 +28,7 @@ public func expect<T>(
public func expect<T>(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure () -> sending (() throws -> sending T?)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
) -> SyncExpectation<T> {
return SyncExpectation(
expression: Expression(
Expand All @@ -41,7 +41,7 @@ public func expect<T>(
public func expect(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure () -> sending (() throws -> sending Void)
_ expression: @autoclosure () -> (@Sendable () throws -> Void)
) -> SyncExpectation<Void> {
return SyncExpectation(
expression: Expression(
Expand All @@ -55,7 +55,7 @@ public func expect(
public func expects<T>(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure @escaping () throws -> sending T?
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
) -> SyncExpectation<T> {
return SyncExpectation(
expression: Expression(
Expand All @@ -69,7 +69,7 @@ public func expects<T>(
public func expects<T>(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure () -> sending (() throws -> sending T)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
) -> SyncExpectation<T> {
return SyncExpectation(
expression: Expression(
Expand All @@ -83,7 +83,7 @@ public func expects<T>(
public func expects<T>(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure () -> sending (() throws -> sending T?)
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
) -> SyncExpectation<T> {
return SyncExpectation(
expression: Expression(
Expand All @@ -97,7 +97,7 @@ public func expects<T>(
public func expects(
file: FileString = #file,
line: UInt = #line,
_ expression: @autoclosure () -> sending (() throws -> sending Void)
_ expression: @autoclosure () -> (@Sendable () throws -> Void)
) -> SyncExpectation<Void> {
return SyncExpectation(
expression: Expression(
Expand Down
Loading

0 comments on commit 73b8963

Please sign in to comment.