Skip to content

Commit 89f5479

Browse files
authored
Make AsyncPredicate Sendable and operate only on Sendable types (#1072)
Make Predicate's closure Sendable, and make Predicate Sendable when the returning value is Sendable
1 parent 5ec0d3a commit 89f5479

File tree

5 files changed

+96
-50
lines changed

5 files changed

+96
-50
lines changed

Sources/Nimble/ExpectationMessage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public indirect enum ExpectationMessage {
1+
public indirect enum ExpectationMessage: Sendable {
22
// --- Primary Expectations ---
33
/// includes actual value in output ("expected to <message>, got <actual>")
44
case expectedActualValueTo(/* message: */ String)
@@ -204,7 +204,7 @@ extension FailureMessage {
204204
#if canImport(Darwin)
205205
import class Foundation.NSObject
206206

207-
public class NMBExpectationMessage: NSObject {
207+
public final class NMBExpectationMessage: NSObject, Sendable {
208208
private let msg: ExpectationMessage
209209

210210
internal init(swift msg: ExpectationMessage) {

Sources/Nimble/Matchers/AsyncPredicate.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ extension Predicate: AsyncablePredicate {
2929
/// These can also be used with either `Expectation`s or `AsyncExpectation`s.
3030
/// But these can only be used from async contexts, and are unavailable in Objective-C.
3131
/// You can, however, call regular Predicates from an AsyncPredicate, if you wish to compose one like that.
32-
public struct AsyncPredicate<T>: AsyncablePredicate {
33-
fileprivate var matcher: (AsyncExpression<T>) async throws -> PredicateResult
32+
public struct AsyncPredicate<T: Sendable>: AsyncablePredicate, Sendable {
33+
fileprivate var matcher: @Sendable (AsyncExpression<T>) async throws -> PredicateResult
3434

35-
public init(_ matcher: @escaping (AsyncExpression<T>) async throws -> PredicateResult) {
35+
public init(_ matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> PredicateResult) {
3636
self.matcher = matcher
3737
}
3838

@@ -48,23 +48,23 @@ public struct AsyncPredicate<T>: AsyncablePredicate {
4848
/// Provides convenience helpers to defining predicates
4949
extension AsyncPredicate {
5050
/// Like Predicate() constructor, but automatically guard against nil (actual) values
51-
public static func define(matcher: @escaping (AsyncExpression<T>) async throws -> PredicateResult) -> AsyncPredicate<T> {
51+
public static func define(matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> PredicateResult) -> AsyncPredicate<T> {
5252
return AsyncPredicate<T> { actual in
5353
return try await matcher(actual)
5454
}.requireNonNil
5555
}
5656

5757
/// Defines a predicate with a default message that can be returned in the closure
5858
/// Also ensures the predicate's actual value cannot pass with `nil` given.
59-
public static func define(_ message: String = "match", matcher: @escaping (AsyncExpression<T>, ExpectationMessage) async throws -> PredicateResult) -> AsyncPredicate<T> {
59+
public static func define(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>, ExpectationMessage) async throws -> PredicateResult) -> AsyncPredicate<T> {
6060
return AsyncPredicate<T> { actual in
6161
return try await matcher(actual, .expectedActualValueTo(message))
6262
}.requireNonNil
6363
}
6464

6565
/// Defines a predicate with a default message that can be returned in the closure
6666
/// Unlike `define`, this allows nil values to succeed if the given closure chooses to.
67-
public static func defineNilable(_ message: String = "match", matcher: @escaping (AsyncExpression<T>, ExpectationMessage) async throws -> PredicateResult) -> AsyncPredicate<T> {
67+
public static func defineNilable(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>, ExpectationMessage) async throws -> PredicateResult) -> AsyncPredicate<T> {
6868
return AsyncPredicate<T> { actual in
6969
return try await matcher(actual, .expectedActualValueTo(message))
7070
}
@@ -74,7 +74,7 @@ extension AsyncPredicate {
7474
/// error message.
7575
///
7676
/// Also ensures the predicate's actual value cannot pass with `nil` given.
77-
public static func simple(_ message: String = "match", matcher: @escaping (AsyncExpression<T>) async throws -> PredicateStatus) -> AsyncPredicate<T> {
77+
public static func simple(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> PredicateStatus) -> AsyncPredicate<T> {
7878
return AsyncPredicate<T> { actual in
7979
return PredicateResult(status: try await matcher(actual), message: .expectedActualValueTo(message))
8080
}.requireNonNil
@@ -84,7 +84,7 @@ extension AsyncPredicate {
8484
/// error message.
8585
///
8686
/// Unlike `simple`, this allows nil values to succeed if the given closure chooses to.
87-
public static func simpleNilable(_ message: String = "match", matcher: @escaping (AsyncExpression<T>) async throws -> PredicateStatus) -> AsyncPredicate<T> {
87+
public static func simpleNilable(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> PredicateStatus) -> AsyncPredicate<T> {
8888
return AsyncPredicate<T> { actual in
8989
return PredicateResult(status: try await matcher(actual), message: .expectedActualValueTo(message))
9090
}
@@ -93,7 +93,7 @@ extension AsyncPredicate {
9393

9494
extension AsyncPredicate {
9595
// Someday, make this public? Needs documentation
96-
internal func after(f: @escaping (AsyncExpression<T>, PredicateResult) async throws -> PredicateResult) -> AsyncPredicate<T> {
96+
internal func after(f: @escaping @Sendable (AsyncExpression<T>, PredicateResult) async throws -> PredicateResult) -> AsyncPredicate<T> {
9797
// swiftlint:disable:previous identifier_name
9898
return AsyncPredicate { actual -> PredicateResult in
9999
let result = try await self.satisfies(actual)

Sources/Nimble/Matchers/PostNotification.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,39 @@ internal class NotificationCollector {
4343
}
4444
}
4545

46-
private let mainThread = pthread_self()
46+
private final class OnlyOnceChecker: @unchecked Sendable {
47+
var hasRun = false
48+
let lock = NSRecursiveLock()
49+
50+
func runOnlyOnce(_ closure: @Sendable () throws -> Void) rethrows {
51+
lock.lock()
52+
defer {
53+
lock.unlock()
54+
}
55+
if !hasRun {
56+
hasRun = true
57+
try closure()
58+
}
59+
}
60+
}
4761

4862
private func _postNotifications<Out>(
4963
_ predicate: Predicate<[Notification]>,
5064
from center: NotificationCenter,
5165
names: Set<Notification.Name> = []
5266
) -> Predicate<Out> {
53-
_ = mainThread // Force lazy-loading of this value
5467
let collector = NotificationCollector(notificationCenter: center, names: names)
5568
collector.startObserving()
56-
var once: Bool = false
69+
let once = OnlyOnceChecker()
5770

5871
return Predicate { actualExpression in
72+
guard Thread.isMainThread else {
73+
let message = ExpectationMessage
74+
.expectedTo("post notifications - but was called off the main thread.")
75+
.appended(details: "postNotifications and postDistributedNotifications attempted to run their predicate off the main thread. This is a bug in Nimble.")
76+
return PredicateResult(status: .fail, message: message)
77+
}
78+
5979
let collectorNotificationsExpression = Expression(
6080
memoizedExpression: { _ in
6181
return collector.observedNotifications
@@ -65,8 +85,7 @@ private func _postNotifications<Out>(
6585
)
6686

6787
assert(Thread.isMainThread, "Only expecting closure to be evaluated on main thread.")
68-
if !once {
69-
once = true
88+
try once.runOnlyOnce {
7089
_ = try actualExpression.evaluate()
7190
}
7291

Sources/Nimble/Matchers/Predicate.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
/// predicates are simple wrappers around closures to provide static type information and
1818
/// allow composition and wrapping of existing behaviors.
1919
public struct Predicate<T> {
20-
fileprivate var matcher: (Expression<T>) throws -> PredicateResult
20+
fileprivate let matcher: @Sendable (Expression<T>) throws -> PredicateResult
2121

2222
/// Constructs a predicate that knows how take a given value
23-
public init(_ matcher: @escaping (Expression<T>) throws -> PredicateResult) {
23+
public init(_ matcher: @escaping @Sendable (Expression<T>) throws -> PredicateResult) {
2424
self.matcher = matcher
2525
}
2626

@@ -33,26 +33,28 @@ public struct Predicate<T> {
3333
}
3434
}
3535

36+
extension Predicate: Sendable where T: Sendable {}
37+
3638
/// Provides convenience helpers to defining predicates
3739
extension Predicate {
3840
/// Like Predicate() constructor, but automatically guard against nil (actual) values
39-
public static func define(matcher: @escaping (Expression<T>) throws -> PredicateResult) -> Predicate<T> {
41+
public static func define(matcher: @escaping @Sendable (Expression<T>) throws -> PredicateResult) -> Predicate<T> {
4042
return Predicate<T> { actual in
4143
return try matcher(actual)
4244
}.requireNonNil
4345
}
4446

4547
/// Defines a predicate with a default message that can be returned in the closure
4648
/// Also ensures the predicate's actual value cannot pass with `nil` given.
47-
public static func define(_ message: String = "match", matcher: @escaping (Expression<T>, ExpectationMessage) throws -> PredicateResult) -> Predicate<T> {
49+
public static func define(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>, ExpectationMessage) throws -> PredicateResult) -> Predicate<T> {
4850
return Predicate<T> { actual in
4951
return try matcher(actual, .expectedActualValueTo(message))
5052
}.requireNonNil
5153
}
5254

5355
/// Defines a predicate with a default message that can be returned in the closure
5456
/// Unlike `define`, this allows nil values to succeed if the given closure chooses to.
55-
public static func defineNilable(_ message: String = "match", matcher: @escaping (Expression<T>, ExpectationMessage) throws -> PredicateResult) -> Predicate<T> {
57+
public static func defineNilable(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>, ExpectationMessage) throws -> PredicateResult) -> Predicate<T> {
5658
return Predicate<T> { actual in
5759
return try matcher(actual, .expectedActualValueTo(message))
5860
}
@@ -64,7 +66,7 @@ extension Predicate {
6466
/// error message.
6567
///
6668
/// Also ensures the predicate's actual value cannot pass with `nil` given.
67-
public static func simple(_ message: String = "match", matcher: @escaping (Expression<T>) throws -> PredicateStatus) -> Predicate<T> {
69+
public static func simple(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>) throws -> PredicateStatus) -> Predicate<T> {
6870
return Predicate<T> { actual in
6971
return PredicateResult(status: try matcher(actual), message: .expectedActualValueTo(message))
7072
}.requireNonNil
@@ -74,21 +76,21 @@ extension Predicate {
7476
/// error message.
7577
///
7678
/// Unlike `simple`, this allows nil values to succeed if the given closure chooses to.
77-
public static func simpleNilable(_ message: String = "match", matcher: @escaping (Expression<T>) throws -> PredicateStatus) -> Predicate<T> {
79+
public static func simpleNilable(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>) throws -> PredicateStatus) -> Predicate<T> {
7880
return Predicate<T> { actual in
7981
return PredicateResult(status: try matcher(actual), message: .expectedActualValueTo(message))
8082
}
8183
}
8284
}
8385

8486
// The Expectation style intended for comparison to a PredicateStatus.
85-
public enum ExpectationStyle {
87+
public enum ExpectationStyle: Sendable {
8688
case toMatch, toNotMatch
8789
}
8890

8991
/// The value that a Predicates return to describe if the given (actual) value matches the
9092
/// predicate.
91-
public struct PredicateResult {
93+
public struct PredicateResult: Sendable {
9294
/// Status indicates if the predicate matches, does not match, or fails.
9395
public var status: PredicateStatus
9496
/// The error message that can be displayed if it does not match
@@ -113,7 +115,7 @@ public struct PredicateResult {
113115
}
114116

115117
/// PredicateStatus is a trinary that indicates if a Predicate matches a given value or not
116-
public enum PredicateStatus {
118+
public enum PredicateStatus: Sendable {
117119
/// Matches indicates if the predicate / matcher passes with the given value
118120
///
119121
/// For example, `equals(1)` returns `.matches` for `expect(1).to(equal(1))`.
@@ -167,7 +169,7 @@ public enum PredicateStatus {
167169

168170
extension Predicate {
169171
// Someday, make this public? Needs documentation
170-
internal func after(f: @escaping (Expression<T>, PredicateResult) throws -> PredicateResult) -> Predicate<T> {
172+
internal func after(f: @escaping @Sendable (Expression<T>, PredicateResult) throws -> PredicateResult) -> Predicate<T> {
171173
// swiftlint:disable:previous identifier_name
172174
return Predicate { actual -> PredicateResult in
173175
let result = try self.satisfies(actual)
@@ -193,7 +195,7 @@ extension Predicate {
193195
#if canImport(Darwin)
194196
import class Foundation.NSObject
195197

196-
public typealias PredicateBlock = (_ actualExpression: Expression<NSObject>) throws -> NMBPredicateResult
198+
public typealias PredicateBlock = @Sendable (_ actualExpression: Expression<NSObject>) throws -> NMBPredicateResult
197199

198200
public class NMBPredicate: NSObject {
199201
private let predicate: PredicateBlock
@@ -202,7 +204,7 @@ public class NMBPredicate: NSObject {
202204
self.predicate = predicate
203205
}
204206

205-
func satisfies(_ expression: @escaping () throws -> NSObject?, location: SourceLocation) -> NMBPredicateResult {
207+
func satisfies(_ expression: @escaping @Sendable() throws -> NSObject?, location: SourceLocation) -> NMBPredicateResult {
206208
let expr = Expression(expression: expression, location: location)
207209
do {
208210
return try self.predicate(expr)
@@ -238,7 +240,7 @@ extension PredicateResult {
238240
}
239241
}
240242

241-
final public class NMBPredicateStatus: NSObject {
243+
final public class NMBPredicateStatus: NSObject, Sendable {
242244
private let status: Int
243245
private init(status: Int) {
244246
self.status = status

Tests/NimbleTests/SynchronousTest.swift

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,33 @@ final class SynchronousTest: XCTestCase {
3737
}
3838

3939
func testToProvidesActualValueExpression() {
40-
var value: Int?
41-
expect(1).to(Predicate.simple { expr in value = try expr.evaluate(); return .matches })
42-
expect(value).to(equal(1))
40+
let recorder = Recorder<Int?>()
41+
expect(1).to(Predicate.simple { expr in recorder.record(try expr.evaluate()); return .matches })
42+
expect(recorder.records).to(equal([1]))
4343
}
4444

4545
func testToProvidesAMemoizedActualValueExpression() {
46-
var callCount = 0
47-
expect { callCount += 1 }.to(Predicate.simple { expr in
46+
let recorder = Recorder<Void>()
47+
expect {
48+
recorder.record(())
49+
}.to(Predicate.simple { expr in
4850
_ = try expr.evaluate()
4951
_ = try expr.evaluate()
5052
return .matches
5153
})
52-
expect(callCount).to(equal(1))
54+
expect(recorder.records).to(haveCount(1))
5355
}
5456

5557
func testToProvidesAMemoizedActualValueExpressionIsEvaluatedAtMatcherControl() {
56-
var callCount = 0
57-
expect { callCount += 1 }.to(Predicate.simple { expr in
58-
expect(callCount).to(equal(0))
58+
let recorder = Recorder<Void>()
59+
expect {
60+
recorder.record(())
61+
}.to(Predicate.simple { expr in
62+
expect(recorder.records).to(beEmpty())
5963
_ = try expr.evaluate()
6064
return .matches
6165
})
62-
expect(callCount).to(equal(1))
66+
expect(recorder.records).to(haveCount(1))
6367
}
6468

6569
func testToMatchAgainstLazyProperties() {
@@ -76,29 +80,29 @@ final class SynchronousTest: XCTestCase {
7680
}
7781

7882
func testToNotProvidesActualValueExpression() {
79-
var value: Int?
80-
expect(1).toNot(Predicate.simple { expr in value = try expr.evaluate(); return .doesNotMatch })
81-
expect(value).to(equal(1))
83+
let recorder = Recorder<Int?>()
84+
expect(1).toNot(Predicate.simple { expr in recorder.record(try expr.evaluate()); return .doesNotMatch })
85+
expect(recorder.records).to(equal([1]))
8286
}
8387

8488
func testToNotProvidesAMemoizedActualValueExpression() {
85-
var callCount = 0
86-
expect { callCount += 1 }.toNot(Predicate.simple { expr in
89+
let recorder = Recorder<Void>()
90+
expect { recorder.record(()) }.toNot(Predicate.simple { expr in
8791
_ = try expr.evaluate()
8892
_ = try expr.evaluate()
8993
return .doesNotMatch
9094
})
91-
expect(callCount).to(equal(1))
95+
expect(recorder.records).to(haveCount(1))
9296
}
9397

9498
func testToNotProvidesAMemoizedActualValueExpressionIsEvaluatedAtMatcherControl() {
95-
var callCount = 0
96-
expect { callCount += 1 }.toNot(Predicate.simple { expr in
97-
expect(callCount).to(equal(0))
99+
let recorder = Recorder<Void>()
100+
expect { recorder.record(()) }.toNot(Predicate.simple { expr in
101+
expect(recorder.records).to(beEmpty())
98102
_ = try expr.evaluate()
99103
return .doesNotMatch
100104
})
101-
expect(callCount).to(equal(1))
105+
expect(recorder.records).to(haveCount(1))
102106
}
103107

104108
func testToNegativeMatches() {
@@ -129,3 +133,24 @@ final class SynchronousTest: XCTestCase {
129133
}
130134
}
131135
}
136+
137+
private final class Recorder<T: Sendable>: @unchecked Sendable {
138+
private var _records: [T] = []
139+
private let lock = NSRecursiveLock()
140+
141+
var records: [T] {
142+
get {
143+
lock.lock()
144+
defer {
145+
lock.unlock()
146+
}
147+
return _records
148+
}
149+
}
150+
151+
func record(_ value: T) {
152+
lock.lock()
153+
self._records.append(value)
154+
lock.unlock()
155+
}
156+
}

0 commit comments

Comments
 (0)