Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 322f9be

Browse files
committedOct 14, 2024·
Eliminate concurrency warnings in polling expectations
1 parent 27cef92 commit 322f9be

26 files changed

+151
-105
lines changed
 

‎Sources/Nimble/Adapters/NMBExpectation.swift

+22-7
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,42 @@ private func from(objcMatcher: NMBMatcher) -> Matcher<NSObject> {
1313
}
1414

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

23-
@objc public init(actualBlock: @escaping () -> NSObject?, negative: Bool, file: FileString, line: UInt) {
23+
@objc public init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt) {
2424
self._actualBlock = actualBlock
2525
self._negative = negative
2626
self._file = file
2727
self._line = line
28+
self._timeout = .seconds(1)
29+
}
30+
31+
private init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt, timeout: NimbleTimeInterval) {
32+
self._actualBlock = actualBlock
33+
self._negative = negative
34+
self._file = file
35+
self._line = line
36+
self._timeout = timeout
2837
}
2938

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

3443
@objc public var withTimeout: (TimeInterval) -> NMBExpectation {
35-
return { timeout in self._timeout = timeout.nimbleInterval
36-
return self
44+
return { timeout in
45+
NMBExpectation(
46+
actualBlock: self._actualBlock,
47+
negative: self._negative,
48+
file: self._file,
49+
line: self._line,
50+
timeout: timeout.nimbleInterval
51+
)
3752
}
3853
}
3954

‎Sources/Nimble/AsyncExpression.swift

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure
4-
private final class MemoizedClosure<T>: Sendable {
4+
private final class MemoizedClosure<T: Sendable>: Sendable {
55
enum State {
66
case notStarted
77
case inProgress
@@ -13,17 +13,17 @@ private final class MemoizedClosure<T>: Sendable {
1313
nonisolated(unsafe) private var _continuations = [CheckedContinuation<T, Error>]()
1414
nonisolated(unsafe) private var _task: Task<Void, Never>?
1515

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

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

2222
deinit {
2323
_task?.cancel()
2424
}
2525

26-
@Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> sending T {
26+
@Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> T {
2727
if withoutCaching {
2828
try await closure()
2929
} else {
@@ -66,7 +66,9 @@ private final class MemoizedClosure<T>: Sendable {
6666

6767
// Memoizes the given closure, only calling the passed
6868
// closure once; even if repeat calls to the returned closure
69-
private func memoizedClosure<T>(_ closure: sending @escaping () async throws -> sending T) -> @Sendable (Bool) async throws -> sending T {
69+
private func memoizedClosure<T: Sendable>(
70+
_ closure: sending @escaping @Sendable () async throws -> T
71+
) -> @Sendable (Bool) async throws -> T {
7072
let memoized = MemoizedClosure(closure)
7173
return memoized.callAsFunction(_:)
7274
}
@@ -82,7 +84,7 @@ private func memoizedClosure<T>(_ closure: sending @escaping () async throws ->
8284
///
8385
/// This provides a common consumable API for matchers to utilize to allow
8486
/// Nimble to change internals to how the captured closure is managed.
85-
public struct AsyncExpression<Value> {
87+
public actor AsyncExpression<Value: Sendable> {
8688
internal let _expression: @Sendable (Bool) async throws -> sending Value?
8789
internal let _withoutCaching: Bool
8890
public let location: SourceLocation

‎Sources/Nimble/DSL+AsyncAwait.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public func waitUntil(
9393
file: FileString = #filePath,
9494
line: UInt = #line,
9595
column: UInt = #column,
96-
action: sending @escaping (@escaping @Sendable () -> Void) async -> Void
96+
action: @escaping @Sendable (@escaping @Sendable () -> Void) async -> Void
9797
) async {
9898
await throwableUntil(
9999
timeout: timeout,
@@ -116,7 +116,7 @@ public func waitUntil(
116116
file: FileString = #filePath,
117117
line: UInt = #line,
118118
column: UInt = #column,
119-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void
119+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void
120120
) async {
121121
await throwableUntil(
122122
timeout: timeout,
@@ -134,7 +134,7 @@ private enum ErrorResult {
134134
private func throwableUntil(
135135
timeout: NimbleTimeInterval,
136136
sourceLocation: SourceLocation,
137-
action: sending @escaping (@escaping @Sendable () -> Void) async throws -> Void) async {
137+
action: @escaping @Sendable (@escaping @Sendable () -> Void) async throws -> Void) async {
138138
let leeway = timeout.divided
139139
let result = await performBlock(
140140
timeoutInterval: timeout,

‎Sources/Nimble/DSL+Require.swift

+12-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// `require` will return the result of the expression if the matcher passes, and throw an error if not.
44
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
55
@discardableResult
6-
public func require<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncRequirement<T> {
6+
public func require<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncRequirement<T> {
77
return SyncRequirement(
88
expression: Expression(
99
expression: expression,
@@ -17,7 +17,7 @@ public func require<T>(fileID: String = #fileID, file: FileString = #filePath, l
1717
/// `require` will return the result of the expression if the matcher passes, and throw an error if not.
1818
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
1919
@discardableResult
20-
public func require<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncRequirement<T> {
20+
public func require<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncRequirement<T> {
2121
return SyncRequirement(
2222
expression: Expression(
2323
expression: expression(),
@@ -31,7 +31,7 @@ public func require<T>(fileID: String = #fileID, file: FileString = #filePath, l
3131
/// `require` will return the result of the expression if the matcher passes, and throw an error if not.
3232
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
3333
@discardableResult
34-
public func require<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncRequirement<T> {
34+
public func require<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncRequirement<T> {
3535
return SyncRequirement(
3636
expression: Expression(
3737
expression: expression(),
@@ -45,7 +45,7 @@ public func require<T>(fileID: String = #fileID, file: FileString = #filePath, l
4545
/// `require` will return the result of the expression if the matcher passes, and throw an error if not.
4646
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
4747
@discardableResult
48-
public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncRequirement<Void> {
48+
public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncRequirement<Void> {
4949
return SyncRequirement(
5050
expression: Expression(
5151
expression: expression(),
@@ -61,7 +61,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, line
6161
///
6262
/// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``.
6363
@discardableResult
64-
public func requires<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncRequirement<T> {
64+
public func requires<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncRequirement<T> {
6565
return SyncRequirement(
6666
expression: Expression(
6767
expression: expression,
@@ -77,7 +77,7 @@ public func requires<T>(fileID: String = #fileID, file: FileString = #filePath,
7777
///
7878
/// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``.
7979
@discardableResult
80-
public func requires<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncRequirement<T> {
80+
public func requires<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncRequirement<T> {
8181
return SyncRequirement(
8282
expression: Expression(
8383
expression: expression(),
@@ -93,7 +93,7 @@ public func requires<T>(fileID: String = #fileID, file: FileString = #filePath,
9393
///
9494
/// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``.
9595
@discardableResult
96-
public func requires<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncRequirement<T> {
96+
public func requires<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncRequirement<T> {
9797
return SyncRequirement(
9898
expression: Expression(
9999
expression: expression(),
@@ -109,7 +109,7 @@ public func requires<T>(fileID: String = #fileID, file: FileString = #filePath,
109109
///
110110
/// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``.
111111
@discardableResult
112-
public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncRequirement<Void> {
112+
public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncRequirement<Void> {
113113
return SyncRequirement(
114114
expression: Expression(
115115
expression: expression(),
@@ -216,7 +216,7 @@ public func requirea<T: Sendable>(fileID: String = #fileID, file: FileString = #
216216
/// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil.
217217
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
218218
@discardableResult
219-
public func unwrap<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) throws -> T {
219+
public func unwrap<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) throws -> T {
220220
try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description)
221221
}
222222

@@ -226,7 +226,7 @@ public func unwrap<T>(fileID: String = #fileID, file: FileString = #filePath, li
226226
/// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil.
227227
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
228228
@discardableResult
229-
public func unwrap<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) throws -> T {
229+
public func unwrap<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T {
230230
try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description)
231231
}
232232

@@ -236,7 +236,7 @@ public func unwrap<T>(fileID: String = #fileID, file: FileString = #filePath, li
236236
/// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil.
237237
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
238238
@discardableResult
239-
public func unwraps<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) throws -> T {
239+
public func unwraps<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) throws -> T {
240240
try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description)
241241
}
242242

@@ -246,7 +246,7 @@ public func unwraps<T>(fileID: String = #fileID, file: FileString = #filePath, l
246246
/// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil.
247247
/// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown.
248248
@discardableResult
249-
public func unwraps<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) throws -> T {
249+
public func unwraps<T>(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T {
250250
try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description)
251251
}
252252

‎Sources/Nimble/DSL+Wait.swift

+20-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class NMBWait: NSObject {
2323
file: FileString = #filePath,
2424
line: UInt = #line,
2525
column: UInt = #column,
26-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
26+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
2727
// Convert TimeInterval to NimbleTimeInterval
2828
until(timeout: timeout.nimbleInterval, file: file, line: line, action: action)
2929
}
@@ -35,7 +35,7 @@ public class NMBWait: NSObject {
3535
file: FileString = #filePath,
3636
line: UInt = #line,
3737
column: UInt = #column,
38-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
38+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
3939
return throwableUntil(timeout: timeout, file: file, line: line) { done in
4040
action(done)
4141
}
@@ -48,9 +48,10 @@ public class NMBWait: NSObject {
4848
file: FileString = #filePath,
4949
line: UInt = #line,
5050
column: UInt = #column,
51-
action: sending @escaping (@escaping @Sendable () -> Void) throws -> Void) {
51+
action: @escaping @Sendable (@escaping @Sendable () -> Void) throws -> Void) {
5252
let awaiter = NimbleEnvironment.activeInstance.awaiter
5353
let leeway = timeout.divided
54+
let location = SourceLocation(fileID: fileID, filePath: file, line: line, column: column)
5455
let result = awaiter.performBlock(file: file, line: line) { (done: @escaping @Sendable (ErrorResult) -> Void) throws -> Void in
5556
DispatchQueue.main.async {
5657
let capture = NMBExceptionCapture(
@@ -69,10 +70,12 @@ public class NMBWait: NSObject {
6970
}
7071
}
7172
}
72-
}.timeout(timeout, forcefullyAbortTimeout: leeway).wait(
73-
"waitUntil(...)",
74-
sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column)
75-
)
73+
}
74+
.timeout(timeout, forcefullyAbortTimeout: leeway)
75+
.wait(
76+
"waitUntil(...)",
77+
sourceLocation: location
78+
)
7679

7780
switch result {
7881
case .incomplete: internalError("Reached .incomplete state for waitUntil(...).")
@@ -110,7 +113,8 @@ public class NMBWait: NSObject {
110113
file: FileString = #filePath,
111114
line: UInt = #line,
112115
column: UInt = #column,
113-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
116+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
117+
until(timeout: .seconds(1), fileID: fileID, file: file, line: line, column: column, action: action)
114118
}
115119
#else
116120
public class func until(
@@ -137,7 +141,14 @@ internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTime
137141
/// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function
138142
/// is executing. Any attempts to touch the run loop may cause non-deterministic behavior.
139143
@available(*, noasync, message: "the sync variant of `waitUntil` does not work in async contexts. Use the async variant as a drop-in replacement")
140-
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) {
144+
public func waitUntil(
145+
timeout: NimbleTimeInterval = PollingDefaults.timeout,
146+
fileID: String = #fileID,
147+
file: FileString = #filePath,
148+
line: UInt = #line,
149+
column: UInt = #column,
150+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void
151+
) {
141152
NMBWait.until(timeout: timeout, fileID: fileID, file: file, line: line, column: column, action: action)
142153
}
143154

0 commit comments

Comments
 (0)
Please sign in to comment.