From 104b50cc38f6abec7af4dfa7e750be107649e7d9 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 17 Mar 2024 12:42:08 -0700 Subject: [PATCH] Update Require DSL to largely be Sendable --- 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 | 8 ++++---- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index d21c39f1d..c04bbfcb2 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -123,7 +123,7 @@ public func requires(file: FileString = #file, line: UInt = #line, customError: /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @escaping () async throws -> T?) -> AsyncRequirement { +public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @escaping @Sendable () async throws -> T?) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, @@ -137,7 +137,7 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (() async throws -> T)) -> AsyncRequirement { +public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> T)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -151,7 +151,7 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) -> AsyncRequirement { +public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> T?)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -167,7 +167,7 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement`. @discardableResult -public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncRequirement { +public func requirea(file: FileString = #file, line: UInt = #line, 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(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncRequirement { +public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -199,7 +199,7 @@ public func requirea(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncRequirement { +public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), @@ -256,7 +256,7 @@ public func unwraps(file: FileString = #file, line: UInt = #line, customError /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @escaping () async throws -> T?) async throws -> T { +public func unwrap(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @escaping @Sendable () async throws -> T?) async throws -> T { try await requirea(file: file, line: line, customError: customError, try await expression()).toNot(beNil()) } @@ -266,7 +266,7 @@ public func unwrap(file: FileString = #file, line: UInt = #line, customError: /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { +public func unwrap(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(file: file, line: line, customError: customError, expression()).toNot(beNil()) } @@ -276,7 +276,7 @@ public func unwrap(file: FileString = #file, line: UInt = #line, customError: /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { +public func unwrapa(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async throws -> T { try await requirea(file: file, line: line, customError: customError, try await expression()).toNot(beNil()) } @@ -286,6 +286,6 @@ public func unwrapa(file: FileString = #file, line: UInt = #line, customError /// `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(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { +public func unwrapa(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(file: file, line: line, customError: customError, expression()).toNot(beNil()) } diff --git a/Sources/Nimble/Matchers/PostNotification.swift b/Sources/Nimble/Matchers/PostNotification.swift index 6601b68a7..fd00d1a22 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 7f9c9268d..09ee815d4 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, _ expre /// 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, _ expression: @escaping () async throws -> T?) async throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, _ expression: @escaping @Sendable () async throws -> T?) async throws -> T { try await requirea(file: file, line: line, try await expression()).toEventuallyNot(beNil()) } /// 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, _ expression: () -> (() async throws -> T?)) async throws -> T { +public func pollUnwrap(file: FileString = #file, line: UInt = #line, _ expression: () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil()) } /// 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, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { +public func pollUnwrapa(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping @Sendable () async throws -> T?) async throws -> T { try await requirea(file: file, line: line, try await expression()).toEventuallyNot(beNil()) } /// 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, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { +public func pollUnwrapa(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (@Sendable () async throws -> T?)) async throws -> T { try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil()) } diff --git a/Sources/Nimble/Requirement.swift b/Sources/Nimble/Requirement.swift index 91c8487da..d03cdcc33 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 7925cc36d..1f5503a4a 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 } @@ -49,7 +49,7 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b } 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 } @@ -141,7 +141,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()) @@ -153,7 +153,7 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b 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 } + @Sendable func isMainThread() -> Bool { Thread.isMainThread } try await requirea(isMainThread()).toEventually(beTrue()) try await requirea(isMainThread()).toEventuallyNot(beFalse())