Skip to content

Commit 73b8963

Browse files
committed
Eliminate concurrency warnings in polling expectations
1 parent 99c5a39 commit 73b8963

26 files changed

+173
-135
lines changed

Sources/Nimble/Adapters/NMBExpectation.swift

Lines changed: 22 additions & 7 deletions
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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure
2-
private final class MemoizedClosure<T>: Sendable {
2+
private final class MemoizedClosure<T: Sendable>: Sendable {
33
enum State {
44
case notStarted
55
case inProgress
@@ -11,17 +11,17 @@ private final class MemoizedClosure<T>: Sendable {
1111
nonisolated(unsafe) private var _continuations = [CheckedContinuation<T, Error>]()
1212
nonisolated(unsafe) private var _task: Task<Void, Never>?
1313

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

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

2020
deinit {
2121
_task?.cancel()
2222
}
2323

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

6565
// Memoizes the given closure, only calling the passed
6666
// closure once; even if repeat calls to the returned closure
67-
private func memoizedClosure<T>(_ closure: sending @escaping () async throws -> sending T) -> @Sendable (Bool) async throws -> sending T {
67+
private func memoizedClosure<T: Sendable>(
68+
_ closure: sending @escaping @Sendable () async throws -> T
69+
) -> @Sendable (Bool) async throws -> T {
6870
let memoized = MemoizedClosure(closure)
6971
return memoized.callAsFunction(_:)
7072
}
@@ -80,7 +82,7 @@ private func memoizedClosure<T>(_ closure: sending @escaping () async throws ->
8082
///
8183
/// This provides a common consumable API for matchers to utilize to allow
8284
/// Nimble to change internals to how the captured closure is managed.
83-
public struct AsyncExpression<Value> {
85+
public actor AsyncExpression<Value: Sendable> {
8486
internal let _expression: @Sendable (Bool) async throws -> sending Value?
8587
internal let _withoutCaching: Bool
8688
public let location: SourceLocation

Sources/Nimble/DSL+AsyncAwait.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ public func expecta(file: FileString = #file, line: UInt = #line, _ expression:
8787
///
8888
/// @warning
8989
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
90-
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: sending @escaping (@escaping @Sendable () -> Void) async -> Void) async {
91-
await throwableUntil(timeout: timeout) { done in
90+
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping @Sendable (@escaping @Sendable () -> Void) async -> Void) async {
91+
await throwableUntil(timeout: timeout, sourceLocation: SourceLocation(file: file, line: line)) { done in
9292
await action(done)
9393
}
9494
}
@@ -100,8 +100,8 @@ public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fil
100100
///
101101
/// @warning
102102
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
103-
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) async {
104-
await throwableUntil(timeout: timeout, file: file, line: line) { done in
103+
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) async {
104+
await throwableUntil(timeout: timeout, sourceLocation: SourceLocation(file: file, line: line)) { done in
105105
action(done)
106106
}
107107
}
@@ -113,14 +113,13 @@ private enum ErrorResult {
113113

114114
private func throwableUntil(
115115
timeout: NimbleTimeInterval,
116-
file: FileString = #file,
117-
line: UInt = #line,
118-
action: sending @escaping (@escaping @Sendable () -> Void) async throws -> Void) async {
116+
sourceLocation: SourceLocation,
117+
action: @escaping @Sendable (@escaping @Sendable () -> Void) async throws -> Void) async {
119118
let leeway = timeout.divided
120119
let result = await performBlock(
121120
timeoutInterval: timeout,
122121
leeway: leeway,
123-
file: file, line: line) { @MainActor (done: @escaping @Sendable (ErrorResult) -> Void) async throws -> Void in
122+
sourceLocation: sourceLocation) { @MainActor (done: @escaping @Sendable (ErrorResult) -> Void) async throws -> Void in
124123
do {
125124
try await action {
126125
done(.none)
@@ -134,9 +133,9 @@ private func throwableUntil(
134133
case .incomplete: internalError("Reached .incomplete state for waitUntil(...).")
135134
case .blockedRunLoop:
136135
fail(blockedRunLoopErrorMessageFor("-waitUntil()", leeway: leeway),
137-
file: file, line: line)
136+
file: sourceLocation.file, line: sourceLocation.line)
138137
case .timedOut:
139-
fail("Waited more than \(timeout.description)", file: file, line: line)
138+
fail("Waited more than \(timeout.description)", file: sourceLocation.file, line: sourceLocation.line)
140139
case let .errorThrown(error):
141140
fail("Unexpected error thrown: \(error)")
142141
case .completed(.error(let error)):

Sources/Nimble/DSL+Require.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public func require<T>(
77
file: FileString = #file,
88
line: UInt = #line,
99
customError: Error? = nil,
10-
_ expression: @autoclosure @escaping () throws -> sending T?
10+
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
1111
) -> SyncRequirement<T> {
1212
return SyncRequirement(
1313
expression: Expression(
@@ -26,7 +26,7 @@ public func require<T>(
2626
file: FileString = #file,
2727
line: UInt = #line,
2828
customError: Error? = nil,
29-
_ expression: @autoclosure () -> sending (() throws -> sending T)
29+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
3030
) -> SyncRequirement<T> {
3131
return SyncRequirement(
3232
expression: Expression(
@@ -45,7 +45,7 @@ public func require<T>(
4545
file: FileString = #file,
4646
line: UInt = #line,
4747
customError: Error? = nil,
48-
_ expression: @autoclosure () -> sending (() throws -> sending T?)
48+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
4949
) -> SyncRequirement<T> {
5050
return SyncRequirement(
5151
expression: Expression(
@@ -64,7 +64,7 @@ public func require(
6464
file: FileString = #file,
6565
line: UInt = #line,
6666
customError: Error? = nil,
67-
_ expression: @autoclosure () -> sending (() throws -> sending Void)
67+
_ expression: @autoclosure () -> (@Sendable () throws -> Void)
6868
) -> SyncRequirement<Void> {
6969
return SyncRequirement(
7070
expression: Expression(
@@ -85,7 +85,7 @@ public func requires<T>(
8585
file: FileString = #file,
8686
line: UInt = #line,
8787
customError: Error? = nil,
88-
_ expression: @autoclosure @escaping () throws -> sending T?
88+
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
8989
) -> SyncRequirement<T> {
9090
return SyncRequirement(
9191
expression: Expression(
@@ -106,7 +106,7 @@ public func requires<T>(
106106
file: FileString = #file,
107107
line: UInt = #line,
108108
customError: Error? = nil,
109-
_ expression: @autoclosure () -> sending (() throws -> sending T)
109+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
110110
) -> SyncRequirement<T> {
111111
return SyncRequirement(
112112
expression: Expression(
@@ -127,7 +127,7 @@ public func requires<T>(
127127
file: FileString = #file,
128128
line: UInt = #line,
129129
customError: Error? = nil,
130-
_ expression: @autoclosure () -> sending (() throws -> sending T?)
130+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
131131
) -> SyncRequirement<T> {
132132
return SyncRequirement(
133133
expression: Expression(
@@ -148,7 +148,7 @@ public func requires(
148148
file: FileString = #file,
149149
line: UInt = #line,
150150
customError: Error? = nil,
151-
_ expression: @autoclosure () -> sending (() throws -> sending Void)
151+
_ expression: @autoclosure () -> (@Sendable () throws -> sending Void)
152152
) -> SyncRequirement<Void> {
153153
return SyncRequirement(
154154
expression: Expression(
@@ -260,7 +260,7 @@ public func unwrap<T>(
260260
file: FileString = #file,
261261
line: UInt = #line,
262262
customError: Error? = nil,
263-
_ expression: @autoclosure @escaping () throws -> sending T?
263+
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
264264
) throws -> T {
265265
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
266266
}
@@ -275,7 +275,7 @@ public func unwrap<T>(
275275
file: FileString = #file,
276276
line: UInt = #line,
277277
customError: Error? = nil,
278-
_ expression: @autoclosure () -> sending (() throws -> sending T?)
278+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
279279
) throws -> T {
280280
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
281281
}
@@ -290,7 +290,7 @@ public func unwraps<T>(
290290
file: FileString = #file,
291291
line: UInt = #line,
292292
customError: Error? = nil,
293-
_ expression: @autoclosure @escaping () throws -> sending T?
293+
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
294294
) throws -> T {
295295
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
296296
}
@@ -305,7 +305,7 @@ public func unwraps<T>(
305305
file: FileString = #file,
306306
line: UInt = #line,
307307
customError: Error? = nil,
308-
_ expression: @autoclosure () -> sending (() throws -> sending T?)
308+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
309309
) throws -> T {
310310
try requires(file: file, line: line, customError: customError, expression()).toNot(beNil())
311311
}

Sources/Nimble/DSL+Wait.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class NMBWait: NSObject {
2121
timeout: TimeInterval,
2222
file: FileString = #file,
2323
line: UInt = #line,
24-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
24+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
2525
// Convert TimeInterval to NimbleTimeInterval
2626
until(timeout: timeout.nimbleInterval, file: file, line: line, action: action)
2727
}
@@ -31,7 +31,7 @@ public class NMBWait: NSObject {
3131
timeout: NimbleTimeInterval,
3232
file: FileString = #file,
3333
line: UInt = #line,
34-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
34+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
3535
return throwableUntil(timeout: timeout, file: file, line: line) { done in
3636
action(done)
3737
}
@@ -42,9 +42,10 @@ public class NMBWait: NSObject {
4242
timeout: NimbleTimeInterval,
4343
file: FileString = #file,
4444
line: UInt = #line,
45-
action: sending @escaping (@escaping @Sendable () -> Void) throws -> Void) {
45+
action: @escaping @Sendable (@escaping @Sendable () -> Void) throws -> Void) {
4646
let awaiter = NimbleEnvironment.activeInstance.awaiter
4747
let leeway = timeout.divided
48+
let location = SourceLocation(file: file, line: line)
4849
let result = awaiter.performBlock(file: file, line: line) { (done: @escaping @Sendable (ErrorResult) -> Void) throws -> Void in
4950
DispatchQueue.main.async {
5051
let capture = NMBExceptionCapture(
@@ -63,7 +64,9 @@ public class NMBWait: NSObject {
6364
}
6465
}
6566
}
66-
}.timeout(timeout, forcefullyAbortTimeout: leeway).wait("waitUntil(...)", file: file, line: line)
67+
}
68+
.timeout(timeout, forcefullyAbortTimeout: leeway)
69+
.wait("waitUntil(...)", sourceLocation: location)
6770

6871
switch result {
6972
case .incomplete: internalError("Reached .incomplete state for waitUntil(...).")
@@ -90,7 +93,7 @@ public class NMBWait: NSObject {
9093
public class func until(
9194
_ file: FileString = #file,
9295
line: UInt = #line,
93-
action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
96+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) {
9497
until(timeout: .seconds(1), file: file, line: line, action: action)
9598
}
9699
#else
@@ -116,7 +119,12 @@ internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTime
116119
/// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function
117120
/// is executing. Any attempts to touch the run loop may cause non-deterministic behavior.
118121
@available(*, noasync, message: "the sync variant of `waitUntil` does not work in async contexts. Use the async variant as a drop-in replacement")
119-
public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) {
122+
public func waitUntil(
123+
timeout: NimbleTimeInterval = PollingDefaults.timeout,
124+
file: FileString = #file,
125+
line: UInt = #line,
126+
action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void
127+
) {
120128
NMBWait.until(timeout: timeout, file: file, line: line, action: action)
121129
}
122130

Sources/Nimble/DSL.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
public func expect<T>(
33
file: FileString = #file,
44
line: UInt = #line,
5-
_ expression: @autoclosure @escaping () throws -> sending T?
5+
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
66
) -> SyncExpectation<T> {
77
return SyncExpectation(
88
expression: Expression(
@@ -15,7 +15,7 @@ public func expect<T>(
1515
public func expect<T>(
1616
file: FileString = #file,
1717
line: UInt = #line,
18-
_ expression: @autoclosure () -> sending (() throws -> sending T)
18+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
1919
) -> SyncExpectation<T> {
2020
return SyncExpectation(
2121
expression: Expression(
@@ -28,7 +28,7 @@ public func expect<T>(
2828
public func expect<T>(
2929
file: FileString = #file,
3030
line: UInt = #line,
31-
_ expression: @autoclosure () -> sending (() throws -> sending T?)
31+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
3232
) -> SyncExpectation<T> {
3333
return SyncExpectation(
3434
expression: Expression(
@@ -41,7 +41,7 @@ public func expect<T>(
4141
public func expect(
4242
file: FileString = #file,
4343
line: UInt = #line,
44-
_ expression: @autoclosure () -> sending (() throws -> sending Void)
44+
_ expression: @autoclosure () -> (@Sendable () throws -> Void)
4545
) -> SyncExpectation<Void> {
4646
return SyncExpectation(
4747
expression: Expression(
@@ -55,7 +55,7 @@ public func expect(
5555
public func expects<T>(
5656
file: FileString = #file,
5757
line: UInt = #line,
58-
_ expression: @autoclosure @escaping () throws -> sending T?
58+
_ expression: @autoclosure @escaping @Sendable () throws -> sending T?
5959
) -> SyncExpectation<T> {
6060
return SyncExpectation(
6161
expression: Expression(
@@ -69,7 +69,7 @@ public func expects<T>(
6969
public func expects<T>(
7070
file: FileString = #file,
7171
line: UInt = #line,
72-
_ expression: @autoclosure () -> sending (() throws -> sending T)
72+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T)
7373
) -> SyncExpectation<T> {
7474
return SyncExpectation(
7575
expression: Expression(
@@ -83,7 +83,7 @@ public func expects<T>(
8383
public func expects<T>(
8484
file: FileString = #file,
8585
line: UInt = #line,
86-
_ expression: @autoclosure () -> sending (() throws -> sending T?)
86+
_ expression: @autoclosure () -> (@Sendable () throws -> sending T?)
8787
) -> SyncExpectation<T> {
8888
return SyncExpectation(
8989
expression: Expression(
@@ -97,7 +97,7 @@ public func expects<T>(
9797
public func expects(
9898
file: FileString = #file,
9999
line: UInt = #line,
100-
_ expression: @autoclosure () -> sending (() throws -> sending Void)
100+
_ expression: @autoclosure () -> (@Sendable () throws -> Void)
101101
) -> SyncExpectation<Void> {
102102
return SyncExpectation(
103103
expression: Expression(

0 commit comments

Comments
 (0)