Skip to content

Commit

Permalink
The async variants of expect now require Sendable values (#1071)
Browse files Browse the repository at this point in the history
  • Loading branch information
younata committed Mar 17, 2024
1 parent 53de1eb commit af25cde
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 24 deletions.
6 changes: 3 additions & 3 deletions Sources/Nimble/AsyncExpression.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
private actor MemoizedClosure<T> {
private actor MemoizedClosure<T: Sendable> {
var closure: @Sendable () async throws -> T
var cache: T?

Expand All @@ -25,7 +25,7 @@ private actor MemoizedClosure<T> {

// Memoizes the given closure, only calling the passed
// closure once; even if repeat calls to the returned closure
private func memoizedClosure<T>(_ closure: @escaping @Sendable () async throws -> T) -> @Sendable (Bool) async throws -> T {
private func memoizedClosure<T: Sendable>(_ closure: @escaping @Sendable () async throws -> T) -> @Sendable (Bool) async throws -> T {
let memoized = MemoizedClosure(closure)
return { withoutCaching in
try await memoized.call(withoutCaching)
Expand All @@ -43,7 +43,7 @@ private func memoizedClosure<T>(_ 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<Value>: Sendable {
public struct AsyncExpression<Value: Sendable>: Sendable {
internal let _expression: @Sendable (Bool) async throws -> Value?
internal let _withoutCaching: Bool
public let location: SourceLocation
Expand Down
12 changes: 6 additions & 6 deletions Sources/Nimble/DSL+AsyncAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Dispatch
#endif

/// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated.
public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation<T> {
public func expect<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression,
Expand All @@ -12,7 +12,7 @@ public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression
}

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation<T> {
public func expect<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -21,7 +21,7 @@ public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression
}

/// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked.
public func expect<T>(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation<T> {
public func expect<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -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<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation<T> {
public func expecta<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression,
Expand All @@ -50,7 +50,7 @@ public func expecta<T>(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<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation<T> {
public func expecta<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand All @@ -60,7 +60,7 @@ public func expecta<T>(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<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation<T> {
public func expecta<T: Sendable>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation<T> {
return AsyncExpectation(
expression: AsyncExpression(
expression: expression(),
Expand Down
8 changes: 5 additions & 3 deletions Sources/Nimble/Expectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal func execute<T>(_ expression: AsyncExpression<T>, _ style: ExpectationS
}
}

public enum ExpectationStatus: Equatable {
public enum ExpectationStatus: Equatable, Sendable {

/// No matchers have been performed.
case pending
Expand Down Expand Up @@ -214,8 +214,10 @@ public struct SyncExpectation<Value>: Expectation {
public func notTo(_ matcher: Matcher<Value>, 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<Value>, description: String? = nil) async -> Self {
Expand Down Expand Up @@ -243,7 +245,7 @@ public struct SyncExpectation<Value>: Expectation {
// - NMBExpectation for Objective-C interface
}

public struct AsyncExpectation<Value>: Expectation {
public struct AsyncExpectation<Value: Sendable>: Expectation, Sendable {
public let expression: AsyncExpression<Value>

/// The status of the test after matchers have been evaluated.
Expand Down
24 changes: 12 additions & 12 deletions Sources/Nimble/Matchers/AsyncMatcher.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
public protocol AsyncableMatcher<Value> {
associatedtype Value
public protocol AsyncableMatcher<Value>: Sendable {
associatedtype Value: Sendable
func satisfies(_ expression: AsyncExpression<Value>) async throws -> MatcherResult
}

extension Matcher: AsyncableMatcher {
extension Matcher: AsyncableMatcher where T: Sendable {
public func satisfies(_ expression: AsyncExpression<T>) async throws -> MatcherResult {
try satisfies(await expression.toSynchronousExpression())
}
Expand All @@ -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<T>: AsyncableMatcher {
fileprivate var matcher: (AsyncExpression<T>) async throws -> MatcherResult
public struct AsyncMatcher<T: Sendable>: AsyncableMatcher, Sendable {
fileprivate var matcher: @Sendable (AsyncExpression<T>) async throws -> MatcherResult

public init(_ matcher: @escaping (AsyncExpression<T>) async throws -> MatcherResult) {
public init(_ matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> MatcherResult) {
self.matcher = matcher
}

Expand All @@ -49,23 +49,23 @@ 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<T>) async throws -> MatcherResult) -> AsyncMatcher<T> {
public static func define(matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> MatcherResult) -> AsyncMatcher<T> {
return AsyncMatcher<T> { actual in
return try await matcher(actual)
}.requireNonNil
}

/// 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<T>, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher<T> {
public static func define(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher<T> {
return AsyncMatcher<T> { actual in
return try await matcher(actual, .expectedActualValueTo(message))
}.requireNonNil
}

/// 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<T>, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher<T> {
public static func defineNilable(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>, ExpectationMessage) async throws -> MatcherResult) -> AsyncMatcher<T> {
return AsyncMatcher<T> { actual in
return try await matcher(actual, .expectedActualValueTo(message))
}
Expand All @@ -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<T>) async throws -> MatcherStatus) -> AsyncMatcher<T> {
public static func simple(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> MatcherStatus) -> AsyncMatcher<T> {
return AsyncMatcher<T> { actual in
return MatcherResult(status: try await matcher(actual), message: .expectedActualValueTo(message))
}.requireNonNil
Expand All @@ -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<T>) async throws -> MatcherStatus) -> AsyncMatcher<T> {
public static func simpleNilable(_ message: String = "match", matcher: @escaping @Sendable (AsyncExpression<T>) async throws -> MatcherStatus) -> AsyncMatcher<T> {
return AsyncMatcher<T> { actual in
return MatcherResult(status: try await matcher(actual), message: .expectedActualValueTo(message))
}
Expand All @@ -94,7 +94,7 @@ extension AsyncMatcher {

extension AsyncMatcher {
// Someday, make this public? Needs documentation
internal func after(f: @escaping (AsyncExpression<T>, MatcherResult) async throws -> MatcherResult) -> AsyncMatcher<T> {
internal func after(f: @escaping @Sendable (AsyncExpression<T>, MatcherResult) async throws -> MatcherResult) -> AsyncMatcher<T> {
// swiftlint:disable:previous identifier_name
return AsyncMatcher { actual -> MatcherResult in
let result = try await self.satisfies(actual)
Expand Down

0 comments on commit af25cde

Please sign in to comment.