Skip to content

Commit 2575114

Browse files
committed
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 af25cde commit 2575114

File tree

5 files changed

+88
-39
lines changed

5 files changed

+88
-39
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/AsyncMatcher.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ public protocol AsyncableMatcher<Value>: Sendable {
33
func satisfies(_ expression: AsyncExpression<Value>) async throws -> MatcherResult
44
}
55

6-
extension Matcher: AsyncableMatcher where T: Sendable {
6+
extension Matcher: AsyncableMatcher {
77
public func satisfies(_ expression: AsyncExpression<T>) async throws -> MatcherResult {
88
try satisfies(await expression.toSynchronousExpression())
99
}

Sources/Nimble/Matchers/Matcher.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
/// renamed `NSMatcher` to `Matcher`. In response, we decided to rename `Matcher` to
2020
/// `Matcher`.
2121
public struct Matcher<T> {
22-
fileprivate var matcher: (Expression<T>) throws -> MatcherResult
22+
fileprivate let matcher: @Sendable (Expression<T>) throws -> MatcherResult
2323

2424
/// Constructs a matcher that knows how take a given value
25-
public init(_ matcher: @escaping (Expression<T>) throws -> MatcherResult) {
25+
public init(_ matcher: @escaping @Sendable (Expression<T>) throws -> MatcherResult) {
2626
self.matcher = matcher
2727
}
2828

@@ -39,26 +39,28 @@ public struct Matcher<T> {
3939
@available(*, deprecated, renamed: "Matcher")
4040
public typealias Predicate = Matcher
4141

42+
extension Matcher: Sendable where T: Sendable {}
43+
4244
/// Provides convenience helpers to defining matchers
4345
extension Matcher {
4446
/// Like Matcher() constructor, but automatically guard against nil (actual) values
45-
public static func define(matcher: @escaping (Expression<T>) throws -> MatcherResult) -> Matcher<T> {
47+
public static func define(matcher: @escaping @Sendable (Expression<T>) throws -> MatcherResult) -> Matcher<T> {
4648
return Matcher<T> { actual in
4749
return try matcher(actual)
4850
}.requireNonNil
4951
}
5052

5153
/// Defines a matcher with a default message that can be returned in the closure
5254
/// Also ensures the matcher's actual value cannot pass with `nil` given.
53-
public static func define(_ message: String = "match", matcher: @escaping (Expression<T>, ExpectationMessage) throws -> MatcherResult) -> Matcher<T> {
55+
public static func define(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>, ExpectationMessage) throws -> MatcherResult) -> Matcher<T> {
5456
return Matcher<T> { actual in
5557
return try matcher(actual, .expectedActualValueTo(message))
5658
}.requireNonNil
5759
}
5860

5961
/// Defines a matcher with a default message that can be returned in the closure
6062
/// Unlike `define`, this allows nil values to succeed if the given closure chooses to.
61-
public static func defineNilable(_ message: String = "match", matcher: @escaping (Expression<T>, ExpectationMessage) throws -> MatcherResult) -> Matcher<T> {
63+
public static func defineNilable(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>, ExpectationMessage) throws -> MatcherResult) -> Matcher<T> {
6264
return Matcher<T> { actual in
6365
return try matcher(actual, .expectedActualValueTo(message))
6466
}
@@ -70,7 +72,7 @@ extension Matcher {
7072
/// error message.
7173
///
7274
/// Also ensures the matcher's actual value cannot pass with `nil` given.
73-
public static func simple(_ message: String = "match", matcher: @escaping (Expression<T>) throws -> MatcherStatus) -> Matcher<T> {
75+
public static func simple(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>) throws -> MatcherStatus) -> Matcher<T> {
7476
return Matcher<T> { actual in
7577
return MatcherResult(status: try matcher(actual), message: .expectedActualValueTo(message))
7678
}.requireNonNil
@@ -80,15 +82,15 @@ extension Matcher {
8082
/// error message.
8183
///
8284
/// Unlike `simple`, this allows nil values to succeed if the given closure chooses to.
83-
public static func simpleNilable(_ message: String = "match", matcher: @escaping (Expression<T>) throws -> MatcherStatus) -> Matcher<T> {
85+
public static func simpleNilable(_ message: String = "match", matcher: @escaping @Sendable (Expression<T>) throws -> MatcherStatus) -> Matcher<T> {
8486
return Matcher<T> { actual in
8587
return MatcherResult(status: try matcher(actual), message: .expectedActualValueTo(message))
8688
}
8789
}
8890
}
8991

9092
/// The Expectation style intended for comparison to a MatcherStatus.
91-
public enum ExpectationStyle {
93+
public enum ExpectationStyle: Sendable {
9294
case toMatch, toNotMatch
9395
}
9496

@@ -123,7 +125,7 @@ public struct MatcherResult {
123125
public typealias PredicateResult = MatcherResult
124126

125127
/// MatcherStatus is a trinary that indicates if a Matcher matches a given value or not
126-
public enum MatcherStatus {
128+
public enum MatcherStatus: Sendable {
127129
/// Matches indicates if the matcher / matcher passes with the given value
128130
///
129131
/// For example, `equals(1)` returns `.matches` for `expect(1).to(equal(1))`.
@@ -181,7 +183,7 @@ public typealias PredicateStatus = MatcherStatus
181183

182184
extension Matcher {
183185
// Someday, make this public? Needs documentation
184-
internal func after(f: @escaping (Expression<T>, MatcherResult) throws -> MatcherResult) -> Matcher<T> {
186+
internal func after(f: @escaping @Sendable (Expression<T>, MatcherResult) throws -> MatcherResult) -> Matcher<T> {
185187
// swiftlint:disable:previous identifier_name
186188
return Matcher { actual -> MatcherResult in
187189
let result = try self.satisfies(actual)
@@ -207,7 +209,7 @@ extension Matcher {
207209
#if canImport(Darwin)
208210
import class Foundation.NSObject
209211

210-
public typealias MatcherBlock = (_ actualExpression: Expression<NSObject>) throws -> NMBMatcherResult
212+
public typealias MatcherBlock = @Sendable (_ actualExpression: Expression<NSObject>) throws -> NMBMatcherResult
211213

212214
/// Provides an easy upgrade path for custom Matchers to be renamed to Matchers
213215
@available(*, deprecated, renamed: "MatcherBlock")
@@ -225,7 +227,7 @@ public class NMBMatcher: NSObject {
225227
self.init(matcher: predicate)
226228
}
227229

228-
func satisfies(_ expression: @escaping () throws -> NSObject?, location: SourceLocation) -> NMBMatcherResult {
230+
func satisfies(_ expression: @escaping @Sendable () throws -> NSObject?, location: SourceLocation) -> NMBMatcherResult {
229231
let expr = Expression(expression: expression, location: location)
230232
do {
231233
return try self.matcher(expr)
@@ -269,7 +271,7 @@ extension MatcherResult {
269271
}
270272
}
271273

272-
final public class NMBMatcherStatus: NSObject {
274+
final public class NMBMatcherStatus: NSObject, Sendable {
273275
private let status: Int
274276
private init(status: Int) {
275277
self.status = status

Sources/Nimble/Matchers/PostNotification.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ private let mainThread = pthread_self()
4949
private let mainThread = Thread.mainThread
5050
#endif
5151

52+
private final class OnlyOnceChecker: @unchecked Sendable {
53+
var hasRun = false
54+
let lock = NSRecursiveLock()
55+
56+
func runOnlyOnce(_ closure: @Sendable () throws -> Void) rethrows {
57+
lock.lock()
58+
defer {
59+
lock.unlock()
60+
}
61+
if !hasRun {
62+
hasRun = true
63+
try closure()
64+
}
65+
}
66+
}
67+
5268
private func _postNotifications<Out>(
5369
_ matcher: Matcher<[Notification]>,
5470
from center: NotificationCenter,
@@ -57,9 +73,16 @@ private func _postNotifications<Out>(
5773
_ = mainThread // Force lazy-loading of this value
5874
let collector = NotificationCollector(notificationCenter: center, names: names)
5975
collector.startObserving()
60-
var once: Bool = false
76+
let once = OnlyOnceChecker()
6177

6278
return Matcher { actualExpression in
79+
guard Thread.isMainThread else {
80+
let message = ExpectationMessage
81+
.expectedTo("post notifications - but was called off the main thread.")
82+
.appended(details: "postNotifications and postDistributedNotifications attempted to run their predicate off the main thread. This is a bug in Nimble.")
83+
return PredicateResult(status: .fail, message: message)
84+
}
85+
6386
let collectorNotificationsExpression = Expression(
6487
memoizedExpression: { _ in
6588
return collector.observedNotifications
@@ -69,8 +92,7 @@ private func _postNotifications<Out>(
6992
)
7093

7194
assert(Thread.isMainThread, "Only expecting closure to be evaluated on main thread.")
72-
if !once {
73-
once = true
95+
try once.runOnlyOnce {
7496
_ = try actualExpression.evaluate()
7597
}
7698

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(Matcher.simple { expr in value = try expr.evaluate(); return .matches })
42-
expect(value).to(equal(1))
40+
let recorder = Recorder<Int?>()
41+
expect(1).to(Matcher.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(Matcher.simple { expr in
46+
let recorder = Recorder<Void>()
47+
expect {
48+
recorder.record(())
49+
}.to(Matcher.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(Matcher.simple { expr in
58-
expect(callCount).to(equal(0))
58+
let recorder = Recorder<Void>()
59+
expect {
60+
recorder.record(())
61+
}.to(Matcher.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(Matcher.simple { expr in value = try expr.evaluate(); return .doesNotMatch })
81-
expect(value).to(equal(1))
83+
let recorder = Recorder<Int?>()
84+
expect(1).toNot(Matcher.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(Matcher.simple { expr in
89+
let recorder = Recorder<Void>()
90+
expect { recorder.record(()) }.toNot(Matcher.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(Matcher.simple { expr in
97-
expect(callCount).to(equal(0))
99+
let recorder = Recorder<Void>()
100+
expect { recorder.record(()) }.toNot(Matcher.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)