From 47e7f753d2911eff71a841afa3857293b5f0bb6a Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 28 Jul 2023 22:15:57 -0700 Subject: [PATCH 01/25] Make AsyncExpression conform to Sendable (#1067) --- Sources/Nimble/AsyncExpression.swift | 46 ++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index b669d5a0..b897d7c5 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -1,12 +1,34 @@ +private actor MemoizedClosure { + var closure: @Sendable () async throws -> T + var cache: T? + + init(_ closure: @escaping @Sendable () async throws -> T) { + self.closure = closure + } + + func set(_ cache: T) -> T { + self.cache = cache + return cache + } + + func call(_ withoutCaching: Bool) async throws -> T { + if withoutCaching { + return try await closure() + } + if let cache { + return cache + } else { + return set(try await closure()) + } + } +} + // Memoizes the given closure, only calling the passed // closure once; even if repeat calls to the returned closure -private func memoizedClosure(_ closure: @escaping () async throws -> T) -> (Bool) async throws -> T { - var cache: T? +private func memoizedClosure(_ closure: @escaping @Sendable () async throws -> T) -> @Sendable (Bool) async throws -> T { + let memoized = MemoizedClosure(closure) return { withoutCaching in - if withoutCaching || cache == nil { - cache = try await closure() - } - return cache! + try await memoized.call(withoutCaching) } } @@ -21,8 +43,8 @@ private func memoizedClosure(_ closure: @escaping () async throws -> T) -> (B /// /// 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 { - internal let _expression: (Bool) async throws -> Value? +public struct AsyncExpression: Sendable { + internal let _expression: @Sendable (Bool) async throws -> Value? internal let _withoutCaching: Bool public let location: SourceLocation public let isClosure: Bool @@ -38,7 +60,7 @@ public struct AsyncExpression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: @escaping () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: @escaping @Sendable () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -59,7 +81,7 @@ public struct AsyncExpression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(memoizedExpression: @escaping (Bool) async throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { + public init(memoizedExpression: @escaping @Sendable (Bool) async throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { self._expression = memoizedExpression self.location = location self._withoutCaching = withoutCaching @@ -90,7 +112,7 @@ public struct AsyncExpression { /// /// - Parameter block: The block that can cast the current Expression value to a /// new type. - public func cast(_ block: @escaping (Value?) throws -> U?) -> AsyncExpression { + public func cast(_ block: @escaping @Sendable (Value?) throws -> U?) -> AsyncExpression { AsyncExpression( expression: ({ try await block(self.evaluate()) }), location: self.location, @@ -98,7 +120,7 @@ public struct AsyncExpression { ) } - public func cast(_ block: @escaping (Value?) async throws -> U?) -> AsyncExpression { + public func cast(_ block: @escaping @Sendable (Value?) async throws -> U?) -> AsyncExpression { AsyncExpression( expression: ({ try await block(self.evaluate()) }), location: self.location, From 7c66a9f92b0e8bc4ea6cd5d99952f6b7379545e7 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sat, 29 Jul 2023 07:46:43 -0700 Subject: [PATCH 02/25] Make expect for async closures take in Sendable closures (#1070) --- Sources/Nimble/DSL+AsyncAwait.swift | 16 ++++++++-------- Tests/NimbleTests/AsyncAwaitTest.swift | 6 +++--- Tests/NimbleTests/Matchers/EqualTest.swift | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index 522e4af5..59ea7b1d 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping () async throws -> T?) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -12,7 +12,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: () -> (() async throws -> T)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -21,7 +21,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: () -> (() async throws -> T?)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -30,7 +30,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: () -> (() async throws -> Void)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> Void)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -40,7 +40,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -50,7 +50,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #filePath, l /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -60,7 +60,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #filePath, l /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -70,7 +70,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #filePath, l /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() async throws -> Void)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> Void)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), diff --git a/Tests/NimbleTests/AsyncAwaitTest.swift b/Tests/NimbleTests/AsyncAwaitTest.swift index d88d4b9a..99cd4372 100644 --- a/Tests/NimbleTests/AsyncAwaitTest.swift +++ b/Tests/NimbleTests/AsyncAwaitTest.swift @@ -8,7 +8,7 @@ import NimbleSharedTestHelpers final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_length func testToPositiveMatches() async { - func someAsyncFunction() async throws -> Int { + @Sendable func someAsyncFunction() async throws -> Int { try await Task.sleep(nanoseconds: 1_000_000) // 1 millisecond return 1 } @@ -119,7 +119,7 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len func testToEventuallyWithAsyncExpectationDoesNotNecessarilyExecutesExpressionOnMainActor() async { // This prevents a "Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in Swift 6" warning. // However, the functionality actually works as you'd expect it to, you're just expected to tag things to use the main actor. - func isMainThread() -> Bool { Thread.isMainThread } + @Sendable func isMainThread() -> Bool { Thread.isMainThread } await expecta(isMainThread()).toEventually(beFalse()) await expecta(isMainThread()).toEventuallyNot(beTrue()) @@ -131,7 +131,7 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len func testToEventuallyWithAsyncExpectationDoesExecuteExpressionOnMainActorWhenTestRunsOnMainActor() async { // This prevents a "Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in Swift 6" warning. // However, the functionality actually works as you'd expect it to, you're just expected to tag things to use the main actor. - func isMainThread() -> Bool { Thread.isMainThread } + @Sendable func isMainThread() -> Bool { Thread.isMainThread } await expecta(isMainThread()).toEventually(beTrue()) await expecta(isMainThread()).toEventuallyNot(beFalse()) diff --git a/Tests/NimbleTests/Matchers/EqualTest.swift b/Tests/NimbleTests/Matchers/EqualTest.swift index 504a8282..c0ccf764 100644 --- a/Tests/NimbleTests/Matchers/EqualTest.swift +++ b/Tests/NimbleTests/Matchers/EqualTest.swift @@ -315,7 +315,7 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { () async in originalArray } + let originalArrayAsync = { @Sendable () async in originalArray } await expect(originalArrayAsync).toEventually(equal(expectedArray)) await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) await expect(originalArrayAsync).toEventuallyNot(equal([])) @@ -348,7 +348,7 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { () async in originalArray } + let originalArrayAsync = { @Sendable () async in originalArray } await expect(originalArrayAsync).toEventually(equal(expectedArray)) await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) await expect(originalArrayAsync).toEventuallyNot(equal([])) @@ -381,7 +381,7 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { () async in originalArray } + let originalArrayAsync = { @Sendable () async in originalArray } await expect(originalArrayAsync).toEventually(equal(expectedArray)) await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) await expect(originalArrayAsync).toEventuallyNot(equal([])) @@ -414,7 +414,7 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { () async in originalArray } + let originalArrayAsync = { @Sendable () async in originalArray } await expect(originalArrayAsync).toEventually(equal(expectedArray)) await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) await expect(originalArrayAsync).toEventuallyNot(equal([])) @@ -447,7 +447,7 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { () async in originalArray } + let originalArrayAsync = { @Sendable () async in originalArray } await expect(originalArrayAsync).toEventually(equal(expectedArray)) await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) await expect(originalArrayAsync).toEventuallyNot(equal([])) From a3301f0e4141c9d607dce6a167a496b2218a1dd9 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sat, 29 Jul 2023 16:49:27 -0700 Subject: [PATCH 03/25] The async variants of expect now require Sendable values (#1071) --- Sources/Nimble/AsyncExpression.swift | 6 +++--- Sources/Nimble/DSL+AsyncAwait.swift | 12 +++++------ Sources/Nimble/Expectation.swift | 8 +++++--- Sources/Nimble/Matchers/AsyncMatcher.swift | 24 +++++++++++----------- 4 files changed, 26 insertions(+), 24 deletions(-) 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 59ea7b1d..d9dba629 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -12,7 +12,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -21,7 +21,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -40,7 +40,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -50,7 +50,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #filePath, l /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -60,7 +60,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #filePath, l /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ 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) From 63743e7b45586d045b60261255f9ef25495d0d17 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sat, 12 Aug 2023 20:56:07 -0700 Subject: [PATCH 04/25] 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 --- Sources/Nimble/ExpectationMessage.swift | 4 +- Sources/Nimble/Matchers/AsyncMatcher.swift | 2 +- Sources/Nimble/Matchers/Matcher.swift | 28 ++++---- .../Nimble/Matchers/PostNotification.swift | 28 +++++++- Tests/NimbleTests/SynchronousTest.swift | 65 +++++++++++++------ 5 files changed, 88 insertions(+), 39 deletions(-) diff --git a/Sources/Nimble/ExpectationMessage.swift b/Sources/Nimble/ExpectationMessage.swift index 4efda7c0..2bff900e 100644 --- a/Sources/Nimble/ExpectationMessage.swift +++ b/Sources/Nimble/ExpectationMessage.swift @@ -1,4 +1,4 @@ -public indirect enum ExpectationMessage { +public indirect enum ExpectationMessage: Sendable { // --- Primary Expectations --- /// includes actual value in output ("expected to , got ") case expectedActualValueTo(/* message: */ String) @@ -204,7 +204,7 @@ extension FailureMessage { #if canImport(Darwin) import class Foundation.NSObject -public class NMBExpectationMessage: NSObject { +public final class NMBExpectationMessage: NSObject, Sendable { private let msg: ExpectationMessage internal init(swift msg: ExpectationMessage) { diff --git a/Sources/Nimble/Matchers/AsyncMatcher.swift b/Sources/Nimble/Matchers/AsyncMatcher.swift index 6b800029..78db294d 100644 --- a/Sources/Nimble/Matchers/AsyncMatcher.swift +++ b/Sources/Nimble/Matchers/AsyncMatcher.swift @@ -3,7 +3,7 @@ public protocol AsyncableMatcher: Sendable { func satisfies(_ expression: AsyncExpression) async throws -> MatcherResult } -extension Matcher: AsyncableMatcher where T: Sendable { +extension Matcher: AsyncableMatcher { public func satisfies(_ expression: AsyncExpression) async throws -> MatcherResult { try satisfies(await expression.toSynchronousExpression()) } diff --git a/Sources/Nimble/Matchers/Matcher.swift b/Sources/Nimble/Matchers/Matcher.swift index 375419e4..8f31e1ca 100644 --- a/Sources/Nimble/Matchers/Matcher.swift +++ b/Sources/Nimble/Matchers/Matcher.swift @@ -19,10 +19,10 @@ /// renamed `NSMatcher` to `Matcher`. In response, we decided to rename `Matcher` to /// `Matcher`. public struct Matcher { - fileprivate var matcher: (Expression) throws -> MatcherResult + fileprivate let matcher: @Sendable (Expression) throws -> MatcherResult /// Constructs a matcher that knows how take a given value - public init(_ matcher: @escaping (Expression) throws -> MatcherResult) { + public init(_ matcher: @escaping @Sendable (Expression) throws -> MatcherResult) { self.matcher = matcher } @@ -39,10 +39,12 @@ public struct Matcher { @available(*, deprecated, renamed: "Matcher") public typealias Predicate = Matcher +extension Matcher: Sendable where T: Sendable {} + /// Provides convenience helpers to defining matchers extension Matcher { /// Like Matcher() constructor, but automatically guard against nil (actual) values - public static func define(matcher: @escaping (Expression) throws -> MatcherResult) -> Matcher { + public static func define(matcher: @escaping @Sendable (Expression) throws -> MatcherResult) -> Matcher { return Matcher { actual in return try matcher(actual) }.requireNonNil @@ -50,7 +52,7 @@ extension Matcher { /// 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 (Expression, ExpectationMessage) throws -> MatcherResult) -> Matcher { + public static func define(_ message: String = "match", matcher: @escaping @Sendable (Expression, ExpectationMessage) throws -> MatcherResult) -> Matcher { return Matcher { actual in return try matcher(actual, .expectedActualValueTo(message)) }.requireNonNil @@ -58,7 +60,7 @@ extension Matcher { /// 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 (Expression, ExpectationMessage) throws -> MatcherResult) -> Matcher { + public static func defineNilable(_ message: String = "match", matcher: @escaping @Sendable (Expression, ExpectationMessage) throws -> MatcherResult) -> Matcher { return Matcher { actual in return try matcher(actual, .expectedActualValueTo(message)) } @@ -70,7 +72,7 @@ extension Matcher { /// error message. /// /// Also ensures the matcher's actual value cannot pass with `nil` given. - public static func simple(_ message: String = "match", matcher: @escaping (Expression) throws -> MatcherStatus) -> Matcher { + public static func simple(_ message: String = "match", matcher: @escaping @Sendable (Expression) throws -> MatcherStatus) -> Matcher { return Matcher { actual in return MatcherResult(status: try matcher(actual), message: .expectedActualValueTo(message)) }.requireNonNil @@ -80,7 +82,7 @@ extension Matcher { /// 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 (Expression) throws -> MatcherStatus) -> Matcher { + public static func simpleNilable(_ message: String = "match", matcher: @escaping @Sendable (Expression) throws -> MatcherStatus) -> Matcher { return Matcher { actual in return MatcherResult(status: try matcher(actual), message: .expectedActualValueTo(message)) } @@ -88,7 +90,7 @@ extension Matcher { } /// The Expectation style intended for comparison to a MatcherStatus. -public enum ExpectationStyle { +public enum ExpectationStyle: Sendable { case toMatch, toNotMatch } @@ -123,7 +125,7 @@ public struct MatcherResult { public typealias PredicateResult = MatcherResult /// MatcherStatus is a trinary that indicates if a Matcher matches a given value or not -public enum MatcherStatus { +public enum MatcherStatus: Sendable { /// Matches indicates if the matcher / matcher passes with the given value /// /// For example, `equals(1)` returns `.matches` for `expect(1).to(equal(1))`. @@ -181,7 +183,7 @@ public typealias PredicateStatus = MatcherStatus extension Matcher { // Someday, make this public? Needs documentation - internal func after(f: @escaping (Expression, MatcherResult) throws -> MatcherResult) -> Matcher { + internal func after(f: @escaping @Sendable (Expression, MatcherResult) throws -> MatcherResult) -> Matcher { // swiftlint:disable:previous identifier_name return Matcher { actual -> MatcherResult in let result = try self.satisfies(actual) @@ -207,7 +209,7 @@ extension Matcher { #if canImport(Darwin) import class Foundation.NSObject -public typealias MatcherBlock = (_ actualExpression: Expression) throws -> NMBMatcherResult +public typealias MatcherBlock = @Sendable (_ actualExpression: Expression) throws -> NMBMatcherResult /// Provides an easy upgrade path for custom Matchers to be renamed to Matchers @available(*, deprecated, renamed: "MatcherBlock") @@ -225,7 +227,7 @@ public class NMBMatcher: NSObject { self.init(matcher: predicate) } - func satisfies(_ expression: @escaping () throws -> NSObject?, location: SourceLocation) -> NMBMatcherResult { + func satisfies(_ expression: @escaping @Sendable () throws -> NSObject?, location: SourceLocation) -> NMBMatcherResult { let expr = Expression(expression: expression, location: location) do { return try self.matcher(expr) @@ -269,7 +271,7 @@ extension MatcherResult { } } -final public class NMBMatcherStatus: NSObject { +final public class NMBMatcherStatus: NSObject, Sendable { private let status: Int private init(status: Int) { self.status = status diff --git a/Sources/Nimble/Matchers/PostNotification.swift b/Sources/Nimble/Matchers/PostNotification.swift index 5144cc13..6601b68a 100644 --- a/Sources/Nimble/Matchers/PostNotification.swift +++ b/Sources/Nimble/Matchers/PostNotification.swift @@ -49,6 +49,22 @@ private let mainThread = pthread_self() private let mainThread = Thread.mainThread #endif +private final class OnlyOnceChecker: @unchecked Sendable { + var hasRun = false + let lock = NSRecursiveLock() + + func runOnlyOnce(_ closure: @Sendable () throws -> Void) rethrows { + lock.lock() + defer { + lock.unlock() + } + if !hasRun { + hasRun = true + try closure() + } + } +} + private func _postNotifications( _ matcher: Matcher<[Notification]>, from center: NotificationCenter, @@ -57,9 +73,16 @@ private func _postNotifications( _ = mainThread // Force lazy-loading of this value let collector = NotificationCollector(notificationCenter: center, names: names) collector.startObserving() - var once: Bool = false + let once = OnlyOnceChecker() return Matcher { actualExpression in + guard Thread.isMainThread else { + let message = ExpectationMessage + .expectedTo("post notifications - but was called off the main thread.") + .appended(details: "postNotifications and postDistributedNotifications attempted to run their predicate off the main thread. This is a bug in Nimble.") + return PredicateResult(status: .fail, message: message) + } + let collectorNotificationsExpression = Expression( memoizedExpression: { _ in return collector.observedNotifications @@ -69,8 +92,7 @@ private func _postNotifications( ) assert(Thread.isMainThread, "Only expecting closure to be evaluated on main thread.") - if !once { - once = true + try once.runOnlyOnce { _ = try actualExpression.evaluate() } diff --git a/Tests/NimbleTests/SynchronousTest.swift b/Tests/NimbleTests/SynchronousTest.swift index 98171f96..ef3dd2e0 100644 --- a/Tests/NimbleTests/SynchronousTest.swift +++ b/Tests/NimbleTests/SynchronousTest.swift @@ -37,29 +37,33 @@ final class SynchronousTest: XCTestCase { } func testToProvidesActualValueExpression() { - var value: Int? - expect(1).to(Matcher.simple { expr in value = try expr.evaluate(); return .matches }) - expect(value).to(equal(1)) + let recorder = Recorder() + expect(1).to(Matcher.simple { expr in recorder.record(try expr.evaluate()); return .matches }) + expect(recorder.records).to(equal([1])) } func testToProvidesAMemoizedActualValueExpression() { - var callCount = 0 - expect { callCount += 1 }.to(Matcher.simple { expr in + let recorder = Recorder() + expect { + recorder.record(()) + }.to(Matcher.simple { expr in _ = try expr.evaluate() _ = try expr.evaluate() return .matches }) - expect(callCount).to(equal(1)) + expect(recorder.records).to(haveCount(1)) } func testToProvidesAMemoizedActualValueExpressionIsEvaluatedAtMatcherControl() { - var callCount = 0 - expect { callCount += 1 }.to(Matcher.simple { expr in - expect(callCount).to(equal(0)) + let recorder = Recorder() + expect { + recorder.record(()) + }.to(Matcher.simple { expr in + expect(recorder.records).to(beEmpty()) _ = try expr.evaluate() return .matches }) - expect(callCount).to(equal(1)) + expect(recorder.records).to(haveCount(1)) } func testToMatchAgainstLazyProperties() { @@ -76,29 +80,29 @@ final class SynchronousTest: XCTestCase { } func testToNotProvidesActualValueExpression() { - var value: Int? - expect(1).toNot(Matcher.simple { expr in value = try expr.evaluate(); return .doesNotMatch }) - expect(value).to(equal(1)) + let recorder = Recorder() + expect(1).toNot(Matcher.simple { expr in recorder.record(try expr.evaluate()); return .doesNotMatch }) + expect(recorder.records).to(equal([1])) } func testToNotProvidesAMemoizedActualValueExpression() { - var callCount = 0 - expect { callCount += 1 }.toNot(Matcher.simple { expr in + let recorder = Recorder() + expect { recorder.record(()) }.toNot(Matcher.simple { expr in _ = try expr.evaluate() _ = try expr.evaluate() return .doesNotMatch }) - expect(callCount).to(equal(1)) + expect(recorder.records).to(haveCount(1)) } func testToNotProvidesAMemoizedActualValueExpressionIsEvaluatedAtMatcherControl() { - var callCount = 0 - expect { callCount += 1 }.toNot(Matcher.simple { expr in - expect(callCount).to(equal(0)) + let recorder = Recorder() + expect { recorder.record(()) }.toNot(Matcher.simple { expr in + expect(recorder.records).to(beEmpty()) _ = try expr.evaluate() return .doesNotMatch }) - expect(callCount).to(equal(1)) + expect(recorder.records).to(haveCount(1)) } func testToNegativeMatches() { @@ -129,3 +133,24 @@ final class SynchronousTest: XCTestCase { } } } + +private final class Recorder: @unchecked Sendable { + private var _records: [T] = [] + private let lock = NSRecursiveLock() + + var records: [T] { + get { + lock.lock() + defer { + lock.unlock() + } + return _records + } + } + + func record(_ value: T) { + lock.lock() + self._records.append(value) + lock.unlock() + } +} From 48d606fcc6b37c48968c424d2c9947127a2f3262 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Mon, 18 Mar 2024 18:22:47 -0700 Subject: [PATCH 05/25] Update Require DSL to be (mostly) Sendable. (#1130) * Update Require DSL to largely be Sendable * Fix now-broken tests --- Sources/Nimble/DSL+Require.swift | 20 ++-- .../Nimble/Matchers/PostNotification.swift | 2 +- Sources/Nimble/Polling+Require.swift | 10 +- Sources/Nimble/Requirement.swift | 6 +- .../NimbleTests/AsyncAwaitTest+Require.swift | 102 +++++++++--------- Tests/NimbleTests/PollingTest.swift | 2 +- 6 files changed, 75 insertions(+), 67 deletions(-) diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index bd73f72f..f3b47d43 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -123,7 +123,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, lin /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping () async throws -> T?) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping () async throws -> T?) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -137,7 +137,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -151,7 +151,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -167,7 +167,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement`. @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -183,7 +183,7 @@ public func requirea(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -199,7 +199,7 @@ public func requirea(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -256,7 +256,7 @@ public func unwraps(fileID: String = #fileID, file: FileString = #filePath, l /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @escaping () async throws -> T?) async throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @escaping () async throws -> T?) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, try await expression()).toNot(beNil(), description: description) } @@ -266,7 +266,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -276,7 +276,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwrapa` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { +public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, try await expression()).toNot(beNil(), description: description) } @@ -286,7 +286,7 @@ public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, l /// `unwrapa` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { +public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } diff --git a/Sources/Nimble/Matchers/PostNotification.swift b/Sources/Nimble/Matchers/PostNotification.swift index 6601b68a..fd00d1a2 100644 --- a/Sources/Nimble/Matchers/PostNotification.swift +++ b/Sources/Nimble/Matchers/PostNotification.swift @@ -80,7 +80,7 @@ private func _postNotifications( let message = ExpectationMessage .expectedTo("post notifications - but was called off the main thread.") .appended(details: "postNotifications and postDistributedNotifications attempted to run their predicate off the main thread. This is a bug in Nimble.") - return PredicateResult(status: .fail, message: message) + return MatcherResult(status: .fail, message: message) } let collectorNotificationsExpression = Expression( diff --git a/Sources/Nimble/Polling+Require.swift b/Sources/Nimble/Polling+Require.swift index 673ce70b..cbf688c5 100644 --- a/Sources/Nimble/Polling+Require.swift +++ b/Sources/Nimble/Polling+Require.swift @@ -189,7 +189,9 @@ extension SyncRequirement { public func alwaysTo(_ matcher: Matcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) throws -> Value { return try toAlways(matcher, until: until, pollInterval: pollInterval, description: description) } +} +extension SyncRequirement where Value: Sendable { // MARK: - Async Polling with Synchronous Matchers /// Tests the actual value using a matcher to match by checking continuously /// at each pollInterval until the timeout is reached. @@ -734,28 +736,28 @@ public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `requirea(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @escaping () async throws -> T?) async throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @escaping @Sendable () async throws -> T?) async throws -> T { try await requirea(file: file, line: line, try await expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `requirea(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `requirea(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrapa(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { +public func pollUnwrapa(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async throws -> T { try await requirea(file: file, line: line, try await expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `requirea(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrapa(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { +public func pollUnwrapa(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } diff --git a/Sources/Nimble/Requirement.swift b/Sources/Nimble/Requirement.swift index 91c8487d..d03cdcc3 100644 --- a/Sources/Nimble/Requirement.swift +++ b/Sources/Nimble/Requirement.swift @@ -1,6 +1,6 @@ import Foundation -public struct RequireError: Error, CustomNSError { +public struct RequireError: Error, CustomNSError, Sendable { let message: String let location: SourceLocation @@ -115,7 +115,9 @@ public struct SyncRequirement { public func notTo(_ matcher: Matcher, description: String? = nil) throws -> Value { try toNot(matcher, description: description) } +} +extension SyncRequirement where Value: Sendable { // MARK: - AsyncMatchers /// Tests the actual value using a matcher to match. @discardableResult @@ -140,7 +142,7 @@ public struct SyncRequirement { } } -public struct AsyncRequirement { +public struct AsyncRequirement: Sendable { public let expression: AsyncExpression /// A custom error to throw. diff --git a/Tests/NimbleTests/AsyncAwaitTest+Require.swift b/Tests/NimbleTests/AsyncAwaitTest+Require.swift index f6dfd712..f88e986a 100644 --- a/Tests/NimbleTests/AsyncAwaitTest+Require.swift +++ b/Tests/NimbleTests/AsyncAwaitTest+Require.swift @@ -8,7 +8,7 @@ import NimbleSharedTestHelpers final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_body_length func testToPositiveMatches() async throws { - func someAsyncFunction() async throws -> Int { + @Sendable func someAsyncFunction() async throws -> Int { try await Task.sleep(nanoseconds: 1_000_000) // 1 millisecond return 1 } @@ -16,10 +16,10 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b try await require { try await someAsyncFunction() }.to(equal(1)) } - class Error: Swift.Error {} - let errorToThrow = Error() + struct Error: Swift.Error, Sendable {} + static let errorToThrow = Error() - private func doThrowError() throws -> Int { + private static func doThrowError() throws -> Int { throw errorToThrow } @@ -40,16 +40,16 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b await failsWithErrorMessage("expected to eventually equal <1>, got <0>") { try await require { value }.toEventually(equal(1)) } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await require { try self.doThrowError() }.toEventually(equal(1)) + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await require { try Self.doThrowError() }.toEventually(equal(1)) } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await require { try self.doThrowError() }.toEventuallyNot(equal(0)) + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await require { try Self.doThrowError() }.toEventuallyNot(equal(0)) } } func testPollUnwrapPositiveCase() async { - func someAsyncFunction() async throws -> Int { + @Sendable func someAsyncFunction() async throws -> Int { try await Task.sleep(nanoseconds: 1_000_000) // 1 millisecond return 1 } @@ -62,11 +62,11 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b await failsWithErrorMessage("expected to eventually not be nil, got ") { try await pollUnwrap { nil as Int? } } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await pollUnwrap { try self.doThrowError() as Int? } + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await pollUnwrap { try Self.doThrowError() as Int? } } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await pollUnwrap { try self.doThrowError() as Int? } + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await pollUnwrap { try Self.doThrowError() as Int? } } } @@ -90,18 +90,20 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b } func testToEventuallyWaitingOnMainTask() async throws { - class EncapsulatedValue { - static var executed = false + class EncapsulatedValue: @unchecked Sendable { + var executed = false - static func execute() { + func execute() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - Self.executed = true + self.executed = true } } } - EncapsulatedValue.execute() - try await require(EncapsulatedValue.executed).toEventually(beTrue()) + let obj = EncapsulatedValue() + + obj.execute() + try await require(obj.executed).toEventually(beTrue()) } @MainActor @@ -117,7 +119,7 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b // is otherwise correctly executing on the main thread. // Double-y so if your CI automatically reads backtraces (like what the main thread checker will output) as test crashes, // and fails your build. - struct MySubject: CustomDebugStringConvertible, Equatable { + struct MySubject: CustomDebugStringConvertible, Equatable, Sendable { var debugDescription: String { expect(Thread.isMainThread).to(beTrue()) return "Test" @@ -141,7 +143,7 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b func testToEventuallyWithAsyncExpectationDoesNotNecessarilyExecutesExpressionOnMainActor() async throws { // This prevents a "Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in Swift 6" warning. // However, the functionality actually works as you'd expect it to, you're just expected to tag things to use the main actor. - func isMainThread() -> Bool { Thread.isMainThread } + @Sendable func isMainThread() -> Bool { Thread.isMainThread } try await requirea(isMainThread()).toEventually(beFalse()) try await requirea(isMainThread()).toEventuallyNot(beTrue()) @@ -149,41 +151,43 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b try await requirea(isMainThread()).toNever(beTrue(), until: .seconds(1)) } - @MainActor - func testToEventuallyWithAsyncExpectationDoesExecuteExpressionOnMainActorWhenTestRunsOnMainActor() async throws { - // This prevents a "Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in Swift 6" warning. - // However, the functionality actually works as you'd expect it to, you're just expected to tag things to use the main actor. - func isMainThread() -> Bool { Thread.isMainThread } - - try await requirea(isMainThread()).toEventually(beTrue()) - try await requirea(isMainThread()).toEventuallyNot(beFalse()) - try await requirea(isMainThread()).toAlways(beTrue(), until: .seconds(1)) - try await requirea(isMainThread()).toNever(beFalse(), until: .seconds(1)) - } - func testToEventuallyWithCustomDefaultTimeout() async throws { PollingDefaults.timeout = .seconds(2) defer { PollingDefaults.timeout = .seconds(1) } - var value = 0 + final class Box: @unchecked Sendable { + private let lock = NSRecursiveLock() + + private var _value = 0 + var value: Int { + lock.lock() + defer { + lock.unlock() + } + return _value + } - let sleepThenSetValueTo: (Int) -> Void = { newValue in - Thread.sleep(forTimeInterval: 1.1) - value = newValue + func sleepThenSetValueTo(_ newValue: Int) { + Thread.sleep(forTimeInterval: 1.1) + lock.lock() + _value = newValue + lock.unlock() + } } + let box = Box() let task = Task { - sleepThenSetValueTo(1) + box.sleepThenSetValueTo(1) } - try await require { value }.toEventually(equal(1)) + try await require { box.value }.toEventually(equal(1)) let secondTask = Task { - sleepThenSetValueTo(0) + box.sleepThenSetValueTo(0) } - try await require { value }.toEventuallyNot(equal(1)) + try await require { box.value }.toEventuallyNot(equal(1)) _ = await task.value _ = await secondTask.result @@ -237,11 +241,11 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b deferToMainQueue { value = 1 } try await require { value }.neverTo(equal(1)) } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await require { try self.doThrowError() }.toNever(equal(0)) + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await require { try Self.doThrowError() }.toNever(equal(0)) } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await require { try self.doThrowError() }.neverTo(equal(0)) + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await require { try Self.doThrowError() }.neverTo(equal(0)) } await failsWithErrorMessage("expected to never equal <1>, got <1>") { try await require(1).toNever(equal(1)) @@ -273,11 +277,11 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b deferToMainQueue { value = 0 } try await require { value }.alwaysTo(equal(1)) } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await require { try self.doThrowError() }.toAlways(equal(0)) + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await require { try Self.doThrowError() }.toAlways(equal(0)) } - await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { - try await require { try self.doThrowError() }.alwaysTo(equal(0)) + await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { + try await require { try Self.doThrowError() }.alwaysTo(equal(0)) } await failsWithErrorMessage("expected to always equal <0>, got (use beNil() to match nils)") { try await require(nil).toAlways(equal(0)) diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index 7c636129..e414b7b3 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -13,7 +13,7 @@ import NimbleSharedTestHelpers // swiftlint:disable:next type_body_length final class PollingTest: XCTestCase { - class Error: Swift.Error {} + struct Error: Swift.Error, Sendable {} let errorToThrow = Error() private func doThrowError() throws -> Int { From 8b5e280136dbda3d219e5eff016639b2b82cab49 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Mon, 18 Mar 2024 19:59:14 -0700 Subject: [PATCH 06/25] Make FailureMessage sendable. (#1131) --- Nimble.xcodeproj/project.pbxproj | 4 + Sources/Nimble/ExpectationMessage.swift | 27 --- Sources/Nimble/FailureMessage.swift | 186 ++++++++++++++++---- Sources/Nimble/Utils/NSLocking+Nimble.swift | 11 ++ 4 files changed, 162 insertions(+), 66 deletions(-) create mode 100644 Sources/Nimble/Utils/NSLocking+Nimble.swift diff --git a/Nimble.xcodeproj/project.pbxproj b/Nimble.xcodeproj/project.pbxproj index 90edd560..da2a24c1 100644 --- a/Nimble.xcodeproj/project.pbxproj +++ b/Nimble.xcodeproj/project.pbxproj @@ -139,6 +139,7 @@ 895644DF2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */; }; 896962412A5FABD000A7929D /* AsyncAllPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962402A5FABD000A7929D /* AsyncAllPass.swift */; }; 8969624A2A5FAD5F00A7929D /* AsyncAllPassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */; }; + 897F84F42BA922B500BF354B /* NSLocking+Nimble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */; }; 898F28B025D9F4C30052B8D0 /* AlwaysFailMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */; }; 899441EF2902EE4B00C1FAF9 /* AsyncAwaitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 899441EE2902EE4B00C1FAF9 /* AsyncAwaitTest.swift */; }; 899441F82902EF2500C1FAF9 /* DSL+AsyncAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */; }; @@ -330,6 +331,7 @@ 895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestingSupportTest.swift; sourceTree = ""; }; 896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = ""; }; 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = ""; }; + 897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLocking+Nimble.swift"; sourceTree = ""; }; 898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysFailMatcher.swift; sourceTree = ""; }; 899441EE2902EE4B00C1FAF9 /* AsyncAwaitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAwaitTest.swift; sourceTree = ""; }; 899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+AsyncAwait.swift"; sourceTree = ""; }; @@ -624,6 +626,7 @@ isa = PBXGroup; children = ( 1FD8CD261968AB07008ED995 /* PollAwait.swift */, + 897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */, 89F5E08B290B8D22001F9377 /* AsyncAwait.swift */, 891A04702AB0164500B46613 /* AsyncTimerSequence.swift */, 1FD8CD271968AB07008ED995 /* SourceLocation.swift */, @@ -892,6 +895,7 @@ 1FD8CD571968AB07008ED995 /* Contain.swift in Sources */, 7A0A26231E7F52360092A34E /* ToSucceed.swift in Sources */, 89F5E0862908E655001F9377 /* Polling+AsyncAwait.swift in Sources */, + 897F84F42BA922B500BF354B /* NSLocking+Nimble.swift in Sources */, 899441F82902EF2500C1FAF9 /* DSL+AsyncAwait.swift in Sources */, 1FD8CD491968AB07008ED995 /* BeGreaterThanOrEqualTo.swift in Sources */, 1FE661571E6574E30035F243 /* ExpectationMessage.swift in Sources */, diff --git a/Sources/Nimble/ExpectationMessage.swift b/Sources/Nimble/ExpectationMessage.swift index 2bff900e..c3072eef 100644 --- a/Sources/Nimble/ExpectationMessage.swift +++ b/Sources/Nimble/ExpectationMessage.swift @@ -174,33 +174,6 @@ public indirect enum ExpectationMessage: Sendable { } } -extension FailureMessage { - internal func toExpectationMessage() -> ExpectationMessage { - let defaultMessage = FailureMessage() - if expected != defaultMessage.expected || _stringValueOverride != nil { - return .fail(stringValue) - } - - var message: ExpectationMessage = .fail(userDescription ?? "") - if actualValue != "" && actualValue != nil { - message = .expectedCustomValueTo(postfixMessage, actual: actualValue ?? "") - } else if postfixMessage != defaultMessage.postfixMessage { - if actualValue == nil { - message = .expectedTo(postfixMessage) - } else { - message = .expectedActualValueTo(postfixMessage) - } - } - if postfixActual != defaultMessage.postfixActual { - message = .appends(message, postfixActual) - } - if let extended = extendedMessage { - message = .details(message, extended) - } - return message - } -} - #if canImport(Darwin) import class Foundation.NSObject diff --git a/Sources/Nimble/FailureMessage.swift b/Sources/Nimble/FailureMessage.swift index 8b60b9c2..12561b1e 100644 --- a/Sources/Nimble/FailureMessage.swift +++ b/Sources/Nimble/FailureMessage.swift @@ -3,19 +3,81 @@ import Foundation /// Encapsulates the failure message that matchers can report to the end user. /// /// This is shared state between Nimble and matchers that mutate this value. -public class FailureMessage: NSObject { - public var expected: String = "expected" - public var actualValue: String? = "" // empty string -> use default; nil -> exclude - public var to: String = "to" - public var postfixMessage: String = "match" - public var postfixActual: String = "" +public final class FailureMessage: NSObject, @unchecked Sendable { + private let lock = NSRecursiveLock() + + private var _expected: String = "expected" + private var _actualValue: String? = "" // empty string -> use default; nil -> exclude + private var _to: String = "to" + private var _postfixMessage: String = "match" + private var _postfixActual: String = "" /// An optional message that will be appended as a new line and provides additional details /// about the failure. This message will only be visible in the issue navigator / in logs but /// not directly in the source editor since only a single line is presented there. - public var extendedMessage: String? - public var userDescription: String? + private var _extendedMessage: String? + private var _userDescription: String? - public var stringValue: String { + public var expected: String { + get { + return lock.sync { return _expected } + } + set { + lock.sync { _expected = newValue } + } + } + public var actualValue: String? { + get { + return lock.sync { return _actualValue } + } + set { + lock.sync { _actualValue = newValue } + } + } // empty string -> use default; nil -> exclude + public var to: String { + get { + return lock.sync { return _to } + } + set { + lock.sync { _to = newValue } + } + } + public var postfixMessage: String { + get { + return lock.sync { return _postfixMessage } + } + set { + lock.sync { _postfixMessage = newValue } + } + } + public var postfixActual: String { + get { + return lock.sync { return _postfixActual } + } + set { + lock.sync { _postfixActual = newValue } + } + } + /// An optional message that will be appended as a new line and provides additional details + /// about the failure. This message will only be visible in the issue navigator / in logs but + /// not directly in the source editor since only a single line is presented there. + public var extendedMessage: String? { + get { + return lock.sync { return _extendedMessage } + } + set { + lock.sync { _extendedMessage = newValue } + } + } + public var userDescription: String? { + get { + return lock.sync { return _userDescription } + } + set { + lock.sync { _userDescription = newValue } + } + } + + private var _stringValue: String { get { if let value = _stringValueOverride { return value @@ -27,20 +89,33 @@ public class FailureMessage: NSObject { _stringValueOverride = newValue } } + public var stringValue: String { + get { + return lock.sync { return _stringValue } + } + set { + lock.sync { _stringValue = newValue } + } + } - internal var _stringValueOverride: String? - internal var hasOverriddenStringValue: Bool { + private var _stringValueOverride: String? + private var _hasOverriddenStringValue: Bool { return _stringValueOverride != nil } + internal var hasOverriddenStringValue: Bool { + return lock.sync { return _hasOverriddenStringValue } + } + public override init() { + super.init() } public init(stringValue: String) { _stringValueOverride = stringValue } - internal func stripNewlines(_ str: String) -> String { + private func stripNewlines(_ str: String) -> String { let whitespaces = CharacterSet.whitespacesAndNewlines return str .components(separatedBy: "\n") @@ -48,45 +123,78 @@ public class FailureMessage: NSObject { .joined(separator: "") } - internal func computeStringValue() -> String { - var value = "\(expected) \(to) \(postfixMessage)" - if let actualValue = actualValue { - value = "\(expected) \(to) \(postfixMessage), got \(actualValue)\(postfixActual)" - } - value = stripNewlines(value) + private func computeStringValue() -> String { + return lock.sync { + var value = "\(_expected) \(_to) \(_postfixMessage)" + if let actualValue = _actualValue { + value = "\(_expected) \(_to) \(_postfixMessage), got \(actualValue)\(_postfixActual)" + } + value = stripNewlines(value) - if let extendedMessage = extendedMessage { - value += "\n\(extendedMessage)" - } + if let extendedMessage = _extendedMessage { + value += "\n\(extendedMessage)" + } - if let userDescription = userDescription { - return "\(userDescription)\n\(value)" - } + if let userDescription = _userDescription { + return "\(userDescription)\n\(value)" + } - return value + return value + } } internal func appendMessage(_ msg: String) { - if hasOverriddenStringValue { - stringValue += "\(msg)" - } else if actualValue != nil { - postfixActual += msg - } else { - postfixMessage += msg + lock.sync { + if _hasOverriddenStringValue { + _stringValue += "\(msg)" + } else if _actualValue != nil { + _postfixActual += msg + } else { + _postfixMessage += msg + } } } internal func appendDetails(_ msg: String) { - if hasOverriddenStringValue { - if let desc = userDescription { - stringValue = "\(desc)\n\(stringValue)" + lock.sync { + if _hasOverriddenStringValue { + if let desc = _userDescription { + _stringValue = "\(desc)\n\(_stringValue)" + } + _stringValue += "\n\(msg)" + } else { + if let desc = _userDescription { + _userDescription = desc + } + _extendedMessage = msg + } + } + } + + internal func toExpectationMessage() -> ExpectationMessage { + lock.sync { + let defaultMessage = FailureMessage() + if _expected != defaultMessage._expected || _hasOverriddenStringValue { + return .fail(_stringValue) + } + + var message: ExpectationMessage = .fail(_userDescription ?? "") + if _actualValue != "" && _actualValue != nil { + message = .expectedCustomValueTo(_postfixMessage, actual: _actualValue ?? "") + } else if _postfixMessage != defaultMessage._postfixMessage { + if _actualValue == nil { + message = .expectedTo(_postfixMessage) + } else { + message = .expectedActualValueTo(_postfixMessage) + } + } + if _postfixActual != defaultMessage._postfixActual { + message = .appends(message, _postfixActual) } - stringValue += "\n\(msg)" - } else { - if let desc = userDescription { - userDescription = desc + if let extended = _extendedMessage { + message = .details(message, extended) } - extendedMessage = msg + return message } } } diff --git a/Sources/Nimble/Utils/NSLocking+Nimble.swift b/Sources/Nimble/Utils/NSLocking+Nimble.swift new file mode 100644 index 00000000..67bd89e9 --- /dev/null +++ b/Sources/Nimble/Utils/NSLocking+Nimble.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NSLocking { + internal func sync(_ closure: () throws -> T) rethrows -> T { + lock() + defer { + unlock() + } + return try closure() + } +} From 696e8604c799e90af28e5022fdff230954caf67f Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Mon, 18 Mar 2024 20:30:01 -0700 Subject: [PATCH 07/25] Make MatcherResult conform to Sendable (#1132) * Make FailureMessage sendable. * Make MatcherResult conform to Sendable --- Sources/Nimble/Matchers/Matcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nimble/Matchers/Matcher.swift b/Sources/Nimble/Matchers/Matcher.swift index 8f31e1ca..376be7a8 100644 --- a/Sources/Nimble/Matchers/Matcher.swift +++ b/Sources/Nimble/Matchers/Matcher.swift @@ -96,7 +96,7 @@ public enum ExpectationStyle: Sendable { /// The value that a Matcher returns to describe if the given (actual) value matches the /// matcher. -public struct MatcherResult { +public struct MatcherResult: Sendable { /// Status indicates if the matcher matches, does not match, or fails. public var status: MatcherStatus /// The error message that can be displayed if it does not match From bde016acfac9b761ad6bc0e67654816d47535615 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 19 Mar 2024 09:11:13 -0700 Subject: [PATCH 08/25] Make the Polling Helpers Sendable (#1133) --- Sources/Nimble/Polling+AsyncAwait.swift | 2 +- Sources/Nimble/Utils/AsyncAwait.swift | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index 2238fb42..a351fac6 100644 --- a/Sources/Nimble/Polling+AsyncAwait.swift +++ b/Sources/Nimble/Polling+AsyncAwait.swift @@ -40,7 +40,7 @@ internal actor Poller { timeoutInterval: timeout, sourceLocation: expression.location, fnName: fnName) { - if self.updateMatcherResult(result: try await matcherRunner()) + if await self.updateMatcherResult(result: try await matcherRunner()) .toBoolean(expectation: style) { if matchStyle.isContinous { return .incomplete diff --git a/Sources/Nimble/Utils/AsyncAwait.swift b/Sources/Nimble/Utils/AsyncAwait.swift index 14166225..22cf94ca 100644 --- a/Sources/Nimble/Utils/AsyncAwait.swift +++ b/Sources/Nimble/Utils/AsyncAwait.swift @@ -12,7 +12,7 @@ private let pollLeeway = NimbleTimeInterval.milliseconds(1) // Like PollResult, except it doesn't support objective-c exceptions. // Which is tolerable because Swift Concurrency doesn't support recording objective-c exceptions. -internal enum AsyncPollResult { +internal enum AsyncPollResult: Sendable { /// Incomplete indicates None (aka - this value hasn't been fulfilled yet) case incomplete /// TimedOut indicates the result reached its defined timeout limit before returning @@ -57,10 +57,10 @@ internal enum AsyncPollResult { // Inspired by swift-async-algorithm's AsyncChannel, but massively simplified // especially given Nimble's usecase. // AsyncChannel: https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift -internal actor AsyncPromise { +internal actor AsyncPromise { private let storage = Storage() - private final class Storage { + private final class Storage: @unchecked Sendable { private var continuations: [UnsafeContinuation] = [] private var value: T? // Yes, this is not the fastest lock, but it's platform independent, @@ -131,7 +131,7 @@ internal actor AsyncPromise { /// checked. /// /// In addition, stopping the run loop is used to halt code executed on the main run loop. -private func timeout(timeoutQueue: DispatchQueue, timeoutInterval: NimbleTimeInterval, forcefullyAbortTimeout: NimbleTimeInterval) async -> AsyncPollResult { +private func timeout(timeoutQueue: DispatchQueue, timeoutInterval: NimbleTimeInterval, forcefullyAbortTimeout: NimbleTimeInterval) async -> AsyncPollResult { do { try await Task.sleep(nanoseconds: timeoutInterval.nanoseconds) } catch {} @@ -165,7 +165,7 @@ private func timeout(timeoutQueue: DispatchQueue, timeoutInterval: NimbleTime return await promise.value } -private func poll(_ pollInterval: NimbleTimeInterval, expression: @escaping () async throws -> PollStatus) async -> AsyncPollResult { +private func poll(_ pollInterval: NimbleTimeInterval, expression: @escaping @Sendable () async throws -> PollStatus) async -> AsyncPollResult { for try await _ in AsyncTimerSequence(interval: pollInterval) { do { if case .finished(let result) = try await expression() { @@ -200,7 +200,7 @@ private func runPoller( awaiter: Awaiter, fnName: String, sourceLocation: SourceLocation, - expression: @escaping () async throws -> PollStatus + expression: @escaping @Sendable () async throws -> PollStatus ) async -> AsyncPollResult { awaiter.waitLock.acquireWaitingLock( fnName, @@ -259,7 +259,7 @@ private func runAwaitTrigger( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, sourceLocation: SourceLocation, - _ closure: @escaping (@escaping (T) -> Void) async throws -> Void + _ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void ) async -> AsyncPollResult { let timeoutQueue = awaiter.timeoutQueue let completionCount = Box(value: 0) @@ -315,7 +315,7 @@ internal func performBlock( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, sourceLocation: SourceLocation, - _ closure: @escaping (@escaping (T) -> Void) async throws -> Void + _ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void ) async -> AsyncPollResult { await runAwaitTrigger( awaiter: NimbleEnvironment.activeInstance.awaiter, @@ -330,7 +330,7 @@ internal func pollBlock( timeoutInterval: NimbleTimeInterval, sourceLocation: SourceLocation, fnName: String, - expression: @escaping () async throws -> PollStatus) async -> AsyncPollResult { + expression: @escaping @Sendable () async throws -> PollStatus) async -> AsyncPollResult { await runPoller( timeoutInterval: timeoutInterval, pollInterval: pollInterval, From a321e373ed643483b7c3bab927ebdb3352296d75 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 12 May 2024 21:56:28 -0700 Subject: [PATCH 09/25] Make AssertionHandler Sendable (#1141) --- Nimble.xcodeproj/project.pbxproj | 4 +++ .../Nimble/Adapters/AdapterProtocols.swift | 16 ++++++++-- .../Nimble/Adapters/AssertionDispatcher.swift | 2 +- .../Nimble/Adapters/AssertionRecorder.swift | 14 ++++++-- .../Adapters/NimbleSwiftTestingHandler.swift | 2 +- .../Nimble/Adapters/NimbleXCTestHandler.swift | 29 ++++++++++++----- Sources/Nimble/Utils/LockedContainer.swift | 32 +++++++++++++++++++ 7 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 Sources/Nimble/Utils/LockedContainer.swift diff --git a/Nimble.xcodeproj/project.pbxproj b/Nimble.xcodeproj/project.pbxproj index da2a24c1..dd5ec809 100644 --- a/Nimble.xcodeproj/project.pbxproj +++ b/Nimble.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 89D8AC852B3211C600410644 /* CwlCatchException in Frameworks */ = {isa = PBXBuildFile; productRef = 89D8AC842B3211C600410644 /* CwlCatchException */; }; 89D8AC872B3211EA00410644 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, watchos, ); productRef = 89D8AC862B3211EA00410644 /* CwlPosixPreconditionTesting */; }; 89D8AC892B3211EA00410644 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; platformFilters = (driverkit, ios, maccatalyst, macos, xros, ); productRef = 89D8AC882B3211EA00410644 /* CwlPreconditionTesting */; }; + 89E5E1682BC78724002D54ED /* LockedContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E5E1672BC78724002D54ED /* LockedContainer.swift */; }; 89EEF5A52A03293100988224 /* AsyncMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5A42A03293100988224 /* AsyncMatcher.swift */; }; 89EEF5B72A032C3200988224 /* AsyncPredicateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */; }; 89EEF5C02A06211C00988224 /* AsyncHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */; }; @@ -339,6 +340,7 @@ 89B8C6102C6478F2001F12D3 /* NegationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NegationTest.swift; sourceTree = ""; }; 89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTimerSequenceTest.swift; sourceTree = ""; }; 89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPromiseTest.swift; sourceTree = ""; }; + 89E5E1672BC78724002D54ED /* LockedContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedContainer.swift; sourceTree = ""; }; 89EEF5A42A03293100988224 /* AsyncMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMatcher.swift; sourceTree = ""; }; 89EEF5B22A032C2500988224 /* AsyncPredicateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPredicateTest.swift; sourceTree = ""; }; 89EEF5BB2A06210D00988224 /* AsyncHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHelpers.swift; sourceTree = ""; }; @@ -633,6 +635,7 @@ 1FD8CD281968AB07008ED995 /* Stringers.swift */, AE4BA9AC1C88DDB500B73906 /* Errors.swift */, 0477153423B740AD00402D4E /* NimbleTimeInterval.swift */, + 89E5E1672BC78724002D54ED /* LockedContainer.swift */, ); path = Utils; sourceTree = ""; @@ -867,6 +870,7 @@ 1F1871D91CA89EF100A34BF2 /* NMBExpectation.swift in Sources */, DA9E8C831A414BB9002633C2 /* DSL+Wait.swift in Sources */, DDB1BC7A1A92235600F743C3 /* AllPass.swift in Sources */, + 89E5E1682BC78724002D54ED /* LockedContainer.swift in Sources */, 1FD8CD3F1968AB07008ED995 /* BeAKindOf.swift in Sources */, 1FD8CD2F1968AB07008ED995 /* AssertionRecorder.swift in Sources */, 7B13BA061DD360AA00C9098C /* ContainElementSatisfying.swift in Sources */, diff --git a/Sources/Nimble/Adapters/AdapterProtocols.swift b/Sources/Nimble/Adapters/AdapterProtocols.swift index d7734879..5aba0179 100644 --- a/Sources/Nimble/Adapters/AdapterProtocols.swift +++ b/Sources/Nimble/Adapters/AdapterProtocols.swift @@ -1,5 +1,5 @@ /// Protocol for the assertion handler that Nimble uses for all expectations. -public protocol AssertionHandler { +public protocol AssertionHandler: Sendable { func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) } @@ -10,11 +10,21 @@ public protocol AssertionHandler { /// before using any matchers, otherwise Nimble will abort the program. /// /// @see AssertionHandler -public var NimbleAssertionHandler: AssertionHandler = { () -> AssertionHandler in +public var NimbleAssertionHandler: AssertionHandler { + // swiftlint:disable:previous identifier_name + get { + _NimbleAssertionHandler.value + } + set { + _NimbleAssertionHandler.set(newValue) + } +} + +private let _NimbleAssertionHandler = LockedContainer { // swiftlint:disable:previous identifier_name if isSwiftTestingAvailable() || isXCTestAvailable() { return NimbleTestingHandler() } return NimbleTestingUnavailableHandler() -}() +} diff --git a/Sources/Nimble/Adapters/AssertionDispatcher.swift b/Sources/Nimble/Adapters/AssertionDispatcher.swift index 94a9030e..cf0b8ac7 100644 --- a/Sources/Nimble/Adapters/AssertionDispatcher.swift +++ b/Sources/Nimble/Adapters/AssertionDispatcher.swift @@ -4,7 +4,7 @@ /// @warning Does not fully dispatch if one of the handlers raises an exception. /// This is possible with XCTest-based assertion handlers. /// -public class AssertionDispatcher: AssertionHandler { +public final class AssertionDispatcher: AssertionHandler { let handlers: [AssertionHandler] public init(handlers: [AssertionHandler]) { diff --git a/Sources/Nimble/Adapters/AssertionRecorder.swift b/Sources/Nimble/Adapters/AssertionRecorder.swift index 0ee39721..4a3df689 100644 --- a/Sources/Nimble/Adapters/AssertionRecorder.swift +++ b/Sources/Nimble/Adapters/AssertionRecorder.swift @@ -3,7 +3,7 @@ /// /// @see AssertionRecorder /// @see AssertionHandler -public struct AssertionRecord: CustomStringConvertible { +public struct AssertionRecord: CustomStringConvertible, Sendable { /// Whether the assertion succeeded or failed public let success: Bool /// The failure message the assertion would display on failure. @@ -20,9 +20,17 @@ public struct AssertionRecord: CustomStringConvertible { /// This is useful for testing failure messages for matchers. /// /// @see AssertionHandler -public class AssertionRecorder: AssertionHandler { +public final class AssertionRecorder: AssertionHandler { /// All the assertions that were captured by this recorder - public var assertions = [AssertionRecord]() + public var assertions: [AssertionRecord] { + get { + _assertion.value + } + set { + _assertion.set(newValue) + } + } + private let _assertion = LockedContainer([AssertionRecord]()) public init() {} diff --git a/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift b/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift index bf86bb6d..ca7511f0 100644 --- a/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift +++ b/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift @@ -7,7 +7,7 @@ import Foundation @_implementationOnly import Testing #endif -public class NimbleSwiftTestingHandler: AssertionHandler { +public struct NimbleSwiftTestingHandler: AssertionHandler { public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) { if !assertion { recordTestingFailure("\(message.stringValue)\n", location: location) diff --git a/Sources/Nimble/Adapters/NimbleXCTestHandler.swift b/Sources/Nimble/Adapters/NimbleXCTestHandler.swift index 8db21f44..56a932a9 100644 --- a/Sources/Nimble/Adapters/NimbleXCTestHandler.swift +++ b/Sources/Nimble/Adapters/NimbleXCTestHandler.swift @@ -2,7 +2,7 @@ import Foundation import XCTest /// Default handler for Nimble. This assertion handler passes on to Swift Testing or XCTest. -public class NimbleTestingHandler: AssertionHandler { +public struct NimbleTestingHandler: AssertionHandler { public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) { if isRunningSwiftTest() { NimbleSwiftTestingHandler().assert(assertion, message: message, location: location) @@ -13,7 +13,7 @@ public class NimbleTestingHandler: AssertionHandler { } /// This assertion handler passes failures along to XCTest. -public class NimbleXCTestHandler: AssertionHandler { +public struct NimbleXCTestHandler: AssertionHandler { public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) { if !assertion { recordFailure("\(message.stringValue)\n", location: location) @@ -23,7 +23,7 @@ public class NimbleXCTestHandler: AssertionHandler { /// Alternative handler for Nimble. This assertion handler passes failures along /// to XCTest by attempting to reduce the failure message size. -public class NimbleShortXCTestHandler: AssertionHandler { +public struct NimbleShortXCTestHandler: AssertionHandler { public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) { if !assertion { let msg: String @@ -39,7 +39,7 @@ public class NimbleShortXCTestHandler: AssertionHandler { /// Fallback handler in case XCTest/Swift Testing is unavailable. This assertion handler will abort /// the program if it is invoked. -class NimbleTestingUnavailableHandler: AssertionHandler { +struct NimbleTestingUnavailableHandler: AssertionHandler { func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) { fatalError("XCTest and Swift Testing are not available and no custom assertion handler was configured. Aborting.") } @@ -47,24 +47,37 @@ class NimbleTestingUnavailableHandler: AssertionHandler { #if canImport(Darwin) /// Helper class providing access to the currently executing XCTestCase instance, if any -@objc final public class CurrentTestCaseTracker: NSObject, XCTestObservation { +@objc final public class CurrentTestCaseTracker: NSObject, XCTestObservation, @unchecked Sendable { @objc public static let sharedInstance = CurrentTestCaseTracker() - private(set) var currentTestCase: XCTestCase? + private let lock = NSRecursiveLock() + + private var _currentTestCase: XCTestCase? + var currentTestCase: XCTestCase? { + lock.lock() + defer { lock.unlock() } + return _currentTestCase + } private var stashed_swift_reportFatalErrorsToDebugger: Bool = false @objc public func testCaseWillStart(_ testCase: XCTestCase) { + lock.lock() + defer { lock.unlock() } + #if (os(macOS) || os(iOS) || os(visionOS)) && !SWIFT_PACKAGE stashed_swift_reportFatalErrorsToDebugger = _swift_reportFatalErrorsToDebugger _swift_reportFatalErrorsToDebugger = false #endif - currentTestCase = testCase + _currentTestCase = testCase } @objc public func testCaseDidFinish(_ testCase: XCTestCase) { - currentTestCase = nil + lock.lock() + defer { lock.unlock() } + + _currentTestCase = nil #if (os(macOS) || os(iOS) || os(visionOS)) && !SWIFT_PACKAGE _swift_reportFatalErrorsToDebugger = stashed_swift_reportFatalErrorsToDebugger diff --git a/Sources/Nimble/Utils/LockedContainer.swift b/Sources/Nimble/Utils/LockedContainer.swift new file mode 100644 index 00000000..fcc8455b --- /dev/null +++ b/Sources/Nimble/Utils/LockedContainer.swift @@ -0,0 +1,32 @@ +import Foundation + +final class LockedContainer: @unchecked Sendable { + private let lock = NSRecursiveLock() + private var _value: T + + var value: T { + lock.lock() + defer { lock.unlock() } + return _value + } + + init(_ value: T) { + _value = value + } + + init(_ closure: () -> T) { + _value = closure() + } + + func operate(_ closure: (T) -> T) { + lock.lock() + defer { lock.unlock() } + _value = closure(_value) + } + + func set(_ newValue: T) { + lock.lock() + defer { lock.unlock() } + _value = newValue + } +} From 5beb29e55e1ccd4f3708aa068f2254eb613020d3 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Mon, 17 Jun 2024 23:21:16 -0700 Subject: [PATCH 10/25] Mark the protocol conformances in BeLogical as retroactive --- Sources/Nimble/Matchers/BeLogical.swift | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Nimble/Matchers/BeLogical.swift b/Sources/Nimble/Matchers/BeLogical.swift index ea04915e..d24444d7 100644 --- a/Sources/Nimble/Matchers/BeLogical.swift +++ b/Sources/Nimble/Matchers/BeLogical.swift @@ -1,72 +1,72 @@ import Foundation -extension Int8: Swift.ExpressibleByBooleanLiteral { +extension Int8: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).int8Value } } -extension UInt8: Swift.ExpressibleByBooleanLiteral { +extension UInt8: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).uint8Value } } -extension Int16: Swift.ExpressibleByBooleanLiteral { +extension Int16: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).int16Value } } -extension UInt16: Swift.ExpressibleByBooleanLiteral { +extension UInt16: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).uint16Value } } -extension Int32: Swift.ExpressibleByBooleanLiteral { +extension Int32: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).int32Value } } -extension UInt32: Swift.ExpressibleByBooleanLiteral { +extension UInt32: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).uint32Value } } -extension Int64: Swift.ExpressibleByBooleanLiteral { +extension Int64: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).int64Value } } -extension UInt64: ExpressibleByBooleanLiteral { +extension UInt64: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).uint64Value } } -extension Float: ExpressibleByBooleanLiteral { +extension Float: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).floatValue } } -extension Double: ExpressibleByBooleanLiteral { +extension Double: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).doubleValue } } -extension Int: ExpressibleByBooleanLiteral { +extension Int: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).intValue } } -extension UInt: ExpressibleByBooleanLiteral { +extension UInt: @retroactive ExpressibleByBooleanLiteral { public init(booleanLiteral value: Bool) { self = NSNumber(value: value).uintValue } From 8b75e85be60347dabd8cbf083a1270a9ce241c5f Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Mon, 17 Jun 2024 23:33:53 -0700 Subject: [PATCH 11/25] Mark the synchronous DSL funcs as sending --- Sources/Nimble/DSL+Require.swift | 24 ++++++++++++------------ Sources/Nimble/DSL.swift | 16 ++++++++-------- Sources/Nimble/Expression.swift | 8 ++++---- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index f3b47d43..c864e206 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -3,7 +3,7 @@ /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, @@ -17,7 +17,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -31,7 +31,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -45,7 +45,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -61,7 +61,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, line /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, @@ -77,7 +77,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -93,7 +93,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -109,7 +109,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -216,7 +216,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -226,7 +226,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -236,7 +236,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -246,7 +246,7 @@ public func unwraps(fileID: String = #fileID, file: FileString = #filePath, l /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } diff --git a/Sources/Nimble/DSL.swift b/Sources/Nimble/DSL.swift index 334a4ddb..1d6011b6 100644 --- a/Sources/Nimble/DSL.swift +++ b/Sources/Nimble/DSL.swift @@ -1,5 +1,5 @@ /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> T?) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, @@ -8,7 +8,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -17,7 +17,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -26,7 +26,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -36,7 +36,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> T?) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, @@ -46,7 +46,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -56,7 +56,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -66,7 +66,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), diff --git a/Sources/Nimble/Expression.swift b/Sources/Nimble/Expression.swift index 1bab44fc..be6d8d01 100644 --- a/Sources/Nimble/Expression.swift +++ b/Sources/Nimble/Expression.swift @@ -22,7 +22,7 @@ private func memoizedClosure(_ closure: @escaping () throws -> T) -> (Bool) t /// 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 Expression { - internal let _expression: (Bool) throws -> Value? + internal let _expression: (Bool) throws -> sending Value? internal let _withoutCaching: Bool public let location: SourceLocation public let isClosure: Bool @@ -38,7 +38,7 @@ public struct Expression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: @escaping () throws -> Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: @escaping () throws -> sending Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -59,7 +59,7 @@ public struct Expression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(memoizedExpression: @escaping (Bool) throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { + public init(memoizedExpression: @escaping (Bool) throws -> sending Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { self._expression = memoizedExpression self.location = location self._withoutCaching = withoutCaching @@ -74,7 +74,7 @@ public struct Expression { /// /// - Parameter block: The block that can cast the current Expression value to a /// new type. - public func cast(_ block: @escaping (Value?) throws -> U?) -> Expression { + public func cast(_ block: @escaping (Value?) throws -> sending U?) -> Expression { Expression( expression: ({ try block(self.evaluate()) }), location: self.location, From c60c44102f1caa9b6984eb4d1ded3d72005b88a4 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 12:48:47 -0700 Subject: [PATCH 12/25] Mark values in AsyncExpression as sending --- Sources/Nimble/AsyncExpression.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index 887226ab..7eeba260 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -1,8 +1,8 @@ -private actor MemoizedClosure { - var closure: @Sendable () async throws -> T +private actor MemoizedClosure { + var closure: () async throws -> sending T var cache: T? - init(_ closure: @escaping @Sendable () async throws -> T) { + init(_ closure: @escaping () async throws -> sending T) { self.closure = closure } @@ -11,7 +11,7 @@ private actor MemoizedClosure { return cache } - func call(_ withoutCaching: Bool) async throws -> T { + func call(_ withoutCaching: Bool) async throws -> sending T { if withoutCaching { return try await closure() } @@ -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: sending @escaping () async throws -> sending T) -> @Sendable (Bool) async throws -> sending T { let memoized = MemoizedClosure(closure) return { withoutCaching in try await memoized.call(withoutCaching) @@ -43,8 +43,8 @@ private func memoizedClosure(_ closure: @escaping @Sendable () asyn /// /// 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 { - internal let _expression: @Sendable (Bool) async throws -> Value? +public struct AsyncExpression { + internal let _expression: @Sendable (Bool) async throws -> sending Value? internal let _withoutCaching: Bool public let location: SourceLocation public let isClosure: Bool @@ -60,7 +60,7 @@ public struct AsyncExpression: Sendable { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: @escaping @Sendable () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: sending @escaping @Sendable () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -112,7 +112,7 @@ public struct AsyncExpression: Sendable { /// /// - Parameter block: The block that can cast the current Expression value to a /// new type. - public func cast(_ block: @escaping @Sendable (Value?) throws -> U?) -> AsyncExpression { + public func cast(_ block: @escaping @Sendable (Value?) throws -> sending U?) -> AsyncExpression { AsyncExpression( expression: ({ try await block(self.evaluate()) }), location: self.location, From 3abba9dc41d9001a58ce2ab280246bdce8c0be97 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 13:09:09 -0700 Subject: [PATCH 13/25] Reimplement MemoizedClosure to avoid the actor reentrant problem --- Sources/Nimble/AsyncExpression.swift | 67 +++++++++++++++++++++------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index 7eeba260..af2b0bff 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -1,24 +1,63 @@ -private actor MemoizedClosure { - var closure: () async throws -> sending T - var cache: T? +/// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure +private final class MemoizedClosure: Sendable { + enum State { + case notStarted + case inProgress + case finished(Result) + } + + private let lock = NSRecursiveLock() + nonisolated(unsafe) private var _state = State.notStarted + nonisolated(unsafe) private var _continuations = [CheckedContinuation]() + nonisolated(unsafe) private var _task: Task? + + nonisolated(unsafe) let closure: () async throws -> sending T init(_ closure: @escaping () async throws -> sending T) { self.closure = closure } - func set(_ cache: T) -> T { - self.cache = cache - return cache + deinit { + _task?.cancel() } - func call(_ withoutCaching: Bool) async throws -> sending T { + @Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> sending T { if withoutCaching { - return try await closure() - } - if let cache { - return cache + try await closure() } else { - return set(try await closure()) + try await withCheckedThrowingContinuation { continuation in + lock.withLock { + switch _state { + case .notStarted: + _state = .inProgress + _task = Task { [weak self] in + guard let self else { return } + do { + let value = try await self.closure() + self.handle(.success(value)) + } catch { + self.handle(.failure(error)) + } + } + _continuations.append(continuation) + case .inProgress: + _continuations.append(continuation) + case .finished(let result): + continuation.resume(with: result) + } + } + } + } + } + + private func handle(_ result: Result) { + lock.withLock { + _state = .finished(result) + for continuation in _continuations { + continuation.resume(with: result) + } + _continuations = [] + _task = nil } } } @@ -27,9 +66,7 @@ private actor MemoizedClosure { // closure once; even if repeat calls to the returned closure private func memoizedClosure(_ closure: sending @escaping () async throws -> sending T) -> @Sendable (Bool) async throws -> sending T { let memoized = MemoizedClosure(closure) - return { withoutCaching in - try await memoized.call(withoutCaching) - } + return memoized.callAsFunction(_:) } /// Expression represents the closure of the value inside expect(...). From be285e23e6814bb0cd473879aa88a3f607016d1f Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 13:12:10 -0700 Subject: [PATCH 14/25] Mark the closures in AsyncExpression as sending --- Sources/Nimble/AsyncExpression.swift | 6 +++--- Sources/Nimble/Requirement.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index af2b0bff..8ceb2f12 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -97,7 +97,7 @@ public struct AsyncExpression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: sending @escaping @Sendable () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: sending @escaping @Sendable () async throws -> sending Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -118,7 +118,7 @@ public struct AsyncExpression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(memoizedExpression: @escaping @Sendable (Bool) async throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { + public init(memoizedExpression: @escaping @Sendable (Bool) async throws -> sending Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { self._expression = memoizedExpression self.location = location self._withoutCaching = withoutCaching @@ -157,7 +157,7 @@ public struct AsyncExpression { ) } - public func cast(_ block: @escaping @Sendable (Value?) async throws -> U?) -> AsyncExpression { + public func cast(_ block: @escaping @Sendable (Value?) async throws -> sending U?) -> AsyncExpression { AsyncExpression( expression: ({ try await block(self.evaluate()) }), location: self.location, diff --git a/Sources/Nimble/Requirement.swift b/Sources/Nimble/Requirement.swift index d03cdcc3..6f70d224 100644 --- a/Sources/Nimble/Requirement.swift +++ b/Sources/Nimble/Requirement.swift @@ -57,7 +57,7 @@ internal func executeRequire(_ expression: AsyncExpression, _ style: Expec msg.userDescription = description msg.to = to do { - let cachedExpression = expression.withCaching() + let cachedExpression = await expression.withCaching() let result = try await matcher.satisfies(cachedExpression) let value = try await cachedExpression.evaluate() result.message.update(failureMessage: msg) From 49c831689e2fe44467acc6463690a7c454f145fe Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 13:14:52 -0700 Subject: [PATCH 15/25] Update the PostNotification matcher to be sendable --- .../Nimble/Matchers/PostNotification.swift | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Sources/Nimble/Matchers/PostNotification.swift b/Sources/Nimble/Matchers/PostNotification.swift index fd00d1a2..4bbeeff7 100644 --- a/Sources/Nimble/Matchers/PostNotification.swift +++ b/Sources/Nimble/Matchers/PostNotification.swift @@ -3,12 +3,13 @@ #if canImport(Foundation) import Foundation -internal class NotificationCollector { - private(set) var observedNotifications: [Notification] - private(set) var observedNotificationDescriptions: [String] +final class NotificationCollector: Sendable { + nonisolated(unsafe) private(set) var observedNotifications: [Notification] + nonisolated(unsafe) private(set) var observedNotificationDescriptions: [String] private let notificationCenter: NotificationCenter private let names: Set - private var tokens: [NSObjectProtocol] + nonisolated(unsafe) private var tokens: [NSObjectProtocol] + private let lock = NSRecursiveLock() required init(notificationCenter: NotificationCenter, names: Set = []) { self.notificationCenter = notificationCenter @@ -22,11 +23,21 @@ internal class NotificationCollector { func addObserver(forName name: Notification.Name?) -> NSObjectProtocol { return notificationCenter.addObserver(forName: name, object: nil, queue: nil) { [weak self] notification in // linux-swift gets confused by .append(n) - self?.observedNotifications.append(notification) - self?.observedNotificationDescriptions.append(stringify(notification)) + guard let self else { return } + + self.lock.lock() + defer { + self.lock.unlock() + } + self.observedNotifications.append(notification) + self.observedNotificationDescriptions.append(stringify(notification)) } } + lock.lock() + defer { + lock.unlock() + } if names.isEmpty { tokens.append(addObserver(forName: nil)) } else { @@ -44,13 +55,13 @@ internal class NotificationCollector { } #if !os(Windows) -private let mainThread = pthread_self() +nonisolated(unsafe) private let mainThread = pthread_self() #else private let mainThread = Thread.mainThread #endif -private final class OnlyOnceChecker: @unchecked Sendable { - var hasRun = false +private final class OnlyOnceChecker: Sendable { + nonisolated(unsafe) var hasRun = false let lock = NSRecursiveLock() func runOnlyOnce(_ closure: @Sendable () throws -> Void) rethrows { From 01b1ed5d90113b741b64ddd0a8f5421dfc9fc299 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 13:16:12 -0700 Subject: [PATCH 16/25] Update the BeResult matchers to take in sendable closures --- Sources/Nimble/Matchers/BeResult.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Nimble/Matchers/BeResult.swift b/Sources/Nimble/Matchers/BeResult.swift index 6c445766..88f4473b 100644 --- a/Sources/Nimble/Matchers/BeResult.swift +++ b/Sources/Nimble/Matchers/BeResult.swift @@ -5,7 +5,7 @@ import Foundation /// You can pass a closure to do any arbitrary custom matching to the value inside result. /// The closure only gets called when the result is success. public func beSuccess( - test: ((Success) -> Void)? = nil + test: (@Sendable (Success) -> Void)? = nil ) -> Matcher> { return Matcher.define { expression in var rawMessage = "be " @@ -92,7 +92,7 @@ public func beSuccess( /// You can pass a closure to do any arbitrary custom matching to the error inside result. /// The closure only gets called when the result is failure. public func beFailure( - test: ((Failure) -> Void)? = nil + test: (@Sendable (Failure) -> Void)? = nil ) -> Matcher> { return Matcher.define { expression in var rawMessage = "be " From 5cd0493f75b2f0027175830ecc67fd8806955187 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 13:17:04 -0700 Subject: [PATCH 17/25] Update the Map matchers to take in sendable closures --- Sources/Nimble/Matchers/Map.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Nimble/Matchers/Map.swift b/Sources/Nimble/Matchers/Map.swift index 7132ab3c..f34c86e2 100644 --- a/Sources/Nimble/Matchers/Map.swift +++ b/Sources/Nimble/Matchers/Map.swift @@ -3,7 +3,7 @@ /// For example, you might only care that a particular property on a method equals some other value. /// So, you could write `expect(myObject).to(lens(\.someIntValue, equal(3))`. /// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object. -public func map(_ transform: @escaping (T) throws -> U, _ matcher: Matcher) -> Matcher { +public func map(_ transform: @escaping @Sendable (T) throws -> U, _ matcher: Matcher) -> Matcher { Matcher { (received: Expression) in try matcher.satisfies(received.cast { value in guard let value else { return nil } @@ -17,7 +17,7 @@ public func map(_ transform: @escaping (T) throws -> U, _ matcher: Matcher /// For example, you might only care that a particular property on a method equals some other value. /// So, you could write `expect(myObject).to(lens(\.someIntValue, equal(3))`. /// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object. -public func map(_ transform: @escaping (T) async throws -> U, _ matcher: some AsyncableMatcher) -> AsyncMatcher { +public func map(_ transform: @escaping @Sendable (T) async throws -> U, _ matcher: some AsyncableMatcher) -> AsyncMatcher { AsyncMatcher { (received: AsyncExpression) in try await matcher.satisfies(received.cast { value in guard let value else { return nil } From b5044036beaba9dbdd226ad588649b0990b003e5 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 16:05:33 -0700 Subject: [PATCH 18/25] Update the waitUntil DSL to be sendable... ish Still more to do here. Will likely to require further refactoring --- Sources/Nimble/AsyncExpression.swift | 2 ++ Sources/Nimble/DSL+AsyncAwait.swift | 8 ++--- Sources/Nimble/DSL+Wait.swift | 13 ++++--- Sources/Nimble/Utils/AsyncAwait.swift | 4 +-- Sources/Nimble/Utils/PollAwait.swift | 51 ++++++++++++++------------- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index 8ceb2f12..40f0e5e7 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -1,3 +1,5 @@ +import Foundation + /// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure private final class MemoizedClosure: Sendable { enum State { diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index d9dba629..d90b16b0 100644 --- a/Sources/Nimble/DSL+AsyncAwait.swift +++ b/Sources/Nimble/DSL+AsyncAwait.swift @@ -93,7 +93,7 @@ public func waitUntil( file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: @escaping (@escaping () -> Void) async -> Void + action: sending @escaping (@escaping @Sendable () -> Void) async -> Void ) async { await throwableUntil( timeout: timeout, @@ -116,7 +116,7 @@ public func waitUntil( file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: @escaping (@escaping () -> Void) -> Void + action: sending @escaping (@escaping @Sendable () -> Void) -> Void ) async { await throwableUntil( timeout: timeout, @@ -134,12 +134,12 @@ private enum ErrorResult { private func throwableUntil( timeout: NimbleTimeInterval, sourceLocation: SourceLocation, - action: @escaping (@escaping () -> Void) async throws -> Void) async { + action: sending @escaping (@escaping @Sendable () -> Void) async throws -> Void) async { let leeway = timeout.divided let result = await performBlock( timeoutInterval: timeout, leeway: leeway, - sourceLocation: sourceLocation) { @MainActor (done: @escaping (ErrorResult) -> Void) async throws -> Void in + sourceLocation: sourceLocation) { @MainActor (done: @escaping @Sendable (ErrorResult) -> Void) async throws -> Void in do { try await action { done(.none) diff --git a/Sources/Nimble/DSL+Wait.swift b/Sources/Nimble/DSL+Wait.swift index 8995bc96..911cbc9a 100644 --- a/Sources/Nimble/DSL+Wait.swift +++ b/Sources/Nimble/DSL+Wait.swift @@ -23,7 +23,7 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: @escaping (@escaping () -> Void) -> Void) { + action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { // Convert TimeInterval to NimbleTimeInterval until(timeout: timeout.nimbleInterval, file: file, line: line, action: action) } @@ -35,7 +35,7 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: @escaping (@escaping () -> Void) -> Void) { + action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { return throwableUntil(timeout: timeout, file: file, line: line) { done in action(done) } @@ -48,10 +48,10 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: @escaping (@escaping () -> Void) throws -> Void) { + action: sending @escaping (@escaping @Sendable () -> Void) throws -> Void) { let awaiter = NimbleEnvironment.activeInstance.awaiter let leeway = timeout.divided - let result = awaiter.performBlock(file: file, line: line) { (done: @escaping (ErrorResult) -> Void) throws -> Void in + let result = awaiter.performBlock(file: file, line: line) { (done: @escaping @Sendable (ErrorResult) -> Void) throws -> Void in DispatchQueue.main.async { let capture = NMBExceptionCapture( handler: ({ exception in @@ -110,8 +110,7 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: @escaping (@escaping () -> Void) -> Void) { - until(timeout: .seconds(1), fileID: fileID, file: file, line: line, column: column, action: action) + action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { } #else public class func until( @@ -138,7 +137,7 @@ internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTime /// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function /// is executing. Any attempts to touch the run loop may cause non-deterministic behavior. @available(*, noasync, message: "the sync variant of `waitUntil` does not work in async contexts. Use the async variant as a drop-in replacement") -public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) { +public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { NMBWait.until(timeout: timeout, fileID: fileID, file: file, line: line, column: column, action: action) } diff --git a/Sources/Nimble/Utils/AsyncAwait.swift b/Sources/Nimble/Utils/AsyncAwait.swift index 22cf94ca..cd4dd9d5 100644 --- a/Sources/Nimble/Utils/AsyncAwait.swift +++ b/Sources/Nimble/Utils/AsyncAwait.swift @@ -259,7 +259,7 @@ private func runAwaitTrigger( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, sourceLocation: SourceLocation, - _ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void + _ closure: @escaping (@escaping @Sendable (T) -> Void) async throws -> Void ) async -> AsyncPollResult { let timeoutQueue = awaiter.timeoutQueue let completionCount = Box(value: 0) @@ -315,7 +315,7 @@ internal func performBlock( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, sourceLocation: SourceLocation, - _ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void + _ closure: @escaping (@escaping @Sendable (T) -> Void) async throws -> Void ) async -> AsyncPollResult { await runAwaitTrigger( awaiter: NimbleEnvironment.activeInstance.awaiter, diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index 1bc1311b..76f31530 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -64,7 +64,7 @@ internal final class AssertionWaitLock: WaitLock, @unchecked Sendable { } } -internal enum PollResult { +internal enum PollResult: Sendable { /// Incomplete indicates None (aka - this value hasn't been fulfilled yet) case incomplete /// TimedOut indicates the result reached its defined timeout limit before returning @@ -104,9 +104,9 @@ internal enum PollStatus { /// Holds the resulting value from an asynchronous expectation. /// This class is thread-safe at receiving a "response" to this promise. -internal final class AwaitPromise { - private(set) internal var asyncResult: PollResult = .incomplete - private var signal: DispatchSemaphore +internal final class AwaitPromise: Sendable { + nonisolated(unsafe) private(set) internal var asyncResult: PollResult = .incomplete + private let signal: DispatchSemaphore init() { signal = DispatchSemaphore(value: 1) @@ -142,7 +142,7 @@ internal struct PollAwaitTrigger { /// /// This factory stores all the state for an async expectation so that Await doesn't /// doesn't have to manage it. -internal class AwaitPromiseBuilder { +internal class AwaitPromiseBuilder { let awaiter: Awaiter let waitLock: WaitLock let trigger: PollAwaitTrigger @@ -313,36 +313,39 @@ internal class Awaiter { return DispatchSource.makeTimerSource(flags: .strict, queue: queue) } - func performBlock( + func performBlock( file: FileString, line: UInt, - _ closure: @escaping (@escaping (T) -> Void) throws -> Void + _ closure: sending @escaping (@escaping @Sendable (T) -> Void) throws -> Void ) -> AwaitPromiseBuilder { let promise = AwaitPromise() let timeoutSource = createTimerSource(timeoutQueue) - var completionCount = 0 + nonisolated(unsafe) var completionCount = 0 + let lock = NSRecursiveLock() let trigger = PollAwaitTrigger(timeoutSource: timeoutSource, actionSource: nil) { try closure { result in - completionCount += 1 - if completionCount < 2 { - func completeBlock() { - if promise.resolveResult(.completed(result)) { - #if canImport(CoreFoundation) - CFRunLoopStop(CFRunLoopGetMain()) - #else - RunLoop.main._stop() - #endif + lock.withLock { + completionCount += 1 + if completionCount < 2 { + @Sendable func completeBlock() { + if promise.resolveResult(.completed(result)) { +#if canImport(CoreFoundation) + CFRunLoopStop(CFRunLoopGetMain()) +#else + RunLoop.main._stop() +#endif + } } - } - if Thread.isMainThread { - completeBlock() + if Thread.isMainThread { + completeBlock() + } else { + DispatchQueue.main.async { completeBlock() } + } } else { - DispatchQueue.main.async { completeBlock() } + fail("waitUntil(..) expects its completion closure to be only called once", + file: file, line: line) } - } else { - fail("waitUntil(..) expects its completion closure to be only called once", - file: file, line: line) } } } From 599568bb547179628e29d48ca275e14b6a42f819 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 18 Jun 2024 16:06:41 -0700 Subject: [PATCH 19/25] Make NMBMatcher unchecked sendable There are 2 subclasses which are also sendable/safe to use. And no one else can make subclasses outside of Nimble, so this is tolerable --- Sources/Nimble/Matchers/Matcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nimble/Matchers/Matcher.swift b/Sources/Nimble/Matchers/Matcher.swift index 376be7a8..cb994d56 100644 --- a/Sources/Nimble/Matchers/Matcher.swift +++ b/Sources/Nimble/Matchers/Matcher.swift @@ -215,7 +215,7 @@ public typealias MatcherBlock = @Sendable (_ actualExpression: Expression Date: Tue, 18 Jun 2024 16:16:47 -0700 Subject: [PATCH 20/25] Mork work on getting the async-version of polling expectations to be compatible with swift 6 --- Sources/Nimble/AsyncExpression.swift | 2 +- Sources/Nimble/Polling+AsyncAwait.swift | 110 ++++++++++++++++++++---- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index 40f0e5e7..8a32f26d 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -167,7 +167,7 @@ public struct AsyncExpression { ) } - public func evaluate() async throws -> Value? { + public func evaluate() async throws -> sending Value? { try await self._expression(_withoutCaching) } diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index a351fac6..59bf4c8c 100644 --- a/Sources/Nimble/Polling+AsyncAwait.swift +++ b/Sources/Nimble/Polling+AsyncAwait.swift @@ -4,7 +4,13 @@ import Dispatch @MainActor -internal func execute(_ expression: AsyncExpression, style: ExpectationStyle, to: String, description: String?, matcherExecutor: () async throws -> MatcherResult) async -> (Bool, FailureMessage) { +internal func execute( + _ expression: AsyncExpression, + style: ExpectationStyle, + to: String, + description: String?, + matcherExecutor: @Sendable () async throws -> MatcherResult +) async -> (Bool, FailureMessage) { let msg = FailureMessage() msg.userDescription = description msg.to = to @@ -33,7 +39,7 @@ internal actor Poller { timeout: NimbleTimeInterval, poll: NimbleTimeInterval, fnName: String, - matcherRunner: @escaping () async throws -> MatcherResult) async -> MatcherResult { + matcherRunner: @escaping @Sendable () async throws -> MatcherResult) async -> MatcherResult { let fnName = "expect(...).\(fnName)(...)" let result = await pollBlock( pollInterval: poll, @@ -71,7 +77,7 @@ internal func poll( timeout: NimbleTimeInterval, poll: NimbleTimeInterval, fnName: String, - matcherRunner: @escaping () async throws -> MatcherResult + matcherRunner: @escaping @Sendable () async throws -> MatcherResult ) async -> MatcherResult { let poller = Poller() return await poller.poll( @@ -90,7 +96,12 @@ extension SyncExpectation { /// Tests the actual value using a matcher to match by checking continuously /// at each pollInterval until the timeout is reached. @discardableResult - public func toEventually(_ matcher: Matcher, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toEventually( + _ matcher: Matcher, + timeout: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -116,7 +127,12 @@ extension SyncExpectation { /// Tests the actual value using a matcher to not match by checking /// continuously at each pollInterval until the timeout is reached. @discardableResult - public func toEventuallyNot(_ matcher: Matcher, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toEventuallyNot( + _ matcher: Matcher, + timeout: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -144,14 +160,24 @@ extension SyncExpectation { /// /// Alias of toEventuallyNot() @discardableResult - public func toNotEventually(_ matcher: Matcher, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toNotEventually( + _ matcher: Matcher, + timeout: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { return await toEventuallyNot(matcher, timeout: timeout, pollInterval: pollInterval, description: description) } /// Tests the actual value using a matcher to never match by checking /// continuously at each pollInterval until the timeout is reached. @discardableResult - public func toNever(_ matcher: Matcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toNever( + _ matcher: Matcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -178,14 +204,24 @@ extension SyncExpectation { /// /// Alias of toNever() @discardableResult - public func neverTo(_ matcher: Matcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func neverTo( + _ matcher: Matcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { return await toNever(matcher, until: until, pollInterval: pollInterval, description: description) } /// Tests the actual value using a matcher to always match by checking /// continusouly at each pollInterval until the timeout is reached @discardableResult - public func toAlways(_ matcher: Matcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toAlways( + _ matcher: Matcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -212,7 +248,12 @@ extension SyncExpectation { /// /// Alias of toAlways() @discardableResult - public func alwaysTo(_ matcher: Matcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func alwaysTo( + _ matcher: Matcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { return await toAlways(matcher, until: until, pollInterval: pollInterval, description: description) } @@ -220,7 +261,12 @@ extension SyncExpectation { /// Tests the actual value using a matcher to match by checking continuously /// at each pollInterval until the timeout is reached. @discardableResult - public func toEventually(_ matcher: AsyncMatcher, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toEventually( + _ matcher: AsyncMatcher, + timeout: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -246,7 +292,12 @@ extension SyncExpectation { /// Tests the actual value using a matcher to not match by checking /// continuously at each pollInterval until the timeout is reached. @discardableResult - public func toEventuallyNot(_ matcher: AsyncMatcher, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toEventuallyNot( + _ matcher: AsyncMatcher, + timeout: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -274,14 +325,24 @@ extension SyncExpectation { /// /// Alias of toEventuallyNot() @discardableResult - public func toNotEventually(_ matcher: AsyncMatcher, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toNotEventually( + _ matcher: AsyncMatcher, + timeout: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { return await toEventuallyNot(matcher, timeout: timeout, pollInterval: pollInterval, description: description) } /// Tests the actual value using a matcher to never match by checking /// continuously at each pollInterval until the timeout is reached. @discardableResult - public func toNever(_ matcher: AsyncMatcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toNever( + _ matcher: AsyncMatcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -308,14 +369,24 @@ extension SyncExpectation { /// /// Alias of toNever() @discardableResult - public func neverTo(_ matcher: AsyncMatcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func neverTo( + _ matcher: AsyncMatcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { return await toNever(matcher, until: until, pollInterval: pollInterval, description: description) } /// Tests the actual value using a matcher to always match by checking /// continusouly at each pollInterval until the timeout is reached @discardableResult - public func toAlways(_ matcher: AsyncMatcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func toAlways( + _ matcher: AsyncMatcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { nimblePrecondition(expression.isClosure, "NimbleInternalError", toEventuallyRequiresClosureError.stringValue) let asyncExpression = expression.toAsyncExpression() @@ -342,7 +413,12 @@ extension SyncExpectation { /// /// Alias of toAlways() @discardableResult - public func alwaysTo(_ matcher: AsyncMatcher, until: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil) async -> Self { + public func alwaysTo( + _ matcher: AsyncMatcher, + until: NimbleTimeInterval = PollingDefaults.timeout, + pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, + description: String? = nil + ) async -> Self { return await toAlways(matcher, until: until, pollInterval: pollInterval, description: description) } } From 322f9beb0193eda293a2cc593563793431e070df Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 17 Jul 2024 21:12:59 -0700 Subject: [PATCH 21/25] Eliminate concurrency warnings in polling expectations --- Sources/Nimble/Adapters/NMBExpectation.swift | 29 ++++++++++--- Sources/Nimble/AsyncExpression.swift | 14 +++--- Sources/Nimble/DSL+AsyncAwait.swift | 6 +-- Sources/Nimble/DSL+Require.swift | 24 +++++------ Sources/Nimble/DSL+Wait.swift | 29 +++++++++---- Sources/Nimble/DSL.swift | 16 +++---- Sources/Nimble/Expectation.swift | 2 +- Sources/Nimble/Expression.swift | 43 +++++++++++++------ Sources/Nimble/Matchers/AllPass.swift | 4 +- Sources/Nimble/Matchers/AsyncAllPass.swift | 8 ++-- Sources/Nimble/Matchers/BeEmpty.swift | 4 +- Sources/Nimble/Matchers/Equal+Tuple.swift | 10 ++--- .../Nimble/Matchers/Equal+TupleArray.swift | 12 +++--- Sources/Nimble/Matchers/Equal.swift | 2 +- Sources/Nimble/Matchers/Match.swift | 3 +- Sources/Nimble/Matchers/Matcher.swift | 4 +- Sources/Nimble/Matchers/RaisesException.swift | 2 +- Sources/Nimble/Matchers/SatisfyAllOf.swift | 2 +- Sources/Nimble/Matchers/SatisfyAnyOf.swift | 2 +- Sources/Nimble/Matchers/ThrowError.swift | 10 ++--- Sources/Nimble/Polling+AsyncAwait.swift | 8 ++-- Sources/Nimble/Polling+Require.swift | 8 ++-- Sources/Nimble/Polling.swift | 4 +- Sources/Nimble/Requirement.swift | 2 +- Sources/Nimble/Utils/AsyncAwait.swift | 6 +-- Sources/Nimble/Utils/PollAwait.swift | 2 +- 26 files changed, 151 insertions(+), 105 deletions(-) diff --git a/Sources/Nimble/Adapters/NMBExpectation.swift b/Sources/Nimble/Adapters/NMBExpectation.swift index f8d2d692..4279c778 100644 --- a/Sources/Nimble/Adapters/NMBExpectation.swift +++ b/Sources/Nimble/Adapters/NMBExpectation.swift @@ -13,18 +13,27 @@ private func from(objcMatcher: NMBMatcher) -> Matcher { } // Equivalent to Expectation, but for Nimble's Objective-C interface -public class NMBExpectation: NSObject { - internal let _actualBlock: () -> NSObject? - internal var _negative: Bool +public final class NMBExpectation: NSObject, Sendable { + internal let _actualBlock: @Sendable () -> NSObject? + internal let _negative: Bool internal let _file: FileString internal let _line: UInt - internal var _timeout: NimbleTimeInterval = .seconds(1) + internal let _timeout: NimbleTimeInterval - @objc public init(actualBlock: @escaping () -> NSObject?, negative: Bool, file: FileString, line: UInt) { + @objc public init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt) { self._actualBlock = actualBlock self._negative = negative self._file = file self._line = line + self._timeout = .seconds(1) + } + + private init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt, timeout: NimbleTimeInterval) { + self._actualBlock = actualBlock + self._negative = negative + self._file = file + self._line = line + self._timeout = timeout } private var expectValue: SyncExpectation { @@ -32,8 +41,14 @@ public class NMBExpectation: NSObject { } @objc public var withTimeout: (TimeInterval) -> NMBExpectation { - return { timeout in self._timeout = timeout.nimbleInterval - return self + return { timeout in + NMBExpectation( + actualBlock: self._actualBlock, + negative: self._negative, + file: self._file, + line: self._line, + timeout: timeout.nimbleInterval + ) } } diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index 8a32f26d..7eb0d228 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -1,7 +1,7 @@ import Foundation /// Memoizes the given closure, only calling the passed closure once; even if repeat calls to the returned closure -private final class MemoizedClosure: Sendable { +private final class MemoizedClosure: Sendable { enum State { case notStarted case inProgress @@ -13,9 +13,9 @@ private final class MemoizedClosure: Sendable { nonisolated(unsafe) private var _continuations = [CheckedContinuation]() nonisolated(unsafe) private var _task: Task? - nonisolated(unsafe) let closure: () async throws -> sending T + let closure: @Sendable () async throws -> T - init(_ closure: @escaping () async throws -> sending T) { + init(_ closure: @escaping @Sendable () async throws -> T) { self.closure = closure } @@ -23,7 +23,7 @@ private final class MemoizedClosure: Sendable { _task?.cancel() } - @Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> sending T { + @Sendable func callAsFunction(_ withoutCaching: Bool) async throws -> T { if withoutCaching { try await closure() } else { @@ -66,7 +66,9 @@ private final class MemoizedClosure: Sendable { // Memoizes the given closure, only calling the passed // closure once; even if repeat calls to the returned closure -private func memoizedClosure(_ closure: sending @escaping () async throws -> sending T) -> @Sendable (Bool) async throws -> sending T { +private func memoizedClosure( + _ closure: sending @escaping @Sendable () async throws -> T +) -> @Sendable (Bool) async throws -> T { let memoized = MemoizedClosure(closure) return memoized.callAsFunction(_:) } @@ -82,7 +84,7 @@ private func memoizedClosure(_ closure: sending @escaping () 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 { +public actor AsyncExpression { internal let _expression: @Sendable (Bool) async throws -> sending Value? internal let _withoutCaching: Bool public let location: SourceLocation diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index d90b16b0..c7deecc9 100644 --- a/Sources/Nimble/DSL+AsyncAwait.swift +++ b/Sources/Nimble/DSL+AsyncAwait.swift @@ -93,7 +93,7 @@ public func waitUntil( file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: sending @escaping (@escaping @Sendable () -> Void) async -> Void + action: @escaping @Sendable (@escaping @Sendable () -> Void) async -> Void ) async { await throwableUntil( timeout: timeout, @@ -116,7 +116,7 @@ public func waitUntil( file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: sending @escaping (@escaping @Sendable () -> Void) -> Void + action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void ) async { await throwableUntil( timeout: timeout, @@ -134,7 +134,7 @@ private enum ErrorResult { private func throwableUntil( timeout: NimbleTimeInterval, sourceLocation: SourceLocation, - action: sending @escaping (@escaping @Sendable () -> Void) async throws -> Void) async { + action: @escaping @Sendable (@escaping @Sendable () -> Void) async throws -> Void) async { let leeway = timeout.divided let result = await performBlock( timeoutInterval: timeout, diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index c864e206..40832be6 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -3,7 +3,7 @@ /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, @@ -17,7 +17,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -31,7 +31,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -45,7 +45,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -61,7 +61,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, line /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, @@ -77,7 +77,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -93,7 +93,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -109,7 +109,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -216,7 +216,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -226,7 +226,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -236,7 +236,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping () throws -> sending T?) throws -> T { +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -246,7 +246,7 @@ public func unwraps(fileID: String = #fileID, file: FileString = #filePath, l /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> sending (() throws -> sending T?)) throws -> T { +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } diff --git a/Sources/Nimble/DSL+Wait.swift b/Sources/Nimble/DSL+Wait.swift index 911cbc9a..885b71d9 100644 --- a/Sources/Nimble/DSL+Wait.swift +++ b/Sources/Nimble/DSL+Wait.swift @@ -23,7 +23,7 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { + action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) { // Convert TimeInterval to NimbleTimeInterval until(timeout: timeout.nimbleInterval, file: file, line: line, action: action) } @@ -35,7 +35,7 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { + action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) { return throwableUntil(timeout: timeout, file: file, line: line) { done in action(done) } @@ -48,9 +48,10 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: sending @escaping (@escaping @Sendable () -> Void) throws -> Void) { + action: @escaping @Sendable (@escaping @Sendable () -> Void) throws -> Void) { let awaiter = NimbleEnvironment.activeInstance.awaiter let leeway = timeout.divided + let location = SourceLocation(fileID: fileID, filePath: file, line: line, column: column) let result = awaiter.performBlock(file: file, line: line) { (done: @escaping @Sendable (ErrorResult) -> Void) throws -> Void in DispatchQueue.main.async { let capture = NMBExceptionCapture( @@ -69,10 +70,12 @@ public class NMBWait: NSObject { } } } - }.timeout(timeout, forcefullyAbortTimeout: leeway).wait( - "waitUntil(...)", - sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column) - ) + } + .timeout(timeout, forcefullyAbortTimeout: leeway) + .wait( + "waitUntil(...)", + sourceLocation: location + ) switch result { case .incomplete: internalError("Reached .incomplete state for waitUntil(...).") @@ -110,7 +113,8 @@ public class NMBWait: NSObject { file: FileString = #filePath, line: UInt = #line, column: UInt = #column, - action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { + action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void) { + until(timeout: .seconds(1), fileID: fileID, file: file, line: line, column: column, action: action) } #else public class func until( @@ -137,7 +141,14 @@ internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTime /// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function /// is executing. Any attempts to touch the run loop may cause non-deterministic behavior. @available(*, noasync, message: "the sync variant of `waitUntil` does not work in async contexts. Use the async variant as a drop-in replacement") -public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, action: sending @escaping (@escaping @Sendable () -> Void) -> Void) { +public func waitUntil( + timeout: NimbleTimeInterval = PollingDefaults.timeout, + fileID: String = #fileID, + file: FileString = #filePath, + line: UInt = #line, + column: UInt = #column, + action: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void +) { NMBWait.until(timeout: timeout, fileID: fileID, file: file, line: line, column: column, action: action) } diff --git a/Sources/Nimble/DSL.swift b/Sources/Nimble/DSL.swift index 1d6011b6..c6261a5f 100644 --- a/Sources/Nimble/DSL.swift +++ b/Sources/Nimble/DSL.swift @@ -1,5 +1,5 @@ /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, @@ -8,7 +8,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -17,7 +17,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -26,7 +26,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -36,7 +36,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> sending T?) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, @@ -46,7 +46,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -56,7 +56,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending T?)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -66,7 +66,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> sending (() throws -> sending Void)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), diff --git a/Sources/Nimble/Expectation.swift b/Sources/Nimble/Expectation.swift index 7363483b..e001a2a2 100644 --- a/Sources/Nimble/Expectation.swift +++ b/Sources/Nimble/Expectation.swift @@ -150,7 +150,7 @@ extension Expectation { } } -public struct SyncExpectation: Expectation { +public struct SyncExpectation: Expectation, Sendable { public let expression: Expression /// The status of the test after matchers have been evaluated. diff --git a/Sources/Nimble/Expression.swift b/Sources/Nimble/Expression.swift index be6d8d01..c41c7b5e 100644 --- a/Sources/Nimble/Expression.swift +++ b/Sources/Nimble/Expression.swift @@ -1,15 +1,30 @@ -// Memoizes the given closure, only calling the passed -// closure once; even if repeat calls to the returned closure -private func memoizedClosure(_ closure: @escaping () throws -> T) -> (Bool) throws -> T { - var cache: T? - return { withoutCaching in - if withoutCaching || cache == nil { - cache = try closure() +import Foundation + +private final class MemoizedValue: Sendable { + private let lock = NSRecursiveLock() + nonisolated(unsafe) private var cache: T? = nil + private let closure: @Sendable () throws -> sending T + + init(_ closure: @escaping @Sendable () throws -> sending T) { + self.closure = closure + } + + @Sendable func evaluate(withoutCaching: Bool) throws -> sending T { + try lock.withLock { + if withoutCaching || cache == nil { + cache = try closure() + } + return cache! } - return cache! } } +// Memoizes the given closure, only calling the passed +// closure once; even if repeat calls to the returned closure +private func memoizedClosure(_ closure: @escaping @Sendable () throws -> sending T) -> @Sendable (Bool) throws -> sending T { + MemoizedValue(closure).evaluate(withoutCaching:) +} + /// Expression represents the closure of the value inside expect(...). /// Expressions are memoized by default. This makes them safe to call /// evaluate() multiple times without causing a re-evaluation of the underlying @@ -21,8 +36,8 @@ private func memoizedClosure(_ closure: @escaping () throws -> T) -> (Bool) t /// /// 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 Expression { - internal let _expression: (Bool) throws -> sending Value? +public struct Expression: Sendable { + internal let _expression: @Sendable (Bool) throws -> sending Value? internal let _withoutCaching: Bool public let location: SourceLocation public let isClosure: Bool @@ -38,7 +53,7 @@ public struct Expression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: @escaping () throws -> sending Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: @escaping @Sendable () throws -> sending Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -59,7 +74,7 @@ public struct Expression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(memoizedExpression: @escaping (Bool) throws -> sending Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { + public init(memoizedExpression: @escaping @Sendable (Bool) throws -> sending Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { self._expression = memoizedExpression self.location = location self._withoutCaching = withoutCaching @@ -74,7 +89,7 @@ public struct Expression { /// /// - Parameter block: The block that can cast the current Expression value to a /// new type. - public func cast(_ block: @escaping (Value?) throws -> sending U?) -> Expression { + public func cast(_ block: @escaping @Sendable (Value?) throws -> sending U?) -> Expression { Expression( expression: ({ try block(self.evaluate()) }), location: self.location, @@ -103,7 +118,9 @@ public struct Expression { isClosure: isClosure ) } +} +extension Expression where Value: Sendable { public func toAsyncExpression() -> AsyncExpression { AsyncExpression( memoizedExpression: { @MainActor memoize in try _expression(memoize) }, diff --git a/Sources/Nimble/Matchers/AllPass.swift b/Sources/Nimble/Matchers/AllPass.swift index 133c21ec..f1f6e2b5 100644 --- a/Sources/Nimble/Matchers/AllPass.swift +++ b/Sources/Nimble/Matchers/AllPass.swift @@ -1,5 +1,5 @@ public func allPass( - _ passFunc: @escaping (S.Element) throws -> Bool + _ passFunc: @escaping @Sendable (S.Element) throws -> Bool ) -> Matcher { let matcher = Matcher.define("pass a condition") { actualExpression, message in guard let actual = try actualExpression.evaluate() else { @@ -12,7 +12,7 @@ public func allPass( public func allPass( _ passName: String, - _ passFunc: @escaping (S.Element) throws -> Bool + _ passFunc: @escaping @Sendable (S.Element) throws -> Bool ) -> Matcher { let matcher = Matcher.define(passName) { actualExpression, message in guard let actual = try actualExpression.evaluate() else { diff --git a/Sources/Nimble/Matchers/AsyncAllPass.swift b/Sources/Nimble/Matchers/AsyncAllPass.swift index ec04f9eb..d12c38b2 100644 --- a/Sources/Nimble/Matchers/AsyncAllPass.swift +++ b/Sources/Nimble/Matchers/AsyncAllPass.swift @@ -1,6 +1,6 @@ public func allPass( - _ passFunc: @escaping (S.Element) async throws -> Bool -) -> AsyncMatcher { + _ passFunc: @escaping @Sendable (S.Element) async throws -> Bool +) -> AsyncMatcher where S.Element: Sendable { let matcher = AsyncMatcher.define("pass a condition") { actualExpression, message in guard let actual = try await actualExpression.evaluate() else { return MatcherResult(status: .fail, message: message) @@ -12,8 +12,8 @@ public func allPass( public func allPass( _ passName: String, - _ passFunc: @escaping (S.Element) async throws -> Bool -) -> AsyncMatcher { + _ passFunc: @escaping @Sendable (S.Element) async throws -> Bool +) -> AsyncMatcher where S.Element: Sendable { let matcher = AsyncMatcher.define(passName) { actualExpression, message in guard let actual = try await actualExpression.evaluate() else { return MatcherResult(status: .fail, message: message) diff --git a/Sources/Nimble/Matchers/BeEmpty.swift b/Sources/Nimble/Matchers/BeEmpty.swift index 571797ca..43e1ca79 100644 --- a/Sources/Nimble/Matchers/BeEmpty.swift +++ b/Sources/Nimble/Matchers/BeEmpty.swift @@ -85,10 +85,10 @@ extension NMBMatcher { let actualValue = try actualExpression.evaluate() if let value = actualValue as? NMBCollection { - let expr = Expression(expression: ({ value }), location: location) + let expr = Expression(expression: { value }, location: location) return try beEmpty().satisfies(expr).toObjectiveC() } else if let value = actualValue as? NSString { - let expr = Expression(expression: ({ value }), location: location) + let expr = Expression(expression: { value }, location: location) return try beEmpty().satisfies(expr).toObjectiveC() } else if let actualValue = actualValue { let badTypeErrorMsg = "be empty (only works for NSArrays, NSSets, NSIndexSets, NSDictionaries, NSHashTables, and NSStrings)" diff --git a/Sources/Nimble/Matchers/Equal+Tuple.swift b/Sources/Nimble/Matchers/Equal+Tuple.swift index 17b2f2e5..9abe3411 100644 --- a/Sources/Nimble/Matchers/Equal+Tuple.swift +++ b/Sources/Nimble/Matchers/Equal+Tuple.swift @@ -7,7 +7,7 @@ public func equal( _ expectedValue: (T1, T2)? ) -> Matcher<(T1, T2)> { - equal(expectedValue, by: ==) + equal(expectedValue, by: { $0 == $1 }) } public func == ( @@ -45,7 +45,7 @@ public func != ( public func equal( _ expectedValue: (T1, T2, T3)? ) -> Matcher<(T1, T2, T3)> { - equal(expectedValue, by: ==) + equal(expectedValue, by: { $0 == $1 }) } public func == ( @@ -85,7 +85,7 @@ public func != ( public func equal( _ expectedValue: (T1, T2, T3, T4)? ) -> Matcher<(T1, T2, T3, T4)> { - equal(expectedValue, by: ==) + equal(expectedValue, by: { $0 == $1 }) } public func == ( @@ -124,7 +124,7 @@ public func != ( public func equal( _ expectedValue: (T1, T2, T3, T4, T5)? ) -> Matcher<(T1, T2, T3, T4, T5)> { - equal(expectedValue, by: ==) + equal(expectedValue, by: { $0 == $1 }) } public func == ( @@ -164,7 +164,7 @@ public func != ( _ expectedValue: (T1, T2, T3, T4, T5, T6)? ) -> Matcher<(T1, T2, T3, T4, T5, T6)> { - equal(expectedValue, by: ==) + equal(expectedValue, by: { $0 == $1 }) } public func == ( diff --git a/Sources/Nimble/Matchers/Equal+TupleArray.swift b/Sources/Nimble/Matchers/Equal+TupleArray.swift index eff6168d..3139c112 100644 --- a/Sources/Nimble/Matchers/Equal+TupleArray.swift +++ b/Sources/Nimble/Matchers/Equal+TupleArray.swift @@ -7,7 +7,7 @@ public func equal( _ expectedValue: [(T1, T2)]? ) -> Matcher<[(T1, T2)]> { - equalTupleArray(expectedValue, by: ==) + equalTupleArray(expectedValue, by: { $0 == $1 }) } public func == ( @@ -45,7 +45,7 @@ public func != ( public func equal( _ expectedValue: [(T1, T2, T3)]? ) -> Matcher<[(T1, T2, T3)]> { - equalTupleArray(expectedValue, by: ==) + equalTupleArray(expectedValue, by: { $0 == $1 }) } public func == ( @@ -83,7 +83,7 @@ public func != ( public func equal( _ expectedValue: [(T1, T2, T3, T4)]? ) -> Matcher<[(T1, T2, T3, T4)]> { - equalTupleArray(expectedValue, by: ==) + equalTupleArray(expectedValue, by: { $0 == $1 }) } public func == ( @@ -121,7 +121,7 @@ public func != ( public func equal( _ expectedValue: [(T1, T2, T3, T4, T5)]? ) -> Matcher<[(T1, T2, T3, T4, T5)]> { - equalTupleArray(expectedValue, by: ==) + equalTupleArray(expectedValue, by: { $0 == $1 }) } public func == ( @@ -159,7 +159,7 @@ public func != ( _ expectedValue: [(T1, T2, T3, T4, T5, T6)]? ) -> Matcher<[(T1, T2, T3, T4, T5, T6)]> { - equalTupleArray(expectedValue, by: ==) + equalTupleArray(expectedValue, by: { $0 == $1 }) } public func == ( @@ -196,7 +196,7 @@ public func != ( _ expectedValue: [(Tuple)]?, - by areTuplesEquivalent: @escaping (Tuple, Tuple) -> Bool + by areTuplesEquivalent: @escaping @Sendable (Tuple, Tuple) -> Bool ) -> Matcher<[Tuple]> { equal(expectedValue) { $0.elementsEqual($1, by: areTuplesEquivalent) diff --git a/Sources/Nimble/Matchers/Equal.swift b/Sources/Nimble/Matchers/Equal.swift index 4ec21e37..1a5ce017 100644 --- a/Sources/Nimble/Matchers/Equal.swift +++ b/Sources/Nimble/Matchers/Equal.swift @@ -1,6 +1,6 @@ internal func equal( _ expectedValue: T?, - by areEquivalent: @escaping (T, T) -> Bool + by areEquivalent: @escaping @Sendable (T, T) -> Bool ) -> Matcher { Matcher.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in let actualValue = try actualExpression.evaluate() diff --git a/Sources/Nimble/Matchers/Match.swift b/Sources/Nimble/Matchers/Match.swift index b634ad31..1ed3f3c9 100644 --- a/Sources/Nimble/Matchers/Match.swift +++ b/Sources/Nimble/Matchers/Match.swift @@ -14,9 +14,10 @@ import class Foundation.NSString extension NMBMatcher { @objc public class func matchMatcher(_ expected: NSString) -> NMBMatcher { + let expected = String(expected) return NMBMatcher { actualExpression in let actual = actualExpression.cast { $0 as? String } - return try match(expected.description).satisfies(actual).toObjectiveC() + return try match(expected).satisfies(actual).toObjectiveC() } } } diff --git a/Sources/Nimble/Matchers/Matcher.swift b/Sources/Nimble/Matchers/Matcher.swift index cb994d56..ffb5798b 100644 --- a/Sources/Nimble/Matchers/Matcher.swift +++ b/Sources/Nimble/Matchers/Matcher.swift @@ -18,7 +18,7 @@ /// In the 2023 Apple Platform releases (macOS 14, iOS 17, watchOS 10, tvOS 17, visionOS 1), Apple /// renamed `NSMatcher` to `Matcher`. In response, we decided to rename `Matcher` to /// `Matcher`. -public struct Matcher { +public struct Matcher: Sendable { fileprivate let matcher: @Sendable (Expression) throws -> MatcherResult /// Constructs a matcher that knows how take a given value @@ -39,8 +39,6 @@ public struct Matcher { @available(*, deprecated, renamed: "Matcher") public typealias Predicate = Matcher -extension Matcher: Sendable where T: Sendable {} - /// Provides convenience helpers to defining matchers extension Matcher { /// Like Matcher() constructor, but automatically guard against nil (actual) values diff --git a/Sources/Nimble/Matchers/RaisesException.swift b/Sources/Nimble/Matchers/RaisesException.swift index 2bb94094..d1d9b464 100644 --- a/Sources/Nimble/Matchers/RaisesException.swift +++ b/Sources/Nimble/Matchers/RaisesException.swift @@ -141,7 +141,7 @@ internal func exceptionMatchesNonNilFieldsOrClosure( return matches } -public class NMBObjCRaiseExceptionMatcher: NMBMatcher { +public class NMBObjCRaiseExceptionMatcher: NMBMatcher, @unchecked Sendable { private let _name: String? private let _reason: String? private let _userInfo: NSDictionary? diff --git a/Sources/Nimble/Matchers/SatisfyAllOf.swift b/Sources/Nimble/Matchers/SatisfyAllOf.swift index 30f9045a..a9d55e35 100644 --- a/Sources/Nimble/Matchers/SatisfyAllOf.swift +++ b/Sources/Nimble/Matchers/SatisfyAllOf.swift @@ -57,7 +57,7 @@ public func satisfyAllOf(_ matchers: any AsyncableMatcher...) -> AsyncMatc @available(macOS 13.0.0, iOS 16.0.0, tvOS 16.0.0, watchOS 9.0.0, *) public func satisfyAllOf(_ matchers: [any AsyncableMatcher]) -> AsyncMatcher { return AsyncMatcher.define { actualExpression in - let cachedExpression = actualExpression.withCaching() + let cachedExpression = await actualExpression.withCaching() var postfixMessages = [String]() var status: MatcherStatus = .matches for matcher in matchers { diff --git a/Sources/Nimble/Matchers/SatisfyAnyOf.swift b/Sources/Nimble/Matchers/SatisfyAnyOf.swift index 56ffdd10..adb3ebbc 100644 --- a/Sources/Nimble/Matchers/SatisfyAnyOf.swift +++ b/Sources/Nimble/Matchers/SatisfyAnyOf.swift @@ -57,7 +57,7 @@ public func satisfyAnyOf(_ matchers: any AsyncableMatcher...) -> AsyncMatc @available(macOS 13.0.0, iOS 16.0.0, tvOS 16.0.0, watchOS 9.0.0, *) public func satisfyAnyOf(_ matchers: [any AsyncableMatcher]) -> AsyncMatcher { return AsyncMatcher.define { actualExpression in - let cachedExpression = actualExpression.withCaching() + let cachedExpression = await actualExpression.withCaching() var postfixMessages = [String]() var status: MatcherStatus = .doesNotMatch for matcher in matchers { diff --git a/Sources/Nimble/Matchers/ThrowError.swift b/Sources/Nimble/Matchers/ThrowError.swift index 32c2f6c1..818ea4eb 100644 --- a/Sources/Nimble/Matchers/ThrowError.swift +++ b/Sources/Nimble/Matchers/ThrowError.swift @@ -43,7 +43,7 @@ public func throwError() -> Matcher { /// /// nil arguments indicates that the matcher should not attempt to match against /// that parameter. -public func throwError(_ error: T, closure: ((Error) -> Void)? = nil) -> Matcher { +public func throwError(_ error: T, closure: (@Sendable (Error) -> Void)? = nil) -> Matcher { return Matcher { actualExpression in var actualError: Error? do { @@ -89,7 +89,7 @@ public func throwError(_ error: T, closure: ((Error) -> Void)? = /// /// nil arguments indicates that the matcher should not attempt to match against /// that parameter. -public func throwError(_ error: T, closure: ((T) -> Void)? = nil) -> Matcher { +public func throwError(_ error: T, closure: (@Sendable (T) -> Void)? = nil) -> Matcher { return Matcher { actualExpression in var actualError: Error? do { @@ -137,7 +137,7 @@ public func throwError(_ error: T, closure: ((T) -> V /// that parameter. public func throwError( errorType: T.Type, - closure: ((T) -> Void)? = nil + closure: (@Sendable (T) -> Void)? = nil ) -> Matcher { return Matcher { actualExpression in var actualError: Error? @@ -197,7 +197,7 @@ public func throwError( /// values of the existential type `Error` in the closure. /// /// The closure only gets called when an error was thrown. -public func throwError(closure: @escaping ((Error) -> Void)) -> Matcher { +public func throwError(closure: @escaping (@Sendable (Error) -> Void)) -> Matcher { return Matcher { actualExpression in var actualError: Error? do { @@ -232,7 +232,7 @@ public func throwError(closure: @escaping ((Error) -> Void)) -> Matcher(closure: @escaping ((T) -> Void)) -> Matcher { +public func throwError(closure: @escaping (@Sendable (T) -> Void)) -> Matcher { return Matcher { actualExpression in var actualError: Error? do { diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index 59bf4c8c..030b7a11 100644 --- a/Sources/Nimble/Polling+AsyncAwait.swift +++ b/Sources/Nimble/Polling+AsyncAwait.swift @@ -27,7 +27,7 @@ internal func execute( } } -internal actor Poller { +internal actor Poller { private var lastMatcherResult: MatcherResult? init() {} @@ -41,7 +41,7 @@ internal actor Poller { fnName: String, matcherRunner: @escaping @Sendable () async throws -> MatcherResult) async -> MatcherResult { let fnName = "expect(...).\(fnName)(...)" - let result = await pollBlock( + let result = await asyncPollBlock( pollInterval: poll, timeoutInterval: timeout, sourceLocation: expression.location, @@ -91,7 +91,7 @@ internal func poll( ) } -extension SyncExpectation { +extension SyncExpectation where Value: Sendable { // MARK: - With Synchronous Matchers /// Tests the actual value using a matcher to match by checking continuously /// at each pollInterval until the timeout is reached. @@ -256,7 +256,9 @@ extension SyncExpectation { ) async -> Self { return await toAlways(matcher, until: until, pollInterval: pollInterval, description: description) } +} +extension SyncExpectation where Value: Sendable { // MARK: - With AsyncMatchers /// Tests the actual value using a matcher to match by checking continuously /// at each pollInterval until the timeout is reached. diff --git a/Sources/Nimble/Polling+Require.swift b/Sources/Nimble/Polling+Require.swift index cbf688c5..17a3fffa 100644 --- a/Sources/Nimble/Polling+Require.swift +++ b/Sources/Nimble/Polling+Require.swift @@ -708,28 +708,28 @@ extension AsyncRequirement { /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrap(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () throws -> T?) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil()) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { +public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> T?) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { +public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } diff --git a/Sources/Nimble/Polling.swift b/Sources/Nimble/Polling.swift index c74facb6..f54b4caf 100644 --- a/Sources/Nimble/Polling.swift +++ b/Sources/Nimble/Polling.swift @@ -37,8 +37,8 @@ public struct AsyncDefaults { public struct PollingDefaults: @unchecked Sendable { private static let lock = NSRecursiveLock() - private static var _timeout: NimbleTimeInterval = .seconds(1) - private static var _pollInterval: NimbleTimeInterval = .milliseconds(10) + nonisolated(unsafe) private static var _timeout: NimbleTimeInterval = .seconds(1) + nonisolated(unsafe) private static var _pollInterval: NimbleTimeInterval = .milliseconds(10) public static var timeout: NimbleTimeInterval { get { diff --git a/Sources/Nimble/Requirement.swift b/Sources/Nimble/Requirement.swift index 6f70d224..9e573750 100644 --- a/Sources/Nimble/Requirement.swift +++ b/Sources/Nimble/Requirement.swift @@ -71,7 +71,7 @@ internal func executeRequire(_ expression: AsyncExpression, _ style: Expec } } -public struct SyncRequirement { +public struct SyncRequirement: Sendable { public let expression: Expression /// A custom error to throw. diff --git a/Sources/Nimble/Utils/AsyncAwait.swift b/Sources/Nimble/Utils/AsyncAwait.swift index cd4dd9d5..36429791 100644 --- a/Sources/Nimble/Utils/AsyncAwait.swift +++ b/Sources/Nimble/Utils/AsyncAwait.swift @@ -259,7 +259,7 @@ private func runAwaitTrigger( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, sourceLocation: SourceLocation, - _ closure: @escaping (@escaping @Sendable (T) -> Void) async throws -> Void + _ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void ) async -> AsyncPollResult { let timeoutQueue = awaiter.timeoutQueue let completionCount = Box(value: 0) @@ -315,7 +315,7 @@ internal func performBlock( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, sourceLocation: SourceLocation, - _ closure: @escaping (@escaping @Sendable (T) -> Void) async throws -> Void + _ closure: @escaping @Sendable (@escaping @Sendable (T) -> Void) async throws -> Void ) async -> AsyncPollResult { await runAwaitTrigger( awaiter: NimbleEnvironment.activeInstance.awaiter, @@ -325,7 +325,7 @@ internal func performBlock( closure) } -internal func pollBlock( +internal func asyncPollBlock( pollInterval: NimbleTimeInterval, timeoutInterval: NimbleTimeInterval, sourceLocation: SourceLocation, diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index 76f31530..b56eef6a 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -404,7 +404,7 @@ internal func pollBlock( pollInterval: NimbleTimeInterval, timeoutInterval: NimbleTimeInterval, sourceLocation: SourceLocation, - fnName: String = #function, + fnName: String, expression: @escaping () throws -> PollStatus) -> PollResult { let awaiter = NimbleEnvironment.activeInstance.awaiter let result = awaiter.poll(pollInterval) { () throws -> Bool? in From b4b2dada6389e16ce31489148ea577f4762cc7f0 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 13 Oct 2024 22:04:14 -0700 Subject: [PATCH 22/25] Fix concurrency warnings for more objective-c/foundation types (#1165) --- Sources/Nimble/Adapters/NMBExpectation.swift | 2 +- Sources/Nimble/Matchers/AllPass.swift | 4 +++- Sources/Nimble/Matchers/BeCloseTo.swift | 2 +- Sources/Nimble/Matchers/BeEmpty.swift | 3 ++- Sources/Nimble/Matchers/Contain.swift | 9 +++++++-- Sources/Nimble/Matchers/Matcher.swift | 2 +- Tests/NimbleTests/Matchers/BeAKindOfTest.swift | 4 ++-- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Sources/Nimble/Adapters/NMBExpectation.swift b/Sources/Nimble/Adapters/NMBExpectation.swift index 4279c778..6ca25fb7 100644 --- a/Sources/Nimble/Adapters/NMBExpectation.swift +++ b/Sources/Nimble/Adapters/NMBExpectation.swift @@ -14,7 +14,7 @@ private func from(objcMatcher: NMBMatcher) -> Matcher { // Equivalent to Expectation, but for Nimble's Objective-C interface public final class NMBExpectation: NSObject, Sendable { - internal let _actualBlock: @Sendable () -> NSObject? + internal let _actualBlock: @Sendable () -> sending NSObject? internal let _negative: Bool internal let _file: FileString internal let _line: UInt diff --git a/Sources/Nimble/Matchers/AllPass.swift b/Sources/Nimble/Matchers/AllPass.swift index f1f6e2b5..489f2e4b 100644 --- a/Sources/Nimble/Matchers/AllPass.swift +++ b/Sources/Nimble/Matchers/AllPass.swift @@ -100,7 +100,9 @@ extension NMBMatcher { ) } - let expr = Expression(expression: ({ nsObjects }), location: location) + let immutableCollection = nsObjects + + let expr = Expression(expression: ({ immutableCollection }), location: location) let pred: Matcher<[NSObject]> = createMatcher(Matcher { expr in return matcher.satisfies(({ try expr.evaluate() }), location: expr.location).toSwift() }) diff --git a/Sources/Nimble/Matchers/BeCloseTo.swift b/Sources/Nimble/Matchers/BeCloseTo.swift index 36dc0e00..0031d8f3 100644 --- a/Sources/Nimble/Matchers/BeCloseTo.swift +++ b/Sources/Nimble/Matchers/BeCloseTo.swift @@ -67,7 +67,7 @@ private func beCloseTo( } #if canImport(Darwin) -public class NMBObjCBeCloseToMatcher: NMBMatcher { +public final class NMBObjCBeCloseToMatcher: NMBMatcher, @unchecked Sendable { private let _expected: NSNumber fileprivate init(expected: NSNumber, within: CDouble) { diff --git a/Sources/Nimble/Matchers/BeEmpty.swift b/Sources/Nimble/Matchers/BeEmpty.swift index 43e1ca79..cdec3e60 100644 --- a/Sources/Nimble/Matchers/BeEmpty.swift +++ b/Sources/Nimble/Matchers/BeEmpty.swift @@ -88,7 +88,8 @@ extension NMBMatcher { let expr = Expression(expression: { value }, location: location) return try beEmpty().satisfies(expr).toObjectiveC() } else if let value = actualValue as? NSString { - let expr = Expression(expression: { value }, location: location) + let stringValue = String(value) + let expr = Expression(expression: { stringValue }, location: location) return try beEmpty().satisfies(expr).toObjectiveC() } else if let actualValue = actualValue { let badTypeErrorMsg = "be empty (only works for NSArrays, NSSets, NSIndexSets, NSDictionaries, NSHashTables, and NSStrings)" diff --git a/Sources/Nimble/Matchers/Contain.swift b/Sources/Nimble/Matchers/Contain.swift index 555d4d1f..93e5364f 100644 --- a/Sources/Nimble/Matchers/Contain.swift +++ b/Sources/Nimble/Matchers/Contain.swift @@ -77,10 +77,14 @@ public func contain(_ substrings: NSString...) -> Matcher { } public func contain(_ substrings: [NSString]) -> Matcher { + let stringSubstrings = substrings.map { String($0) } return Matcher.simple("contain <\(arrayAsString(substrings))>") { actualExpression in guard let actual = try actualExpression.evaluate() else { return .fail } + let actualString = String(actual) - let matches = substrings.allSatisfy { actual.range(of: $0.description).length != 0 } + let matches = stringSubstrings.allSatisfy { string in + actualString.contains(string) + } return MatcherStatus(bool: matches) } } @@ -115,7 +119,8 @@ extension NMBMatcher { let expectedOptionals: [Any?] = expected.map({ $0 as Any? }) return try contain(expectedOptionals).satisfies(expr).toObjectiveC() } else if let value = actualValue as? NSString { - let expr = Expression(expression: ({ value as String }), location: location) + let stringValue = String(value) + let expr = Expression(expression: ({ stringValue }), location: location) // swiftlint:disable:next force_cast return try contain(expected as! [String]).satisfies(expr).toObjectiveC() } diff --git a/Sources/Nimble/Matchers/Matcher.swift b/Sources/Nimble/Matchers/Matcher.swift index ffb5798b..470985f6 100644 --- a/Sources/Nimble/Matchers/Matcher.swift +++ b/Sources/Nimble/Matchers/Matcher.swift @@ -225,7 +225,7 @@ public class NMBMatcher: NSObject, @unchecked Sendable { self.init(matcher: predicate) } - func satisfies(_ expression: @escaping @Sendable () throws -> NSObject?, location: SourceLocation) -> NMBMatcherResult { + func satisfies(_ expression: @escaping @Sendable () throws -> sending NSObject?, location: SourceLocation) -> NMBMatcherResult { let expr = Expression(expression: expression, location: location) do { return try self.matcher(expr) diff --git a/Tests/NimbleTests/Matchers/BeAKindOfTest.swift b/Tests/NimbleTests/Matchers/BeAKindOfTest.swift index 93bf7b9f..1160eb57 100644 --- a/Tests/NimbleTests/Matchers/BeAKindOfTest.swift +++ b/Tests/NimbleTests/Matchers/BeAKindOfTest.swift @@ -5,9 +5,9 @@ import Nimble import NimbleSharedTestHelpers #endif -private class TestNull: NSNull {} +private final class TestNull: NSNull, @unchecked Sendable {} private protocol TestProtocol {} -private class TestClassConformingToProtocol: TestProtocol {} +private final class TestClassConformingToProtocol: TestProtocol {} private struct TestStructConformingToProtocol: TestProtocol {} final class BeAKindOfSwiftTest: XCTestCase { From ff61b40b0f8043c0300b43f942ff07adb403b071 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 13 Oct 2024 22:09:53 -0700 Subject: [PATCH 23/25] Fix the current set of concurrency warnings in tests (#1166) --- .../NimbleTests/AsyncAwaitTest+Require.swift | 80 +++++++-------- Tests/NimbleTests/AsyncAwaitTest.swift | 88 ++++++++--------- .../ContainElementSatisfyingTest.swift | 12 ++- Tests/NimbleTests/Matchers/ContainTest.swift | 9 +- Tests/NimbleTests/Matchers/MapTest.swift | 6 +- .../Matchers/PostNotificationTest.swift | 28 +++--- .../Matchers/SatisfyAllOfTest.swift | 12 ++- .../Matchers/SatisfyAnyOfTest.swift | 10 +- .../Matchers/ThrowAssertionTest.swift | 20 ++-- .../NimbleTests/Matchers/ThrowErrorTest.swift | 2 +- Tests/NimbleTests/PollingTest+Require.swift | 98 +++++++++---------- Tests/NimbleTests/PollingTest.swift | 94 +++++++++--------- 12 files changed, 231 insertions(+), 228 deletions(-) diff --git a/Tests/NimbleTests/AsyncAwaitTest+Require.swift b/Tests/NimbleTests/AsyncAwaitTest+Require.swift index f88e986a..f681557a 100644 --- a/Tests/NimbleTests/AsyncAwaitTest+Require.swift +++ b/Tests/NimbleTests/AsyncAwaitTest+Require.swift @@ -1,7 +1,7 @@ #if !os(WASI) import XCTest -import Nimble +@testable import Nimble #if SWIFT_PACKAGE import NimbleSharedTestHelpers #endif @@ -24,12 +24,12 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b } func testToEventuallyPositiveMatches() async throws { - var value = 0 - deferToMainQueue { value = 1 } - try await require { value }.toEventually(equal(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + try await require { value.value }.toEventually(equal(1)) - deferToMainQueue { value = 0 } - try await require { value }.toEventuallyNot(equal(1)) + deferToMainQueue { value.set(0) } + try await require { value.value }.toEventuallyNot(equal(1)) } func testToEventuallyNegativeMatches() async { @@ -194,52 +194,52 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b } final class ClassUnderTest { - var deinitCalled: (() -> Void)? - var count = 0 - deinit { deinitCalled?() } + let deinitCalled = LockedContainer<(() -> Void)?>(nil) + let count = LockedContainer(0) + deinit { deinitCalled.value?() } } func testSubjectUnderTestIsReleasedFromMemory() async throws { - var subject: ClassUnderTest? = ClassUnderTest() + let subject = LockedContainer(ClassUnderTest()) - if let sub = subject { - try await require(sub.count).toEventually(equal(0), timeout: .milliseconds(100)) - try await require(sub.count).toEventuallyNot(equal(1), timeout: .milliseconds(100)) + if let sub = subject.value { + try await require(sub.count.value).toEventually(equal(0), timeout: .milliseconds(100)) + try await require(sub.count.value).toEventuallyNot(equal(1), timeout: .milliseconds(100)) } await waitUntil(timeout: .milliseconds(500)) { done in - subject?.deinitCalled = { + subject.value?.deinitCalled.set({ done() - } + }) - deferToMainQueue { subject = nil } + deferToMainQueue { subject.set(nil) } } } func testToNeverPositiveMatches() async throws { - var value = 0 - deferToMainQueue { value = 1 } - try await require { value }.toNever(beGreaterThan(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + try await require { value.value }.toNever(beGreaterThan(1)) - deferToMainQueue { value = 0 } - try await require { value }.neverTo(beGreaterThan(1)) + deferToMainQueue { value.set(0) } + try await require { value.value }.neverTo(beGreaterThan(1)) } func testToNeverNegativeMatches() async { - var value = 0 + let value = LockedContainer(0) await failsWithErrorMessage("expected to never equal <0>, got <0>") { - try await require { value }.toNever(equal(0)) + try await require { value.value }.toNever(equal(0)) } await failsWithErrorMessage("expected to never equal <0>, got <0>") { - try await require { value }.neverTo(equal(0)) + try await require { value.value }.neverTo(equal(0)) } await failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - try await require { value }.toNever(equal(1)) + deferToMainQueue { value.set(1) } + try await require { value.value }.toNever(equal(1)) } await failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - try await require { value }.neverTo(equal(1)) + deferToMainQueue { value.set(1) } + try await require { value.value }.neverTo(equal(1)) } await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { try await require { try Self.doThrowError() }.toNever(equal(0)) @@ -253,29 +253,29 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b } func testToAlwaysPositiveMatches() async throws { - var value = 1 - deferToMainQueue { value = 2 } - try await require { value }.toAlways(beGreaterThan(0)) + let value = LockedContainer(1) + deferToMainQueue { value.set(2) } + try await require { value.value }.toAlways(beGreaterThan(0)) - deferToMainQueue { value = 2 } - try await require { value }.alwaysTo(beGreaterThan(1)) + deferToMainQueue { value.set(2) } + try await require { value.value }.alwaysTo(beGreaterThan(1)) } func testToAlwaysNegativeMatches() async { - var value = 1 + let value = LockedContainer(1) await failsWithErrorMessage("expected to always equal <0>, got <1>") { - try await require { value }.toAlways(equal(0)) + try await require { value.value }.toAlways(equal(0)) } await failsWithErrorMessage("expected to always equal <0>, got <1>") { - try await require { value }.alwaysTo(equal(0)) + try await require { value.value }.alwaysTo(equal(0)) } await failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - try await require { value }.toAlways(equal(1)) + deferToMainQueue { value.set(0) } + try await require { value.value }.toAlways(equal(1)) } await failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - try await require { value }.alwaysTo(equal(1)) + deferToMainQueue { value.set(0) } + try await require { value.value }.alwaysTo(equal(1)) } await failsWithErrorMessage("unexpected error thrown: <\(Self.errorToThrow)>") { try await require { try Self.doThrowError() }.toAlways(equal(0)) diff --git a/Tests/NimbleTests/AsyncAwaitTest.swift b/Tests/NimbleTests/AsyncAwaitTest.swift index 99cd4372..f833784a 100644 --- a/Tests/NimbleTests/AsyncAwaitTest.swift +++ b/Tests/NimbleTests/AsyncAwaitTest.swift @@ -1,7 +1,7 @@ #if !os(WASI) import XCTest -import Nimble +@testable import Nimble #if SWIFT_PACKAGE import NimbleSharedTestHelpers #endif @@ -24,12 +24,12 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len } func testToEventuallyPositiveMatches() async { - var value = 0 - deferToMainQueue { value = 1 } - await expect { value }.toEventually(equal(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + await expect { value.value }.toEventually(equal(1)) - deferToMainQueue { value = 0 } - await expect { value }.toEventuallyNot(equal(1)) + deferToMainQueue { value.set(0) } + await expect { value.value }.toEventuallyNot(equal(1)) } func testToEventuallyNegativeMatches() async { @@ -145,23 +145,23 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len PollingDefaults.timeout = .seconds(1) } - var value = 0 + let value = LockedContainer(0) let sleepThenSetValueTo: (Int) -> Void = { newValue in Thread.sleep(forTimeInterval: 1.1) - value = newValue + value.set(newValue) } let task = Task { sleepThenSetValueTo(1) } - await expect { value }.toEventually(equal(1)) + await expect { value.value }.toEventually(equal(1)) let secondTask = Task { sleepThenSetValueTo(0) } - await expect { value }.toEventuallyNot(equal(1)) + await expect { value.value }.toEventuallyNot(equal(1)) _ = await task.value _ = await secondTask.result @@ -286,52 +286,52 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len } final class ClassUnderTest { - var deinitCalled: (() -> Void)? - var count = 0 - deinit { deinitCalled?() } + let deinitCalled = LockedContainer<(() -> Void)?>(nil) + let count = LockedContainer(0) + deinit { deinitCalled.value?() } } func testSubjectUnderTestIsReleasedFromMemory() async { - var subject: ClassUnderTest? = ClassUnderTest() + let subject = LockedContainer(ClassUnderTest()) - if let sub = subject { - await expect(sub.count).toEventually(equal(0), timeout: .milliseconds(100)) - await expect(sub.count).toEventuallyNot(equal(1), timeout: .milliseconds(100)) + if let sub = subject.value { + await expect(sub.count.value).toEventually(equal(0), timeout: .milliseconds(100)) + await expect(sub.count.value).toEventuallyNot(equal(1), timeout: .milliseconds(100)) } await waitUntil(timeout: .milliseconds(500)) { done in - subject?.deinitCalled = { + subject.value?.deinitCalled.set({ done() - } + }) - deferToMainQueue { subject = nil } + deferToMainQueue { subject.set(nil) } } } func testToNeverPositiveMatches() async { - var value = 0 - deferToMainQueue { value = 1 } - await expect { value }.toNever(beGreaterThan(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + await expect { value.value }.toNever(beGreaterThan(1)) - deferToMainQueue { value = 0 } - await expect { value }.neverTo(beGreaterThan(1)) + deferToMainQueue { value.set(0) } + await expect { value.value }.neverTo(beGreaterThan(1)) } func testToNeverNegativeMatches() async { - var value = 0 + let value = LockedContainer(0) await failsWithErrorMessage("expected to never equal <0>, got <0>") { - await expect { value }.toNever(equal(0)) + await expect { value.value }.toNever(equal(0)) } await failsWithErrorMessage("expected to never equal <0>, got <0>") { - await expect { value }.neverTo(equal(0)) + await expect { value.value }.neverTo(equal(0)) } await failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - await expect { value }.toNever(equal(1)) + deferToMainQueue { value.set(1) } + await expect { value.value }.toNever(equal(1)) } await failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - await expect { value }.neverTo(equal(1)) + deferToMainQueue { value.set(1) } + await expect { value.value }.neverTo(equal(1)) } await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { await expect { try self.doThrowError() }.toNever(equal(0)) @@ -342,29 +342,29 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len } func testToAlwaysPositiveMatches() async { - var value = 1 - deferToMainQueue { value = 2 } - await expect { value }.toAlways(beGreaterThan(0)) + let value = LockedContainer(1) + deferToMainQueue { value.set(2) } + await expect { value.value }.toAlways(beGreaterThan(0)) - deferToMainQueue { value = 2 } - await expect { value }.alwaysTo(beGreaterThan(1)) + deferToMainQueue { value.set(2) } + await expect { value.value }.alwaysTo(beGreaterThan(1)) } func testToAlwaysNegativeMatches() async { - var value = 1 + let value = LockedContainer(1) await failsWithErrorMessage("expected to always equal <0>, got <1>") { - await expect { value }.toAlways(equal(0)) + await expect { value.value }.toAlways(equal(0)) } await failsWithErrorMessage("expected to always equal <0>, got <1>") { - await expect { value }.alwaysTo(equal(0)) + await expect { value.value }.alwaysTo(equal(0)) } await failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - await expect { value }.toAlways(equal(1)) + deferToMainQueue { value.set(0) } + await expect { value.value }.toAlways(equal(1)) } await failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - await expect { value }.alwaysTo(equal(1)) + deferToMainQueue { value.set(0) } + await expect { value.value }.alwaysTo(equal(1)) } await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { await expect { try self.doThrowError() }.toAlways(equal(0)) diff --git a/Tests/NimbleTests/Matchers/ContainElementSatisfyingTest.swift b/Tests/NimbleTests/Matchers/ContainElementSatisfyingTest.swift index 38a3eac1..cce36255 100644 --- a/Tests/NimbleTests/Matchers/ContainElementSatisfyingTest.swift +++ b/Tests/NimbleTests/Matchers/ContainElementSatisfyingTest.swift @@ -8,7 +8,7 @@ import NimbleSharedTestHelpers final class ContainElementSatisfyingTest: XCTestCase { // MARK: - Matcher variant func testContainElementSatisfying() { - var orderIndifferentArray = [1, 2, 3] + let orderIndifferentArray = [1, 2, 3] expect(orderIndifferentArray).to(containElementSatisfying({ number in return number == 1 })) @@ -18,8 +18,10 @@ final class ContainElementSatisfyingTest: XCTestCase { expect(orderIndifferentArray).to(containElementSatisfying({ number in return number == 3 })) + } - orderIndifferentArray = [3, 1, 2] + func testContainElementSatisfying2() { + let orderIndifferentArray = [3, 1, 2] expect(orderIndifferentArray).to(containElementSatisfying({ number in return number == 1 })) @@ -76,7 +78,7 @@ final class ContainElementSatisfyingTest: XCTestCase { // MARK: - AsyncMatcher variant func testAsyncContainElementSatisfying() async { - var orderIndifferentArray = [1, 2, 3] + let orderIndifferentArray = [1, 2, 3] await expect(orderIndifferentArray).to(containElementSatisfying({ number in await asyncEqualityCheck(number, 1) })) @@ -86,8 +88,10 @@ final class ContainElementSatisfyingTest: XCTestCase { await expect(orderIndifferentArray).to(containElementSatisfying({ number in await asyncEqualityCheck(number, 3) })) + } - orderIndifferentArray = [3, 1, 2] + func testAsyncContainElementSatisfying2() async { + let orderIndifferentArray = [3, 1, 2] await expect(orderIndifferentArray).to(containElementSatisfying({ number in await asyncEqualityCheck(number, 1) })) diff --git a/Tests/NimbleTests/Matchers/ContainTest.swift b/Tests/NimbleTests/Matchers/ContainTest.swift index 2991405a..3810a52f 100644 --- a/Tests/NimbleTests/Matchers/ContainTest.swift +++ b/Tests/NimbleTests/Matchers/ContainTest.swift @@ -89,11 +89,10 @@ final class ContainTest: XCTestCase { } func testContainNSStringSubstring() { - let str = "foo" as NSString - expect(str).to(contain("o" as NSString)) - expect(str).to(contain("oo" as NSString)) - expect(str).toNot(contain("z" as NSString)) - expect(str).toNot(contain("zz" as NSString)) + expect("foo" as NSString).to(contain("o" as NSString)) + expect("foo" as NSString).to(contain("oo" as NSString)) + expect("foo" as NSString).toNot(contain("z" as NSString)) + expect("foo" as NSString).toNot(contain("zz" as NSString)) } func testVariadicArguments() { diff --git a/Tests/NimbleTests/Matchers/MapTest.swift b/Tests/NimbleTests/Matchers/MapTest.swift index 9b13c5f6..d9bf87fd 100644 --- a/Tests/NimbleTests/Matchers/MapTest.swift +++ b/Tests/NimbleTests/Matchers/MapTest.swift @@ -56,7 +56,7 @@ final class MapTest: XCTestCase { } func testMapWithAsyncFunction() async { - func someOperation(_ value: Int) async -> String { + @Sendable func someOperation(_ value: Int) async -> String { "\(value)" } await expect(1).to(map(someOperation, equal("1"))) @@ -76,8 +76,8 @@ final class MapTest: XCTestCase { let box = Box(int: 3, string: "world") expect(box).to(satisfyAllOf( - map(\.int, equal(3)), - map(\.string, equal("world")) + map( { $0.int }, equal(3)), + map( { $0.string }, equal("world")) )) } } diff --git a/Tests/NimbleTests/Matchers/PostNotificationTest.swift b/Tests/NimbleTests/Matchers/PostNotificationTest.swift index c92e15ca..cde1b9c3 100644 --- a/Tests/NimbleTests/Matchers/PostNotificationTest.swift +++ b/Tests/NimbleTests/Matchers/PostNotificationTest.swift @@ -19,7 +19,7 @@ final class PostNotificationTest: XCTestCase { func testPassesWhenExpectedNotificationIsPosted() { let testNotification = Notification(name: Notification.Name("Foo"), object: nil) expect { - self.notificationCenter.post(testNotification) + self.notificationCenter.post(Notification(name: Notification.Name("Foo"), object: nil)) }.to(postNotifications(equal([testNotification]), from: notificationCenter)) } @@ -29,8 +29,8 @@ final class PostNotificationTest: XCTestCase { let n1 = Notification(name: Notification.Name("Foo"), object: foo) let n2 = Notification(name: Notification.Name("Bar"), object: bar) expect { - self.notificationCenter.post(n1) - self.notificationCenter.post(n2) + self.notificationCenter.post(Notification(name: Notification.Name("Foo"), object: foo)) + self.notificationCenter.post(Notification(name: Notification.Name("Bar"), object: bar)) }.to(postNotifications(equal([n1, n2]), from: notificationCenter)) } @@ -48,17 +48,18 @@ final class PostNotificationTest: XCTestCase { let n2 = Notification(name: Notification.Name(n1.name.rawValue + "a"), object: nil) failsWithErrorMessage("expected to equal <[\(n1)]>, got <[\(n2)]>") { expect { - self.notificationCenter.post(n2) + self.notificationCenter.post(Notification(name: Notification.Name("Fooa"), object: nil)) }.to(postNotifications(equal([n1]), from: self.notificationCenter)) } } func testFailsWhenNotificationWithWrongObjectIsPosted() { let n1 = Notification(name: Notification.Name("Foo"), object: nil) - let n2 = Notification(name: n1.name, object: NSObject()) + let object = NSObject() + let n2 = Notification(name: n1.name, object: object) failsWithErrorMessage("expected to equal <[\(n1)]>, got <[\(n2)]>") { expect { - self.notificationCenter.post(n2) + self.notificationCenter.post(Notification(name: Notification.Name("Foo"), object: object)) }.to(postNotifications(equal([n1]), from: self.notificationCenter)) } } @@ -67,7 +68,7 @@ final class PostNotificationTest: XCTestCase { let testNotification = Notification(name: Notification.Name("Foo"), object: nil) expect { deferToMainQueue { - self.notificationCenter.post(testNotification) + self.notificationCenter.post(Notification(name: Notification.Name("Foo"), object: nil)) } }.toEventually(postNotifications(equal([testNotification]), from: notificationCenter)) } @@ -76,16 +77,15 @@ final class PostNotificationTest: XCTestCase { let n1 = Notification(name: Notification.Name("Foo"), object: nil) failsWithErrorMessage("expected to not equal <[\(n1)]>, got <[\(n1)]>") { expect { - self.notificationCenter.post(n1) + self.notificationCenter.post(Notification(name: Notification.Name("Foo"), object: nil)) }.toNot(postNotifications(equal([n1]), from: self.notificationCenter)) } } func testPassesWhenNotificationIsNotPosted() { let n1 = Notification(name: Notification.Name("Foo"), object: nil) - let n2 = Notification(name: Notification.Name(n1.name.rawValue + "a"), object: nil) expect { - self.notificationCenter.post(n2) + self.notificationCenter.post(Notification(name: Notification.Name("Fooa"), object: nil)) }.toNever(postNotifications(equal([n1]), from: self.notificationCenter)) } @@ -99,7 +99,7 @@ final class PostNotificationTest: XCTestCase { self.notificationCenter.post(n2) }, ], waitUntilFinished: true) - self.notificationCenter.post(n1) + self.notificationCenter.post(Notification(name: Notification.Name("Foo"), object: nil)) }.to(postNotifications(contain([n1]), from: notificationCenter)) } @@ -109,7 +109,7 @@ final class PostNotificationTest: XCTestCase { OperationQueue().addOperations([ BlockOperation { let backgroundThreadObject = BackgroundThreadObject() - let n2 = Notification(name: Notification.Name(n1.name.rawValue + "a"), object: backgroundThreadObject) + let n2 = Notification(name: Notification.Name("Fooa"), object: backgroundThreadObject) self.notificationCenter.post(n2) }, ], waitUntilFinished: true) @@ -122,8 +122,8 @@ final class PostNotificationTest: XCTestCase { let n1 = Notification(name: Notification.Name("Foo"), object: "1") let n2 = Notification(name: Notification.Name("Bar"), object: "2") expect { - center.post(n1) - center.post(n2) + center.post(Notification(name: Notification.Name("Foo"), object: "1")) + center.post(Notification(name: Notification.Name("Bar"), object: "2")) }.toEventually(postDistributedNotifications(equal([n1, n2]), from: center, names: [n1.name, n2.name])) } #endif diff --git a/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift b/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift index e2563581..42969586 100644 --- a/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift +++ b/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift @@ -1,5 +1,5 @@ import XCTest -import Nimble +@testable import Nimble import Foundation #if SWIFT_PACKAGE import NimbleSharedTestHelpers @@ -57,10 +57,12 @@ final class SatisfyAllOfTest: XCTestCase { func testSatisfyAllOfCachesExpressionBeforePassingToMatchers() { // This is not a great example of assertion writing - functions being asserted on in Expressions should not have side effects. // But we should still handle those cases anyway. - var value: Int = 0 - func testFunction() -> Int { - value += 1 - return value + let value = LockedContainer(0) + @Sendable func testFunction() -> Int { + value.operate { + $0 + 1 + } + return value.value } expect(testFunction()).toEventually(satisfyAllOf(equal(1), equal(1))) diff --git a/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift b/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift index f02c3520..d8afc25e 100644 --- a/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift +++ b/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift @@ -1,5 +1,5 @@ import XCTest -import Nimble +@testable import Nimble import Foundation #if SWIFT_PACKAGE import NimbleSharedTestHelpers @@ -55,10 +55,10 @@ final class SatisfyAnyOfTest: XCTestCase { func testSatisfyAllOfCachesExpressionBeforePassingToMatchers() { // This is not a great example of assertion writing - functions being asserted on in Expressions should not have side effects. // But we should still handle those cases anyway. - var value: Int = 0 - func testFunction() -> Int { - value += 1 - return value + let value = LockedContainer(0) + @Sendable func testFunction() -> Int { + value.operate { $0 + 1 } + return value.value } // This demonstrates caching because the first time this is evaluated, the function should return 1, which doesn't pass the `equal(0)`. diff --git a/Tests/NimbleTests/Matchers/ThrowAssertionTest.swift b/Tests/NimbleTests/Matchers/ThrowAssertionTest.swift index 8d547d12..8bffc575 100644 --- a/Tests/NimbleTests/Matchers/ThrowAssertionTest.swift +++ b/Tests/NimbleTests/Matchers/ThrowAssertionTest.swift @@ -1,6 +1,6 @@ import Foundation import XCTest -import Nimble +@testable import Nimble #if SWIFT_PACKAGE import NimbleSharedTestHelpers #endif @@ -22,27 +22,27 @@ final class ThrowAssertionTest: XCTestCase { func testPostAssertionCodeNotRun() { #if (arch(x86_64) || arch(arm64)) && !os(Windows) - var reachedPoint1 = false - var reachedPoint2 = false + let reachedPoint1 = LockedContainer(false) + let reachedPoint2 = LockedContainer(false) expect { - reachedPoint1 = true + reachedPoint1.set(true) precondition(false, "condition message") - reachedPoint2 = true + reachedPoint2.set(true) }.to(throwAssertion()) - expect(reachedPoint1) == true - expect(reachedPoint2) == false + expect(reachedPoint1.value) == true + expect(reachedPoint2.value) == false #endif } func testNegativeMatch() { #if (arch(x86_64) || arch(arm64)) && !os(Windows) - var reachedPoint1 = false + let reachedPoint1 = LockedContainer(false) - expect { reachedPoint1 = true }.toNot(throwAssertion()) + expect { reachedPoint1.set(true) }.toNot(throwAssertion()) - expect(reachedPoint1) == true + expect(reachedPoint1.value) == true #endif } diff --git a/Tests/NimbleTests/Matchers/ThrowErrorTest.swift b/Tests/NimbleTests/Matchers/ThrowErrorTest.swift index f2523d86..bbdbb4d4 100644 --- a/Tests/NimbleTests/Matchers/ThrowErrorTest.swift +++ b/Tests/NimbleTests/Matchers/ThrowErrorTest.swift @@ -128,7 +128,7 @@ final class ThrowErrorTest: XCTestCase { func testNegativeMatchesWithClosure() { let moduleName = "NimbleTests" let innerFailureMessage = "expected to equal , got <\(moduleName).NimbleError>" - let closure = { (error: Error) -> Void in + let closure = { @Sendable (error: Error) -> Void in expect(error._domain).to(equal("foo")) } diff --git a/Tests/NimbleTests/PollingTest+Require.swift b/Tests/NimbleTests/PollingTest+Require.swift index 7bdc4846..ac3edcfc 100644 --- a/Tests/NimbleTests/PollingTest+Require.swift +++ b/Tests/NimbleTests/PollingTest+Require.swift @@ -6,7 +6,7 @@ import CoreFoundation #endif import Foundation import XCTest -import Nimble +@testable import Nimble #if SWIFT_PACKAGE import NimbleSharedTestHelpers #endif @@ -21,15 +21,15 @@ final class PollingRequireTest: XCTestCase { } func testToEventuallyPositiveMatches() { - var value = 0 - deferToMainQueue { value = 1 } + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } expect { - try require { value }.toEventually(equal(1)) + try require { value.value }.toEventually(equal(1)) }.to(equal(1)) - deferToMainQueue { value = 0 } + deferToMainQueue { value.set(0) } expect { - try require { value }.toEventuallyNot(equal(1)) + try require { value.value }.toEventuallyNot(equal(1)) }.to(equal(0)) } @@ -50,12 +50,10 @@ final class PollingRequireTest: XCTestCase { } func testPollUnwrapPositiveCase() { - var value: Int? = nil - deferToMainQueue { - value = 1 - } + let value = LockedContainer(nil) + deferToMainQueue { value.set(1) } expect { - try pollUnwrap(value) + try pollUnwrap(value.value) }.to(equal(1)) } @@ -83,22 +81,22 @@ final class PollingRequireTest: XCTestCase { PollingDefaults.timeout = .seconds(1) } - var value = 0 + let value = LockedContainer(0) let sleepThenSetValueTo: (Int) -> Void = { newValue in Thread.sleep(forTimeInterval: 1.1) - value = newValue + value.set(newValue) } var asyncOperation: () -> Void = { sleepThenSetValueTo(1) } DispatchQueue.global().async(execute: asyncOperation) - try require { value }.toEventually(equal(1)) + try require { value.value }.toEventually(equal(1)) asyncOperation = { sleepThenSetValueTo(0) } DispatchQueue.global().async(execute: asyncOperation) - try require { value }.toEventuallyNot(equal(1)) + try require { value.value }.toEventuallyNot(equal(1)) } func testToEventuallyAllowsInBackgroundThread() { @@ -120,53 +118,53 @@ final class PollingRequireTest: XCTestCase { #endif } - final class ClassUnderTest { - var deinitCalled: (() -> Void)? - var count = 0 - deinit { deinitCalled?() } + final class ClassUnderTest: Sendable { + let deinitCalled = LockedContainer<(@Sendable () -> Void)?>(nil) + let count = LockedContainer(0) + deinit { deinitCalled.value?() } } func testSubjectUnderTestIsReleasedFromMemory() throws { - var subject: ClassUnderTest? = ClassUnderTest() + let subject = LockedContainer(ClassUnderTest()) - if let sub = subject { - try require(sub.count).toEventually(equal(0), timeout: .milliseconds(100)) - try require(sub.count).toEventuallyNot(equal(1), timeout: .milliseconds(100)) + if let sub = subject.value { + try require(sub.count.value).toEventually(equal(0), timeout: .milliseconds(100)) + try require(sub.count.value).toEventuallyNot(equal(1), timeout: .milliseconds(100)) } waitUntil(timeout: .milliseconds(500)) { done in - subject?.deinitCalled = { + subject.value?.deinitCalled.set({ done() - } + }) - deferToMainQueue { subject = nil } + deferToMainQueue { subject.set(nil) } } } func testToNeverPositiveMatches() throws { - var value = 0 - deferToMainQueue { value = 1 } - try require { value }.toNever(beGreaterThan(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + try require { value.value }.toNever(beGreaterThan(1)) - deferToMainQueue { value = 0 } - try require { value }.neverTo(beGreaterThan(1)) + deferToMainQueue { value.set(0) } + try require { value.value }.neverTo(beGreaterThan(1)) } func testToNeverNegativeMatches() { - var value = 0 + let value = LockedContainer(0) failsWithErrorMessage("expected to never equal <0>, got <0>") { - try require { value }.toNever(equal(0)) + try require { value.value }.toNever(equal(0)) } failsWithErrorMessage("expected to never equal <0>, got <0>") { - try require { value }.neverTo(equal(0)) + try require { value.value }.neverTo(equal(0)) } failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - try require { value }.toNever(equal(1)) + deferToMainQueue { value.set(1) } + try require { value.value }.toNever(equal(1)) } failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - try require { value }.neverTo(equal(1)) + deferToMainQueue { value.set(1) } + try require { value.value }.neverTo(equal(1)) } failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { try require { try self.doThrowError() }.toNever(equal(0)) @@ -180,29 +178,29 @@ final class PollingRequireTest: XCTestCase { } func testToAlwaysPositiveMatches() throws { - var value = 1 - deferToMainQueue { value = 2 } - try require { value }.toAlways(beGreaterThan(0)) + let value = LockedContainer(1) + deferToMainQueue { value.set(2) } + try require { value.value }.toAlways(beGreaterThan(0)) - deferToMainQueue { value = 2 } - try require { value }.alwaysTo(beGreaterThan(1)) + deferToMainQueue { value.set(2) } + try require { value.value }.alwaysTo(beGreaterThan(1)) } func testToAlwaysNegativeMatches() { - var value = 1 + let value = LockedContainer(1) failsWithErrorMessage("expected to always equal <0>, got <1>") { - try require { value }.toAlways(equal(0)) + try require { value.value }.toAlways(equal(0)) } failsWithErrorMessage("expected to always equal <0>, got <1>") { - try require { value }.alwaysTo(equal(0)) + try require { value.value }.alwaysTo(equal(0)) } failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - try require { value }.toAlways(equal(1)) + deferToMainQueue { value.set(0) } + try require { value.value }.toAlways(equal(1)) } failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - try require { value }.alwaysTo(equal(1)) + deferToMainQueue { value.set(0) } + try require { value.value }.alwaysTo(equal(1)) } failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { try require { try self.doThrowError() }.toAlways(equal(0)) diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index e414b7b3..616e411a 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -6,7 +6,7 @@ import CoreFoundation #endif import Foundation import XCTest -import Nimble +@testable import Nimble #if SWIFT_PACKAGE import NimbleSharedTestHelpers #endif @@ -21,12 +21,12 @@ final class PollingTest: XCTestCase { } func testToEventuallyPositiveMatches() { - var value = 0 - deferToMainQueue { value = 1 } - expect { value }.toEventually(equal(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + expect { value.value }.toEventually(equal(1)) - deferToMainQueue { value = 0 } - expect { value }.toEventuallyNot(equal(1)) + deferToMainQueue { value.set(0) } + expect { value.value }.toEventuallyNot(equal(1)) } func testToEventuallyNegativeMatches() { @@ -55,22 +55,22 @@ final class PollingTest: XCTestCase { PollingDefaults.timeout = .seconds(1) } - var value = 0 + let value = LockedContainer(0) let sleepThenSetValueTo: (Int) -> Void = { newValue in Thread.sleep(forTimeInterval: 1.1) - value = newValue + value.set(newValue) } var asyncOperation: () -> Void = { sleepThenSetValueTo(1) } DispatchQueue.global().async(execute: asyncOperation) - expect { value }.toEventually(equal(1)) + expect { value.value }.toEventually(equal(1)) asyncOperation = { sleepThenSetValueTo(0) } DispatchQueue.global().async(execute: asyncOperation) - expect { value }.toEventuallyNot(equal(1)) + expect { value.value }.toEventuallyNot(equal(1)) } func testWaitUntilWithCustomDefaultsTimeout() { @@ -102,13 +102,13 @@ final class PollingTest: XCTestCase { } func testWaitUntilTimesOutWhenExceedingItsTime() { - var waiting = true + let waiting = LockedContainer(true) failsWithErrorMessage("Waited more than 0.01 seconds") { waitUntil(timeout: .milliseconds(10)) { done in - let asyncOperation: () -> Void = { + let asyncOperation: @Sendable () -> Void = { Thread.sleep(forTimeInterval: 0.1) done() - waiting = false + waiting.set(false) } DispatchQueue.global().async(execute: asyncOperation) } @@ -117,7 +117,7 @@ final class PollingTest: XCTestCase { // "clear" runloop to ensure this test doesn't poison other tests repeat { RunLoop.main.run(until: Date().addingTimeInterval(0.2)) - } while(waiting) + } while(waiting.value) } func testWaitUntilNegativeMatches() { @@ -241,53 +241,53 @@ final class PollingTest: XCTestCase { #endif } - final class ClassUnderTest { - var deinitCalled: (() -> Void)? - var count = 0 - deinit { deinitCalled?() } + final class ClassUnderTest: Sendable { + let deinitCalled = LockedContainer<(@Sendable () -> Void)?>(nil) + let count = 0 + deinit { deinitCalled.value?() } } func testSubjectUnderTestIsReleasedFromMemory() { - var subject: ClassUnderTest? = ClassUnderTest() + let subject = LockedContainer(ClassUnderTest()) - if let sub = subject { + if let sub = subject.value { expect(sub.count).toEventually(equal(0), timeout: .milliseconds(100)) expect(sub.count).toEventuallyNot(equal(1), timeout: .milliseconds(100)) } waitUntil(timeout: .milliseconds(500)) { done in - subject?.deinitCalled = { + subject.value?.deinitCalled.set({ done() - } + }) - deferToMainQueue { subject = nil } + deferToMainQueue { subject.set(nil) } } } func testToNeverPositiveMatches() { - var value = 0 - deferToMainQueue { value = 1 } - expect { value }.toNever(beGreaterThan(1)) + let value = LockedContainer(0) + deferToMainQueue { value.set(1) } + expect { value.value }.toNever(beGreaterThan(1)) - deferToMainQueue { value = 0 } - expect { value }.neverTo(beGreaterThan(1)) + deferToMainQueue { value.set(0) } + expect { value.value }.neverTo(beGreaterThan(1)) } func testToNeverNegativeMatches() { - var value = 0 + let value = LockedContainer(0) failsWithErrorMessage("expected to never equal <0>, got <0>") { - expect { value }.toNever(equal(0)) + expect { value.value }.toNever(equal(0)) } failsWithErrorMessage("expected to never equal <0>, got <0>") { - expect { value }.neverTo(equal(0)) + expect { value.value }.neverTo(equal(0)) } failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - expect { value }.toNever(equal(1)) + deferToMainQueue { value.set(1) } + expect { value.value }.toNever(equal(1)) } failsWithErrorMessage("expected to never equal <1>, got <1>") { - deferToMainQueue { value = 1 } - expect { value }.neverTo(equal(1)) + deferToMainQueue { value.set(1) } + expect { value.value }.neverTo(equal(1)) } failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { expect { try self.doThrowError() }.toNever(equal(0)) @@ -301,29 +301,29 @@ final class PollingTest: XCTestCase { } func testToAlwaysPositiveMatches() { - var value = 1 - deferToMainQueue { value = 2 } - expect { value }.toAlways(beGreaterThan(0)) + let value = LockedContainer(1) + deferToMainQueue { value.set(2) } + expect { value.value }.toAlways(beGreaterThan(0)) - deferToMainQueue { value = 2 } - expect { value }.alwaysTo(beGreaterThan(1)) + deferToMainQueue { value.set(2) } + expect { value.value }.alwaysTo(beGreaterThan(1)) } func testToAlwaysNegativeMatches() { - var value = 1 + let value = LockedContainer(1) failsWithErrorMessage("expected to always equal <0>, got <1>") { - expect { value }.toAlways(equal(0)) + expect { value.value }.toAlways(equal(0)) } failsWithErrorMessage("expected to always equal <0>, got <1>") { - expect { value }.alwaysTo(equal(0)) + expect { value.value }.alwaysTo(equal(0)) } failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - expect { value }.toAlways(equal(1)) + deferToMainQueue { value.set(0) } + expect { value.value }.toAlways(equal(1)) } failsWithErrorMessage("expected to always equal <1>, got <0>") { - deferToMainQueue { value = 0 } - expect { value }.alwaysTo(equal(1)) + deferToMainQueue { value.set(0) } + expect { value.value }.alwaysTo(equal(1)) } failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { expect { try self.doThrowError() }.toAlways(equal(0)) From 9c13be6ccf011e122d80445d9433d21403a1e182 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 13 Oct 2024 22:32:33 -0700 Subject: [PATCH 24/25] All parts of the dsl must take in sending (#1167) --- Sources/Nimble/DSL+AsyncAwait.swift | 16 +-- Sources/Nimble/DSL+Require.swift | 16 +-- Sources/Nimble/DSL.swift | 5 +- Sources/Nimble/Polling+Require.swift | 4 +- .../Matchers/BeIdenticalToTest.swift | 4 +- Tests/NimbleTests/Matchers/EqualTest.swift | 125 +++++++++++++----- Tests/NimbleTests/Matchers/NegationTest.swift | 2 +- 7 files changed, 115 insertions(+), 57 deletions(-) diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index c7deecc9..3fb6e6a1 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> sending T?) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -12,7 +12,7 @@ public func expect(fileID: String = #fileID, file: FileString = #fi } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> sending T)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -21,7 +21,7 @@ public func expect(fileID: String = #fileID, file: FileString = #fi } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> sending T?)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -30,7 +30,7 @@ public func expect(fileID: String = #fileID, file: FileString = #fi } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> Void)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> sending Void)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -40,7 +40,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> sending T?) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -50,7 +50,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #f /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> sending T)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -60,7 +60,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #f /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> sending T?)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -70,7 +70,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #f /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> Void)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> sending Void)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index 40832be6..952faddd 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -123,7 +123,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, lin /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping () async throws -> T?) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping @Sendable () async throws -> sending T?) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -137,7 +137,7 @@ public func require(fileID: String = #fileID, file: FileString = #f /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> sending T)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -151,7 +151,7 @@ public func require(fileID: String = #fileID, file: FileString = #f /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> sending T?)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -167,7 +167,7 @@ public func require(fileID: String = #fileID, file: FileString = #f /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement`. @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () async throws -> sending T?) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -183,7 +183,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> sending T)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -199,7 +199,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> sending T?)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -266,7 +266,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #fi /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: () -> (@Sendable () async throws -> sending T?)) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -286,7 +286,7 @@ public func unwrapa(fileID: String = #fileID, file: FileString = #f /// `unwrapa` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { +public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> sending T?)) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } diff --git a/Sources/Nimble/DSL.swift b/Sources/Nimble/DSL.swift index c6261a5f..c93719ac 100644 --- a/Sources/Nimble/DSL.swift +++ b/Sources/Nimble/DSL.swift @@ -66,7 +66,10 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> Void)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncExpectation { + // It would seem like `sending` isn't necessary for the `expression` argument + // because the closure returns void. However, this gets rid of a type + // conversion warning/error. return SyncExpectation( expression: Expression( expression: expression(), diff --git a/Sources/Nimble/Polling+Require.swift b/Sources/Nimble/Polling+Require.swift index 17a3fffa..4f42f442 100644 --- a/Sources/Nimble/Polling+Require.swift +++ b/Sources/Nimble/Polling+Require.swift @@ -715,7 +715,7 @@ public func pollUnwrap(file: FileString = #file, line: UInt = #line, _ expres /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } @@ -729,7 +729,7 @@ public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { +public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } diff --git a/Tests/NimbleTests/Matchers/BeIdenticalToTest.swift b/Tests/NimbleTests/Matchers/BeIdenticalToTest.swift index 8fcf363e..be04bc78 100644 --- a/Tests/NimbleTests/Matchers/BeIdenticalToTest.swift +++ b/Tests/NimbleTests/Matchers/BeIdenticalToTest.swift @@ -26,7 +26,7 @@ final class BeIdenticalToTest: XCTestCase { } func testBeIdenticalToNegativeMessage() { - let value1 = NSArray() + let value1 = 1 as NSNumber let value2 = value1 let message = "expected to not be identical to \(identityAsString(value2)), got \(identityAsString(value1))" failsWithErrorMessage(message) { @@ -46,7 +46,7 @@ final class BeIdenticalToTest: XCTestCase { expect(1 as NSNumber).toNot(be("turtles" as NSString)) expect([1 as NSNumber] as NSArray).toNot(be([1 as NSNumber] as NSArray)) - let value1 = NSArray() + let value1 = 1 as NSNumber let value2 = value1 let message = "expected to not be identical to \(identityAsString(value1)), got \(identityAsString(value2))" failsWithErrorMessage(message) { diff --git a/Tests/NimbleTests/Matchers/EqualTest.swift b/Tests/NimbleTests/Matchers/EqualTest.swift index c0ccf764..0aa7c1d5 100644 --- a/Tests/NimbleTests/Matchers/EqualTest.swift +++ b/Tests/NimbleTests/Matchers/EqualTest.swift @@ -315,13 +315,24 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { @Sendable () async in originalArray } - await expect(originalArrayAsync).toEventually(equal(expectedArray)) - await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) - await expect(originalArrayAsync).toEventuallyNot(equal([])) - await expect(originalArrayAsync) == expectedArray - await expect(originalArrayAsync) != expectedArray.reversed() - await expect(originalArrayAsync) != [] + await expect { @Sendable () async in + originalArray + }.toEventually(equal(expectedArray)) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal(expectedArray.reversed())) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal([])) + await expect { @Sendable () async in + originalArray + } == expectedArray + await expect { @Sendable () async in + originalArray + } != expectedArray.reversed() + await expect { @Sendable () async in + originalArray + } != [] } func testTuple3Array() async { @@ -348,13 +359,24 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { @Sendable () async in originalArray } - await expect(originalArrayAsync).toEventually(equal(expectedArray)) - await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) - await expect(originalArrayAsync).toEventuallyNot(equal([])) - await expect(originalArrayAsync) == expectedArray - await expect(originalArrayAsync) != expectedArray.reversed() - await expect(originalArrayAsync) != [] + await expect { @Sendable () async in + originalArray + }.toEventually(equal(expectedArray)) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal(expectedArray.reversed())) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal([])) + await expect { @Sendable () async in + originalArray + } == expectedArray + await expect { @Sendable () async in + originalArray + } != expectedArray.reversed() + await expect { @Sendable () async in + originalArray + } != [] } func testTuple4Array() async { @@ -381,13 +403,24 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { @Sendable () async in originalArray } - await expect(originalArrayAsync).toEventually(equal(expectedArray)) - await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) - await expect(originalArrayAsync).toEventuallyNot(equal([])) - await expect(originalArrayAsync) == expectedArray - await expect(originalArrayAsync) != expectedArray.reversed() - await expect(originalArrayAsync) != [] + await expect { @Sendable () async in + originalArray + }.toEventually(equal(expectedArray)) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal(expectedArray.reversed())) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal([])) + await expect { @Sendable () async in + originalArray + } == expectedArray + await expect { @Sendable () async in + originalArray + } != expectedArray.reversed() + await expect { @Sendable () async in + originalArray + } != [] } func testTuple5Array() async { @@ -414,13 +447,24 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { @Sendable () async in originalArray } - await expect(originalArrayAsync).toEventually(equal(expectedArray)) - await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) - await expect(originalArrayAsync).toEventuallyNot(equal([])) - await expect(originalArrayAsync) == expectedArray - await expect(originalArrayAsync) != expectedArray.reversed() - await expect(originalArrayAsync) != [] + await expect { @Sendable () async in + originalArray + }.toEventually(equal(expectedArray)) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal(expectedArray.reversed())) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal([])) + await expect { @Sendable () async in + originalArray + } == expectedArray + await expect { @Sendable () async in + originalArray + } != expectedArray.reversed() + await expect { @Sendable () async in + originalArray + } != [] } func testTuple6Array() async { @@ -447,13 +491,24 @@ final class EqualTest: XCTestCase { // swiftlint:disable:this type_body_length expect(originalArray) != expectedArray.reversed() expect(originalArray) != [] - let originalArrayAsync = { @Sendable () async in originalArray } - await expect(originalArrayAsync).toEventually(equal(expectedArray)) - await expect(originalArrayAsync).toEventuallyNot(equal(expectedArray.reversed())) - await expect(originalArrayAsync).toEventuallyNot(equal([])) - await expect(originalArrayAsync) == expectedArray - await expect(originalArrayAsync) != expectedArray.reversed() - await expect(originalArrayAsync) != [] + await expect { @Sendable () async in + originalArray + }.toEventually(equal(expectedArray)) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal(expectedArray.reversed())) + await expect { @Sendable () async in + originalArray + }.toEventuallyNot(equal([])) + await expect { @Sendable () async in + originalArray + } == expectedArray + await expect { @Sendable () async in + originalArray + } != expectedArray.reversed() + await expect { @Sendable () async in + originalArray + } != [] } // swiftlint:enable large_tuple diff --git a/Tests/NimbleTests/Matchers/NegationTest.swift b/Tests/NimbleTests/Matchers/NegationTest.swift index 45d35f39..4929988b 100644 --- a/Tests/NimbleTests/Matchers/NegationTest.swift +++ b/Tests/NimbleTests/Matchers/NegationTest.swift @@ -22,7 +22,7 @@ final class NegationTest: XCTestCase { } func testAsyncNil() async { - @Sendable func nilFunc() async -> Int? { + @Sendable func nilFunc() async -> sending Int? { nil } From 7a336bb292a8dac85639441284479e8574febb6a Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 13 Oct 2024 22:46:55 -0700 Subject: [PATCH 25/25] Remove usage of the swift 6 only sending keyword (#1168) --- Sources/Nimble/Adapters/NMBExpectation.swift | 6 +-- Sources/Nimble/AsyncExpression.swift | 14 +++---- Sources/Nimble/DSL+AsyncAwait.swift | 16 ++++---- Sources/Nimble/DSL+Require.swift | 40 +++++++++---------- Sources/Nimble/DSL.swift | 16 ++++---- Sources/Nimble/Expression.swift | 16 ++++---- Sources/Nimble/Matchers/Equal.swift | 2 +- Sources/Nimble/Matchers/Matcher.swift | 2 +- Sources/Nimble/Polling+Require.swift | 4 +- Sources/Nimble/Utils/PollAwait.swift | 2 +- Tests/NimbleTests/Matchers/NegationTest.swift | 2 +- 11 files changed, 60 insertions(+), 60 deletions(-) diff --git a/Sources/Nimble/Adapters/NMBExpectation.swift b/Sources/Nimble/Adapters/NMBExpectation.swift index 6ca25fb7..be1768d5 100644 --- a/Sources/Nimble/Adapters/NMBExpectation.swift +++ b/Sources/Nimble/Adapters/NMBExpectation.swift @@ -14,13 +14,13 @@ private func from(objcMatcher: NMBMatcher) -> Matcher { // Equivalent to Expectation, but for Nimble's Objective-C interface public final class NMBExpectation: NSObject, Sendable { - internal let _actualBlock: @Sendable () -> sending NSObject? + internal let _actualBlock: @Sendable () -> NSObject? internal let _negative: Bool internal let _file: FileString internal let _line: UInt internal let _timeout: NimbleTimeInterval - @objc public init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt) { + @objc public init(actualBlock: @escaping @Sendable () -> NSObject?, negative: Bool, file: FileString, line: UInt) { self._actualBlock = actualBlock self._negative = negative self._file = file @@ -28,7 +28,7 @@ public final class NMBExpectation: NSObject, Sendable { self._timeout = .seconds(1) } - private init(actualBlock: @escaping @Sendable () -> sending NSObject?, negative: Bool, file: FileString, line: UInt, timeout: NimbleTimeInterval) { + private init(actualBlock: @escaping @Sendable () -> NSObject?, negative: Bool, file: FileString, line: UInt, timeout: NimbleTimeInterval) { self._actualBlock = actualBlock self._negative = negative self._file = file diff --git a/Sources/Nimble/AsyncExpression.swift b/Sources/Nimble/AsyncExpression.swift index 7eb0d228..22c366cf 100644 --- a/Sources/Nimble/AsyncExpression.swift +++ b/Sources/Nimble/AsyncExpression.swift @@ -67,7 +67,7 @@ private final class MemoizedClosure: Sendable { // Memoizes the given closure, only calling the passed // closure once; even if repeat calls to the returned closure private func memoizedClosure( - _ closure: sending @escaping @Sendable () async throws -> T + _ closure: @escaping @Sendable () async throws -> T ) -> @Sendable (Bool) async throws -> T { let memoized = MemoizedClosure(closure) return memoized.callAsFunction(_:) @@ -85,7 +85,7 @@ private func memoizedClosure( /// This provides a common consumable API for matchers to utilize to allow /// Nimble to change internals to how the captured closure is managed. public actor AsyncExpression { - internal let _expression: @Sendable (Bool) async throws -> sending Value? + internal let _expression: @Sendable (Bool) async throws -> Value? internal let _withoutCaching: Bool public let location: SourceLocation public let isClosure: Bool @@ -101,7 +101,7 @@ public actor AsyncExpression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: sending @escaping @Sendable () async throws -> sending Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: @escaping @Sendable () async throws -> Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -122,7 +122,7 @@ public actor AsyncExpression { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(memoizedExpression: @escaping @Sendable (Bool) async throws -> sending Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { + public init(memoizedExpression: @escaping @Sendable (Bool) async throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { self._expression = memoizedExpression self.location = location self._withoutCaching = withoutCaching @@ -153,7 +153,7 @@ public actor AsyncExpression { /// /// - Parameter block: The block that can cast the current Expression value to a /// new type. - public func cast(_ block: @escaping @Sendable (Value?) throws -> sending U?) -> AsyncExpression { + public func cast(_ block: @escaping @Sendable (Value?) throws -> U?) -> AsyncExpression { AsyncExpression( expression: ({ try await block(self.evaluate()) }), location: self.location, @@ -161,7 +161,7 @@ public actor AsyncExpression { ) } - public func cast(_ block: @escaping @Sendable (Value?) async throws -> sending U?) -> AsyncExpression { + public func cast(_ block: @escaping @Sendable (Value?) async throws -> U?) -> AsyncExpression { AsyncExpression( expression: ({ try await block(self.evaluate()) }), location: self.location, @@ -169,7 +169,7 @@ public actor AsyncExpression { ) } - public func evaluate() async throws -> sending Value? { + public func evaluate() async throws -> Value? { try await self._expression(_withoutCaching) } diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index 3fb6e6a1..c7deecc9 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> sending T?) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -12,7 +12,7 @@ public func expect(fileID: String = #fileID, file: FileString = #fi } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> sending T)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -21,7 +21,7 @@ public func expect(fileID: String = #fileID, file: FileString = #fi } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> sending T?)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> T?)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -30,7 +30,7 @@ public func expect(fileID: String = #fileID, file: FileString = #fi } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> sending Void)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @Sendable () -> (@Sendable () async throws -> Void)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -40,7 +40,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> sending T?) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, @@ -50,7 +50,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #f /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> sending T)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -60,7 +60,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #f /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> sending T?)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> T?)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), @@ -70,7 +70,7 @@ public func expecta(fileID: String = #fileID, file: FileString = #f /// 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(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> sending Void)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @Sendable () -> (@Sendable () async throws -> Void)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index 952faddd..b0b72985 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -3,7 +3,7 @@ /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, @@ -17,7 +17,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -31,7 +31,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -45,7 +45,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, l /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -61,7 +61,7 @@ public func require(fileID: String = #fileID, file: FileString = #filePath, line /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, @@ -77,7 +77,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -93,7 +93,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -109,7 +109,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), @@ -123,7 +123,7 @@ public func requires(fileID: String = #fileID, file: FileString = #filePath, lin /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping @Sendable () async throws -> sending T?) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -137,7 +137,7 @@ public func require(fileID: String = #fileID, file: FileString = #f /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> sending T)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> T)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -151,7 +151,7 @@ public func require(fileID: String = #fileID, file: FileString = #f /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> sending T?)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> T?)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -167,7 +167,7 @@ public func require(fileID: String = #fileID, file: FileString = #f /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement`. @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () async throws -> sending T?) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -183,7 +183,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> sending T)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -199,7 +199,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> sending T?)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -216,7 +216,7 @@ public func requirea(fileID: String = #fileID, file: FileString = # /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> T?) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -226,7 +226,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -236,7 +236,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #filePath, li /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) throws -> T { +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure @escaping @Sendable () throws -> T?) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -246,7 +246,7 @@ public func unwraps(fileID: String = #fileID, file: FileString = #filePath, l /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -266,7 +266,7 @@ public func unwrap(fileID: String = #fileID, file: FileString = #fi /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: () -> (@Sendable () async throws -> sending T?)) async throws -> T { +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } @@ -286,7 +286,7 @@ public func unwrapa(fileID: String = #fileID, file: FileString = #f /// `unwrapa` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> sending T?)) async throws -> T { +public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } diff --git a/Sources/Nimble/DSL.swift b/Sources/Nimble/DSL.swift index c93719ac..7595522e 100644 --- a/Sources/Nimble/DSL.swift +++ b/Sources/Nimble/DSL.swift @@ -1,5 +1,5 @@ /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () throws -> T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, @@ -8,7 +8,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -17,7 +17,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -26,7 +26,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, li } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -36,7 +36,7 @@ public func expect(fileID: String = #fileID, file: FileString = #filePath, line: /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () throws -> sending T?) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping @Sendable () throws -> T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, @@ -46,7 +46,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -56,7 +56,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), @@ -66,7 +66,7 @@ public func expects(fileID: String = #fileID, file: FileString = #filePath, l /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> sending Void)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (@Sendable () throws -> Void)) -> SyncExpectation { // It would seem like `sending` isn't necessary for the `expression` argument // because the closure returns void. However, this gets rid of a type // conversion warning/error. diff --git a/Sources/Nimble/Expression.swift b/Sources/Nimble/Expression.swift index c41c7b5e..8b91d439 100644 --- a/Sources/Nimble/Expression.swift +++ b/Sources/Nimble/Expression.swift @@ -3,13 +3,13 @@ import Foundation private final class MemoizedValue: Sendable { private let lock = NSRecursiveLock() nonisolated(unsafe) private var cache: T? = nil - private let closure: @Sendable () throws -> sending T + private let closure: @Sendable () throws -> T - init(_ closure: @escaping @Sendable () throws -> sending T) { + init(_ closure: @escaping @Sendable () throws -> T) { self.closure = closure } - @Sendable func evaluate(withoutCaching: Bool) throws -> sending T { + @Sendable func evaluate(withoutCaching: Bool) throws -> T { try lock.withLock { if withoutCaching || cache == nil { cache = try closure() @@ -21,7 +21,7 @@ private final class MemoizedValue: Sendable { // Memoizes the given closure, only calling the passed // closure once; even if repeat calls to the returned closure -private func memoizedClosure(_ closure: @escaping @Sendable () throws -> sending T) -> @Sendable (Bool) throws -> sending T { +private func memoizedClosure(_ closure: @escaping @Sendable () throws -> T) -> @Sendable (Bool) throws -> T { MemoizedValue(closure).evaluate(withoutCaching:) } @@ -37,7 +37,7 @@ private func memoizedClosure(_ closure: @escaping @Sendable () throws -> send /// 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 Expression: Sendable { - internal let _expression: @Sendable (Bool) throws -> sending Value? + internal let _expression: @Sendable (Bool) throws -> Value? internal let _withoutCaching: Bool public let location: SourceLocation public let isClosure: Bool @@ -53,7 +53,7 @@ public struct Expression: Sendable { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(expression: @escaping @Sendable () throws -> sending Value?, location: SourceLocation, isClosure: Bool = true) { + public init(expression: @escaping @Sendable () throws -> Value?, location: SourceLocation, isClosure: Bool = true) { self._expression = memoizedClosure(expression) self.location = location self._withoutCaching = false @@ -74,7 +74,7 @@ public struct Expression: Sendable { /// requires an explicit closure. This gives Nimble /// flexibility if @autoclosure behavior changes between /// Swift versions. Nimble internals always sets this true. - public init(memoizedExpression: @escaping @Sendable (Bool) throws -> sending Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { + public init(memoizedExpression: @escaping @Sendable (Bool) throws -> Value?, location: SourceLocation, withoutCaching: Bool, isClosure: Bool = true) { self._expression = memoizedExpression self.location = location self._withoutCaching = withoutCaching @@ -89,7 +89,7 @@ public struct Expression: Sendable { /// /// - Parameter block: The block that can cast the current Expression value to a /// new type. - public func cast(_ block: @escaping @Sendable (Value?) throws -> sending U?) -> Expression { + public func cast(_ block: @escaping @Sendable (Value?) throws -> U?) -> Expression { Expression( expression: ({ try block(self.evaluate()) }), location: self.location, diff --git a/Sources/Nimble/Matchers/Equal.swift b/Sources/Nimble/Matchers/Equal.swift index 1a5ce017..4d4c00b0 100644 --- a/Sources/Nimble/Matchers/Equal.swift +++ b/Sources/Nimble/Matchers/Equal.swift @@ -44,7 +44,7 @@ public func equal(_ expectedValue: [T?]) -> Matcher<[T?]> { /// /// @see beCloseTo if you want to match imprecise types (eg - floats, doubles). public func equal(_ expectedValue: T?) -> Matcher { - equal(expectedValue, by: ==) + equal(expectedValue, by: { $0 == $1 }) } /// A Nimble matcher that succeeds when the actual set is equal to the expected set. diff --git a/Sources/Nimble/Matchers/Matcher.swift b/Sources/Nimble/Matchers/Matcher.swift index 470985f6..ffb5798b 100644 --- a/Sources/Nimble/Matchers/Matcher.swift +++ b/Sources/Nimble/Matchers/Matcher.swift @@ -225,7 +225,7 @@ public class NMBMatcher: NSObject, @unchecked Sendable { self.init(matcher: predicate) } - func satisfies(_ expression: @escaping @Sendable () throws -> sending NSObject?, location: SourceLocation) -> NMBMatcherResult { + func satisfies(_ expression: @escaping @Sendable () throws -> NSObject?, location: SourceLocation) -> NMBMatcherResult { let expr = Expression(expression: expression, location: location) do { return try self.matcher(expr) diff --git a/Sources/Nimble/Polling+Require.swift b/Sources/Nimble/Polling+Require.swift index 4f42f442..17a3fffa 100644 --- a/Sources/Nimble/Polling+Require.swift +++ b/Sources/Nimble/Polling+Require.swift @@ -715,7 +715,7 @@ public func pollUnwrap(file: FileString = #file, line: UInt = #line, _ expres /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } @@ -729,7 +729,7 @@ public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. /// As you can tell, this is a much less verbose equivalent to `require(expression).toEventuallyNot(beNil())` @discardableResult -public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> sending T?)) throws -> T { +public func pollUnwraps(file: FileString = #file, line: UInt = #line, timeout: NimbleTimeInterval = PollingDefaults.timeout, pollInterval: NimbleTimeInterval = PollingDefaults.pollInterval, description: String? = nil, _ expression: @autoclosure () -> (@Sendable () throws -> T?)) throws -> T { try require(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index b56eef6a..5f591346 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -316,7 +316,7 @@ internal class Awaiter { func performBlock( file: FileString, line: UInt, - _ closure: sending @escaping (@escaping @Sendable (T) -> Void) throws -> Void + _ closure: @escaping (@escaping @Sendable (T) -> Void) throws -> Void ) -> AwaitPromiseBuilder { let promise = AwaitPromise() let timeoutSource = createTimerSource(timeoutQueue) diff --git a/Tests/NimbleTests/Matchers/NegationTest.swift b/Tests/NimbleTests/Matchers/NegationTest.swift index 4929988b..45d35f39 100644 --- a/Tests/NimbleTests/Matchers/NegationTest.swift +++ b/Tests/NimbleTests/Matchers/NegationTest.swift @@ -22,7 +22,7 @@ final class NegationTest: XCTestCase { } func testAsyncNil() async { - @Sendable func nilFunc() async -> sending Int? { + @Sendable func nilFunc() async -> Int? { nil }