diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index b897d7c5..887226ab 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -1,4 +1,4 @@ -private actor MemoizedClosure { +private actor MemoizedClosure { var closure: @Sendable () async throws -> T var cache: T? @@ -25,7 +25,7 @@ private actor MemoizedClosure { // Memoizes the given closure, only calling the passed // closure once; even if repeat calls to the returned closure -private func memoizedClosure(_ closure: @escaping @Sendable () async throws -> T) -> @Sendable (Bool) async throws -> T { +private func memoizedClosure(_ closure: @escaping @Sendable () async throws -> T) -> @Sendable (Bool) async throws -> T { let memoized = MemoizedClosure(closure) return { withoutCaching in try await memoized.call(withoutCaching) @@ -43,7 +43,7 @@ private func memoizedClosure(_ closure: @escaping @Sendable () 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: Sendable { +public struct AsyncExpression: Sendable { internal let _expression: @Sendable (Bool) async throws -> Value? internal let _withoutCaching: Bool public let location: SourceLocation diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index 68eda1e1..8fbd305a 100644 --- a/Sources/Nimble/DSL+AsyncAwait.swift +++ b/Sources/Nimble/DSL+AsyncAwait.swift @@ -3,7 +3,7 @@ import Dispatch #endif /// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { +public func expect(file: FileString = #file, line: UInt = #line, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -12,7 +12,7 @@ public func expect(file: FileString = #file, line: UInt = #line, _ expression } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { +public func expect(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -21,7 +21,7 @@ public func expect(file: FileString = #file, line: UInt = #line, _ expression } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { +public func expect(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -40,7 +40,7 @@ public func expect(file: FileString = #file, line: UInt = #line, _ expression: @ /// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation`. -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { +public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -50,7 +50,7 @@ public func expecta(file: FileString = #file, line: UInt = #line, _ expressio /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation` -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { +public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -60,7 +60,7 @@ public func expecta(file: FileString = #file, line: UInt = #line, _ expressio /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation` -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation { +public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), diff --git a/Sources/Nimble/Expectation.swift b/Sources/Nimble/Expectation.swift index 732e0c57..7363483b 100644 --- a/Sources/Nimble/Expectation.swift +++ b/Sources/Nimble/Expectation.swift @@ -50,7 +50,7 @@ internal func execute(_ expression: AsyncExpression, _ style: ExpectationS } } -public enum ExpectationStatus: Equatable { +public enum ExpectationStatus: Equatable, Sendable { /// No matchers have been performed. case pending @@ -214,8 +214,10 @@ public struct SyncExpectation: Expectation { public func notTo(_ matcher: Matcher, description: String? = nil) -> Self { toNot(matcher, description: description) } +} - // MARK: - AsyncMatchers +extension SyncExpectation where Value: Sendable { + // MARK: - AsyncPredicates /// Tests the actual value using a matcher to match. @discardableResult public func to(_ matcher: AsyncMatcher, description: String? = nil) async -> Self { @@ -243,7 +245,7 @@ public struct SyncExpectation: Expectation { // - NMBExpectation for Objective-C interface } -public struct AsyncExpectation: Expectation { +public struct AsyncExpectation: Expectation, Sendable { public let expression: AsyncExpression /// The status of the test after matchers have been evaluated. diff --git a/Sources/Nimble/Matchers/AsyncMatcher.swift b/Sources/Nimble/Matchers/AsyncMatcher.swift index 96b118a9..6b800029 100644 --- a/Sources/Nimble/Matchers/AsyncMatcher.swift +++ b/Sources/Nimble/Matchers/AsyncMatcher.swift @@ -1,9 +1,9 @@ -public protocol AsyncableMatcher { - associatedtype Value +public protocol AsyncableMatcher: Sendable { + associatedtype Value: Sendable func satisfies(_ expression: AsyncExpression) async throws -> MatcherResult } -extension Matcher: AsyncableMatcher { +extension Matcher: AsyncableMatcher where T: Sendable { public func satisfies(_ expression: AsyncExpression) async throws -> MatcherResult { try satisfies(await expression.toSynchronousExpression()) } @@ -27,10 +27,10 @@ extension Matcher: AsyncableMatcher { /// These can also be used with either `Expectation`s or `AsyncExpectation`s. /// But these can only be used from async contexts, and are unavailable in Objective-C. /// You can, however, call regular Matchers from an AsyncMatcher, if you wish to compose one like that. -public struct AsyncMatcher: AsyncableMatcher { - fileprivate var matcher: (AsyncExpression) async throws -> MatcherResult +public struct AsyncMatcher: AsyncableMatcher, Sendable { + fileprivate var matcher: @Sendable (AsyncExpression) async throws -> MatcherResult - public init(_ matcher: @escaping (AsyncExpression) async throws -> MatcherResult) { + public init(_ matcher: @escaping @Sendable (AsyncExpression) async throws -> MatcherResult) { self.matcher = matcher } @@ -49,7 +49,7 @@ public typealias AsyncPredicate = AsyncMatcher /// Provides convenience helpers to defining matchers extension AsyncMatcher { /// Like Matcher() constructor, but automatically guard against nil (actual) values - public static func define(matcher: @escaping (AsyncExpression) async throws -> MatcherResult) -> AsyncMatcher { + public static func define(matcher: @escaping @Sendable (AsyncExpression) async throws -> MatcherResult) -> AsyncMatcher { return AsyncMatcher { actual in return try await matcher(actual) }.requireNonNil @@ -57,7 +57,7 @@ extension AsyncMatcher { /// Defines a matcher with a default message that can be returned in the closure /// Also ensures the matcher's actual value cannot pass with `nil` given. - public static func define(_ message: String = "match", matcher: @escaping (AsyncExpression, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher { + public static func define(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher { return AsyncMatcher { actual in return try await matcher(actual, .expectedActualValueTo(message)) }.requireNonNil @@ -65,7 +65,7 @@ extension AsyncMatcher { /// Defines a matcher with a default message that can be returned in the closure /// Unlike `define`, this allows nil values to succeed if the given closure chooses to. - public static func defineNilable(_ message: String = "match", matcher: @escaping (AsyncExpression, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher { + public static func defineNilable(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher { return AsyncMatcher { actual in return try await matcher(actual, .expectedActualValueTo(message)) } @@ -75,7 +75,7 @@ extension AsyncMatcher { /// error message. /// /// Also ensures the matcher's actual value cannot pass with `nil` given. - public static func simple(_ message: String = "match", matcher: @escaping (AsyncExpression) async throws -> MatcherStatus) -> AsyncMatcher { + public static func simple(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression) async throws -> MatcherStatus) -> AsyncMatcher { return AsyncMatcher { actual in return MatcherResult(status: try await matcher(actual), message: .expectedActualValueTo(message)) }.requireNonNil @@ -85,7 +85,7 @@ extension AsyncMatcher { /// error message. /// /// Unlike `simple`, this allows nil values to succeed if the given closure chooses to. - public static func simpleNilable(_ message: String = "match", matcher: @escaping (AsyncExpression) async throws -> MatcherStatus) -> AsyncMatcher { + public static func simpleNilable(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression) async throws -> MatcherStatus) -> AsyncMatcher { return AsyncMatcher { actual in return MatcherResult(status: try await matcher(actual), message: .expectedActualValueTo(message)) } @@ -94,7 +94,7 @@ extension AsyncMatcher { extension AsyncMatcher { // Someday, make this public? Needs documentation - internal func after(f: @escaping (AsyncExpression, MatcherResult) async throws -> MatcherResult) -> AsyncMatcher { + internal func after(f: @escaping @Sendable (AsyncExpression, MatcherResult) async throws -> MatcherResult) -> AsyncMatcher { // swiftlint:disable:previous identifier_name return AsyncMatcher { actual -> MatcherResult in let result = try await self.satisfies(actual)