From f459a201e9f5dd9456ad372f9c09df474104a959 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Thu, 10 Oct 2024 22:38:12 -0700 Subject: [PATCH 01/18] Allow unwrap and pollUnwrap to take in custom descriptions (#1162) Also allow pollUnwrap to take in custom timeout and polling intervals. Fixes a bug in the async version of withAssertionRecorder where we didn't reset the assertion recorder after tests --- .../Adapters/AssertionRecorder+Async.swift | 6 ++- .../Nimble/Adapters/AssertionRecorder.swift | 2 + Sources/Nimble/DSL+Require.swift | 32 +++++++------- Sources/Nimble/Polling+Require.swift | 28 ++++++------- .../NimbleTests/AsyncAwaitTest+Require.swift | 2 +- Tests/NimbleTests/AsyncAwaitTest.swift | 2 +- Tests/NimbleTests/DSLTest.swift | 42 +++++++++++++++++++ Tests/NimbleTests/Helpers/AsyncHelpers.swift | 2 +- .../Matchers/AsyncAllPassTest.swift | 11 +++++ Tests/NimbleTests/Matchers/NegationTest.swift | 2 +- .../Matchers/SatisfyAllOfTest.swift | 4 +- .../Matchers/SatisfyAnyOfTest.swift | 4 +- Tests/NimbleTests/PollingTest+Require.swift | 32 ++++++++++++++ 13 files changed, 129 insertions(+), 40 deletions(-) diff --git a/Sources/Nimble/Adapters/AssertionRecorder+Async.swift b/Sources/Nimble/Adapters/AssertionRecorder+Async.swift index f6d3281be..286be5f2d 100644 --- a/Sources/Nimble/Adapters/AssertionRecorder+Async.swift +++ b/Sources/Nimble/Adapters/AssertionRecorder+Async.swift @@ -15,13 +15,15 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler, closure: () async throws -> Void) async { let environment = NimbleEnvironment.activeInstance let oldRecorder = environment.assertionHandler - _ = NMBExceptionCapture(handler: nil, finally: ({ + defer { environment.assertionHandler = oldRecorder - })) + } environment.assertionHandler = tempAssertionHandler do { try await closure() + } catch is RequireError { + // ignore this } catch { let failureMessage = FailureMessage() failureMessage.stringValue = "unexpected error thrown: <\(error)>" diff --git a/Sources/Nimble/Adapters/AssertionRecorder.swift b/Sources/Nimble/Adapters/AssertionRecorder.swift index adb869132..0ee397219 100644 --- a/Sources/Nimble/Adapters/AssertionRecorder.swift +++ b/Sources/Nimble/Adapters/AssertionRecorder.swift @@ -79,6 +79,8 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler, try capturer.tryBlockThrows { try closure() } + } catch is RequireError { + // specifically ignore RequireError, will be caught by the assertion handler. } catch { let failureMessage = FailureMessage() failureMessage.stringValue = "unexpected error thrown: <\(error)>" diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index 189d23ef7..4df5c5b4d 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -216,8 +216,8 @@ public func requirea(fileID: String = #fileID, file: FileString = #filePath, /// `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, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { - try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) +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 { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. @@ -226,8 +226,8 @@ 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, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { - try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) +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 { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. @@ -236,8 +236,8 @@ 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, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { - try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) +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 { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. @@ -246,8 +246,8 @@ 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, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { - try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) +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 { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil(), description: description) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -256,8 +256,8 @@ 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, _ 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()) +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) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -266,8 +266,8 @@ 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, _ expression: () -> (() async throws -> T?)) async throws -> T { - try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) +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) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -276,8 +276,8 @@ 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, _ 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()) +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) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -286,6 +286,6 @@ 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, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { - try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) +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/Polling+Require.swift b/Sources/Nimble/Polling+Require.swift index 7f9c9268d..673ce70b9 100644 --- a/Sources/Nimble/Polling+Require.swift +++ b/Sources/Nimble/Polling+Require.swift @@ -713,50 +713,50 @@ 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, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { - try require(file: file, line: line, expression()).toEventuallyNot(beNil()) +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 { + 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, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { - try require(file: file, line: line, expression()).toEventuallyNot(beNil()) +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 { + 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, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { - try require(file: file, line: line, expression()).toEventuallyNot(beNil()) +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 { + try require(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 pollUnwrap(file: FileString = #file, line: UInt = #line, _ expression: @escaping () async throws -> T?) async throws -> T { - try await requirea(file: file, line: line, try await expression()).toEventuallyNot(beNil()) +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 { + 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, _ expression: () -> (() async throws -> T?)) async throws -> T { - try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil()) +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 { + 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, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { - try await requirea(file: file, line: line, try await expression()).toEventuallyNot(beNil()) +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 { + 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, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { - try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil()) +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 { + try await requirea(file: file, line: line, expression()).toEventuallyNot(beNil(), timeout: timeout, pollInterval: pollInterval, description: description) } #endif // #if !os(WASI) diff --git a/Tests/NimbleTests/AsyncAwaitTest+Require.swift b/Tests/NimbleTests/AsyncAwaitTest+Require.swift index 7925cc36d..f6dfd712d 100644 --- a/Tests/NimbleTests/AsyncAwaitTest+Require.swift +++ b/Tests/NimbleTests/AsyncAwaitTest+Require.swift @@ -59,7 +59,7 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b } func testPollUnwrapNegativeCase() async { - await failsWithErrorMessage("expected to eventually not be nil, got nil") { + await failsWithErrorMessage("expected to eventually not be nil, got ") { try await pollUnwrap { nil as Int? } } await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { diff --git a/Tests/NimbleTests/AsyncAwaitTest.swift b/Tests/NimbleTests/AsyncAwaitTest.swift index 8fcbcbc3e..d88d4b9a3 100644 --- a/Tests/NimbleTests/AsyncAwaitTest.swift +++ b/Tests/NimbleTests/AsyncAwaitTest.swift @@ -233,7 +233,7 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len } func testWaitUntilDetectsStalledMainThreadActivity() async { - let msg = "-waitUntil() timed out but was unable to run the timeout handler because the main thread is unresponsive (0.5 seconds is allow after the wait times out). Conditions that may cause this include processing blocking IO on the main thread, calls to sleep(), deadlocks, and synchronous IPC. Nimble forcefully stopped run loop which may cause future failures in test run." + let msg = "Waited more than 1.0 second" await failsWithErrorMessage(msg) { await waitUntil(timeout: .seconds(1)) { done in Thread.sleep(forTimeInterval: 3.0) diff --git a/Tests/NimbleTests/DSLTest.swift b/Tests/NimbleTests/DSLTest.swift index d164226c2..1c828686e 100644 --- a/Tests/NimbleTests/DSLTest.swift +++ b/Tests/NimbleTests/DSLTest.swift @@ -1,5 +1,8 @@ import XCTest import Nimble +#if SWIFT_PACKAGE +import NimbleSharedTestHelpers +#endif private func nonThrowingInt() -> Int { return 1 @@ -177,4 +180,43 @@ final class DSLTest: XCTestCase { expect(records.first?.success).to(beFalse()) expect(records.last?.success).to(beTrue()) } + + func testUnwrap() { + expect { try unwrap(Optional.some(1)) }.to(equal(1)) + + failsWithErrorMessage("expected to not be nil, got ") { + try unwrap(nil as Int?) + } + failsWithErrorMessage("expected to not be nil, got ") { + try unwraps(nil as Int?) + } + failsWithErrorMessage("Custom User Message\nexpected to not be nil, got ") { + try unwrap(description: "Custom User Message", nil as Int?) + } + failsWithErrorMessage("Custom User Message 2\nexpected to not be nil, got ") { + try unwraps(description: "Custom User Message 2", nil as Int?) + } + } + + func testUnwrapAsync() async { + @Sendable func asyncOptional(_ value: Int?) async -> Int? { + value + } + + await expect { try await unwrap { await asyncOptional(1) } }.to(equal(1)) + + await failsWithErrorMessage("expected to not be nil, got ") { + try await unwrap { await asyncOptional(nil) } + } + await failsWithErrorMessage("expected to not be nil, got ") { + try await unwrapa(await asyncOptional(nil)) + } + + await failsWithErrorMessage("Some Message\nexpected to not be nil, got ") { + try await unwrap(description: "Some Message") { await asyncOptional(nil) } + } + await failsWithErrorMessage("Other Message\nexpected to not be nil, got ") { + try await unwrapa(description: "Other Message", await asyncOptional(nil)) + } + } } diff --git a/Tests/NimbleTests/Helpers/AsyncHelpers.swift b/Tests/NimbleTests/Helpers/AsyncHelpers.swift index 2770e359f..5b2068218 100644 --- a/Tests/NimbleTests/Helpers/AsyncHelpers.swift +++ b/Tests/NimbleTests/Helpers/AsyncHelpers.swift @@ -53,7 +53,7 @@ final class AsyncMatchersTest: XCTestCase { await expect(1).to(asyncEqual(1)) await expect(2).toNot(asyncEqual(1)) - await failsWithErrorMessage("expected to equal 1, got 2") { + await failsWithErrorMessage("expected to equal 1, got <2>") { await expect(2).to(asyncEqual(1)) } } diff --git a/Tests/NimbleTests/Matchers/AsyncAllPassTest.swift b/Tests/NimbleTests/Matchers/AsyncAllPassTest.swift index e6d22f42f..5c4ec502e 100644 --- a/Tests/NimbleTests/Matchers/AsyncAllPassTest.swift +++ b/Tests/NimbleTests/Matchers/AsyncAllPassTest.swift @@ -26,9 +26,20 @@ private func asyncBeGreaterThan(_ expectedValue: T?) -> AsyncMatc } } +private protocol _OptionalProtocol { + var isNil: Bool { get } +} + +extension Optional: _OptionalProtocol { + var isNil: Bool { self == nil } +} + private func asyncBeNil() -> AsyncMatcher { return AsyncMatcher.simpleNilable("be nil") { actualExpression in let actualValue = try await actualExpression.evaluate() + if let actual = actualValue, let nestedOptionl = actual as? _OptionalProtocol { + return MatcherStatus(bool: nestedOptionl.isNil) + } return MatcherStatus(bool: actualValue == nil) } } diff --git a/Tests/NimbleTests/Matchers/NegationTest.swift b/Tests/NimbleTests/Matchers/NegationTest.swift index a05ea7fbb..45d35f39a 100644 --- a/Tests/NimbleTests/Matchers/NegationTest.swift +++ b/Tests/NimbleTests/Matchers/NegationTest.swift @@ -36,7 +36,7 @@ final class NegationTest: XCTestCase { func testAsyncNonNil() async { await expect(1).to(not(asyncEqual(2))) - await failsWithErrorMessage("expected to not equal <2>, got <2>") { + await failsWithErrorMessage("expected to not equal 2, got <2>") { await expect(2).to(not(asyncEqual(2))) } } diff --git a/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift b/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift index 73bebac0d..e2563581a 100644 --- a/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift +++ b/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift @@ -83,11 +83,11 @@ final class SatisfyAllOfTest: XCTestCase { await expect(true).toNot(satisfyAllOf(beTruthy(), beFalsy(), asyncEqual(true))) await failsWithErrorMessage( - "expected to match all of: {equal <3>}, and {equal <4>}, and {equal <5>}, got 2") { + "expected to match all of: {equal 3}, and {equal 4}, and {equal 5}, got 2") { await expect(2).to(satisfyAllOf(asyncEqual(3), asyncEqual(4), asyncEqual(5))) } await failsWithErrorMessage( - "expected to match all of: {all be less than 4, but failed first at element <5> in <[5, 6, 7]>}, and {equal <[5, 6, 7]>}, got [5, 6, 7]") { + "expected to match all of: {all be less than 4, but failed first at element <5> in <[5, 6, 7]>}, and {equal [5, 6, 7]}, got [5, 6, 7]") { await expect([5, 6, 7]).to(satisfyAllOf(allPass("be less than 4", {$0 < 4}), asyncEqual([5, 6, 7]))) } await failsWithErrorMessage( diff --git a/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift b/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift index f908d1b3b..f02c35206 100644 --- a/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift +++ b/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift @@ -83,11 +83,11 @@ final class SatisfyAnyOfTest: XCTestCase { await expect(true).to(satisfyAnyOf(beTruthy(), beFalsy(), asyncEqual(false), asyncEqual(true))) await failsWithErrorMessage( - "expected to match one of: {equal <3>}, or {equal <4>}, or {equal <5>}, got 2") { + "expected to match one of: {equal 3}, or {equal 4}, or {equal 5}, got 2") { await expect(2).to(satisfyAnyOf(asyncEqual(3), asyncEqual(4), asyncEqual(5))) } await failsWithErrorMessage( - "expected to match one of: {all be less than 4, but failed first at element <5> in <[5, 6, 7]>}, or {equal <[1, 2, 3, 4]>}, got [5, 6, 7]") { + "expected to match one of: {all be less than 4, but failed first at element <5> in <[5, 6, 7]>}, or {equal [1, 2, 3, 4]}, got [5, 6, 7]") { await expect([5, 6, 7]).to(satisfyAnyOf(allPass("be less than 4", {$0 < 4}), asyncEqual([1, 2, 3, 4]))) } await failsWithErrorMessage( diff --git a/Tests/NimbleTests/PollingTest+Require.swift b/Tests/NimbleTests/PollingTest+Require.swift index c91f2c590..7bdc48464 100644 --- a/Tests/NimbleTests/PollingTest+Require.swift +++ b/Tests/NimbleTests/PollingTest+Require.swift @@ -214,6 +214,38 @@ final class PollingRequireTest: XCTestCase { try require(nil).toAlways(equal(1)) } } + + func testPollUnwrapMessage() { + failsWithErrorMessage("expected to eventually not be nil, got ") { + try pollUnwrap(timeout: .milliseconds(100)) { nil as Int? } + } + + failsWithErrorMessage("Custom Message\nexpected to eventually not be nil, got ") { + try pollUnwrap(timeout: .milliseconds(100), description: "Custom Message") { nil as Int? } + } + + failsWithErrorMessage("Custom Message 2\nexpected to eventually not be nil, got ") { + try pollUnwraps(timeout: .milliseconds(100), description: "Custom Message 2") { nil as Int? } + } + } + + func testPollUnwrapMessageAsync() async { + @Sendable func asyncOptional(_ value: Int?) async -> Int? { + value + } + + await failsWithErrorMessage("expected to eventually not be nil, got ") { + try await pollUnwrap(timeout: .milliseconds(100)) { await asyncOptional(nil) as Int? } + } + + await failsWithErrorMessage("Custom Message\nexpected to eventually not be nil, got ") { + try await pollUnwrap(timeout: .milliseconds(100), description: "Custom Message") { await asyncOptional(nil) as Int? } + } + + await failsWithErrorMessage("Custom Message 2\nexpected to eventually not be nil, got ") { + try await pollUnwrapa(timeout: .milliseconds(100), description: "Custom Message 2") { await asyncOptional(nil) as Int? } + } + } } #endif // #if !os(WASI) From 61dcf0cf18f3c41ae526c9886bcbfd8f71e820cc Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 11 Oct 2024 06:53:21 -0700 Subject: [PATCH 02/18] Add requireFail. Like fail(), but it also always throws an error (#1163) --- Sources/Nimble/DSL+Require.swift | 14 ++++++++++++++ Tests/NimbleTests/DSLTest.swift | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index 4df5c5b4d..bd73f72f6 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -289,3 +289,17 @@ public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, l 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) } + +/// Always fails the test and throw an error to prevent further test execution. +/// +/// - Parameter message: A custom message to use in place of the default one. +/// - Parameter customError: A custom error to throw in place of a ``RequireError``. +public func requireFail(_ message: String? = nil, customError: Error? = nil, fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column) throws { + let location = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column) + let handler = NimbleEnvironment.activeInstance.assertionHandler + + let msg = message ?? "requireFail() always fails" + handler.assert(false, message: FailureMessage(stringValue: msg), location: location) + + throw customError ?? RequireError(message: msg, location: location) +} diff --git a/Tests/NimbleTests/DSLTest.swift b/Tests/NimbleTests/DSLTest.swift index 1c828686e..12d3f97a0 100644 --- a/Tests/NimbleTests/DSLTest.swift +++ b/Tests/NimbleTests/DSLTest.swift @@ -219,4 +219,32 @@ final class DSLTest: XCTestCase { try await unwrapa(description: "Other Message", await asyncOptional(nil)) } } + + func testRequireFail() throws { + struct MyCustomError: Error {} + + failsWithErrorMessage("requireFail() always fails") { + do { + try requireFail() + } catch { + expect(error as? RequireError).toNot(beNil()) + } + } + + failsWithErrorMessage("Custom error message") { + do { + try requireFail("Custom error message") + } catch { + expect(error as? RequireError).toNot(beNil()) + } + } + + failsWithErrorMessage("Custom message with custom error") { + do { + try requireFail("Custom message with custom error", customError: MyCustomError()) + } catch { + expect(error as? MyCustomError).toNot(beNil()) + } + } + } } From 566294c3d05b88506ab63423d7225b5cbebce27d Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 13 Oct 2024 16:39:16 -0700 Subject: [PATCH 03/18] Fix build error when using UI Tests (#1164) Thanks PointFree for documenting a fix. --- Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift b/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift index ebd2aa11c..bf86bb6d5 100644 --- a/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift +++ b/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift @@ -1,6 +1,10 @@ import Foundation #if canImport(Testing) -import Testing +// See https://github.com/pointfreeco/swift-snapshot-testing/discussions/901#discussioncomment-10605497 +// tl;dr: Swift Testing is not available when using UI tests. +// And apparently `private import` - the preferred way to do this - doesn't work. +// So we use a deprecated approach that does work with this. +@_implementationOnly import Testing #endif public class NimbleSwiftTestingHandler: AssertionHandler { From 6416749c3c0488664fff6b42f8bf3ea8dc282ca1 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 13 Oct 2024 16:40:15 -0700 Subject: [PATCH 04/18] [v13.6.0] Update podspec --- Nimble.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nimble.podspec b/Nimble.podspec index a16a67d65..2c9865bb1 100644 --- a/Nimble.podspec +++ b/Nimble.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Nimble" - s.version = "13.5.0" + s.version = "13.6.0" s.summary = "A Matcher Framework for Swift and Objective-C" s.description = <<-DESC Use Nimble to express the expected outcomes of Swift or Objective-C expressions. Inspired by Cedar. From 47ba9eccf172dcd495b0f423031fa23c69024d95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:10:24 -0700 Subject: [PATCH 05/18] Bump rexml from 3.3.6 to 3.3.9 (#1171) Bumps [rexml](https://github.com/ruby/rexml) from 3.3.6 to 3.3.9. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4645ea74a..fc4d6cbdb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,8 +97,7 @@ GEM open4 (1.3.4) public_suffix (4.0.7) redcarpet (3.6.0) - rexml (3.3.6) - strscan + rexml (3.3.9) rouge (4.2.0) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -106,7 +105,6 @@ GEM ffi (~> 1.9) sqlite3 (1.6.9) mini_portile2 (~> 2.8.0) - strscan (3.1.0) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) From 856b940939274c3ae6ac338ce9ceee7b3b22db66 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 5 Nov 2024 14:49:22 -0800 Subject: [PATCH 06/18] Allow beAKindOf and beAnInstanceOf to nest inside of other matchers (#1173) --- Sources/Nimble/Matchers/BeAKindOf.swift | 2 +- Sources/Nimble/Matchers/BeAnInstanceOf.swift | 2 +- Tests/NimbleTests/Matchers/BeAKindOfTest.swift | 6 ++++++ Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/Nimble/Matchers/BeAKindOf.swift b/Sources/Nimble/Matchers/BeAKindOf.swift index 6b010ece0..48261b3e0 100644 --- a/Sources/Nimble/Matchers/BeAKindOf.swift +++ b/Sources/Nimble/Matchers/BeAKindOf.swift @@ -6,7 +6,7 @@ private func matcherMessage(forClass expectedClass: AnyClass) -> String { } /// A Nimble matcher that succeeds when the actual value is an instance of the given class. -public func beAKindOf(_ expectedType: T.Type) -> Matcher { +public func beAKindOf(_ expectedType: T.Type) -> Matcher { return Matcher.define { actualExpression in let message: ExpectationMessage diff --git a/Sources/Nimble/Matchers/BeAnInstanceOf.swift b/Sources/Nimble/Matchers/BeAnInstanceOf.swift index d8a13112f..9d55b9efa 100644 --- a/Sources/Nimble/Matchers/BeAnInstanceOf.swift +++ b/Sources/Nimble/Matchers/BeAnInstanceOf.swift @@ -1,7 +1,7 @@ import Foundation /// A Nimble matcher that succeeds when the actual value is an _exact_ instance of the given class. -public func beAnInstanceOf(_ expectedType: T.Type) -> Matcher { +public func beAnInstanceOf(_ expectedType: T.Type) -> Matcher { let errorMessage = "be an instance of \(String(describing: expectedType))" return Matcher.define { actualExpression in let instance = try actualExpression.evaluate() diff --git a/Tests/NimbleTests/Matchers/BeAKindOfTest.swift b/Tests/NimbleTests/Matchers/BeAKindOfTest.swift index 93bf7b9fa..3d8fa1609 100644 --- a/Tests/NimbleTests/Matchers/BeAKindOfTest.swift +++ b/Tests/NimbleTests/Matchers/BeAKindOfTest.swift @@ -34,6 +34,12 @@ final class BeAKindOfSwiftTest: XCTestCase { expect(testProtocolStruct).toNot(beAKindOf(TestClassConformingToProtocol.self)) } + func testNestedMatchers() { + // This test is successful if it even compiles. + let result: Result = .success(1) + expect(result).to(beSuccess(beAKindOf(Int.self))) + } + func testFailureMessages() { failsWithErrorMessage("expected to not be a kind of Int, got ") { expect(1).toNot(beAKindOf(Int.self)) diff --git a/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift b/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift index 820ee5d50..393fba1ae 100644 --- a/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift +++ b/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift @@ -36,6 +36,12 @@ final class BeAnInstanceOfTest: XCTestCase { expect(testProtocolStruct).toNot(beAnInstanceOf(TestClassConformingToProtocol.self)) } + func testNestedMatchers() { + // This test is successful if it even compiles. + let result: Result = .success(1) + expect(result).to(beSuccess(beAnInstanceOf(Int.self))) + } + func testFailureMessages() { failsWithErrorMessageForNil("expected to not be an instance of NSNull, got ") { expect(nil as NSNull?).toNot(beAnInstanceOf(NSNull.self)) From 0e445aacf4bad4e74406d50959e4b4f2bafa45b9 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 5 Nov 2024 14:50:57 -0800 Subject: [PATCH 07/18] [v13.6.1] Update podspec --- Nimble.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nimble.podspec b/Nimble.podspec index 2c9865bb1..e31a3eb7c 100644 --- a/Nimble.podspec +++ b/Nimble.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Nimble" - s.version = "13.6.0" + s.version = "13.6.1" s.summary = "A Matcher Framework for Swift and Objective-C" s.description = <<-DESC Use Nimble to express the expected outcomes of Swift or Objective-C expressions. Inspired by Cedar. From b0cee1b31b58e46a8443593a97f5a69256e76558 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 5 Nov 2024 17:24:52 -0800 Subject: [PATCH 08/18] Fix regression in beAnInstanceOf (#1174) beAnInstanceOf was not correctly matching when an exact type as was expected was given. This fixes that by emulating the previous typing of beAnInstanceOf while still working when used as a submatcher --- Sources/Nimble/Matchers/BeAnInstanceOf.swift | 2 +- Tests/NimbleTests/Matchers/BeAKindOfTest.swift | 13 +++++++++++-- Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift | 13 +++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Sources/Nimble/Matchers/BeAnInstanceOf.swift b/Sources/Nimble/Matchers/BeAnInstanceOf.swift index 9d55b9efa..f36d6689e 100644 --- a/Sources/Nimble/Matchers/BeAnInstanceOf.swift +++ b/Sources/Nimble/Matchers/BeAnInstanceOf.swift @@ -5,7 +5,7 @@ public func beAnInstanceOf(_ expectedType: T.Type) -> Matcher { let errorMessage = "be an instance of \(String(describing: expectedType))" return Matcher.define { actualExpression in let instance = try actualExpression.evaluate() - guard let validInstance = instance else { + guard let validInstance: Any = instance else { return MatcherResult( status: .doesNotMatch, message: .expectedActualValueTo(errorMessage) diff --git a/Tests/NimbleTests/Matchers/BeAKindOfTest.swift b/Tests/NimbleTests/Matchers/BeAKindOfTest.swift index 3d8fa1609..f7c2b0dd6 100644 --- a/Tests/NimbleTests/Matchers/BeAKindOfTest.swift +++ b/Tests/NimbleTests/Matchers/BeAKindOfTest.swift @@ -28,16 +28,25 @@ final class BeAKindOfSwiftTest: XCTestCase { expect(testProtocolClass).to(beAKindOf(TestProtocol.self)) expect(testProtocolClass).toNot(beAKindOf(TestStructConformingToProtocol.self)) + expect(testProtocolClass as TestProtocol).to(beAKindOf(TestClassConformingToProtocol.self)) + expect(testProtocolClass as TestProtocol).to(beAKindOf(TestProtocol.self)) + expect(testProtocolClass as TestProtocol).toNot(beAKindOf(TestStructConformingToProtocol.self)) + let testProtocolStruct = TestStructConformingToProtocol() expect(testProtocolStruct).to(beAKindOf(TestStructConformingToProtocol.self)) expect(testProtocolStruct).to(beAKindOf(TestProtocol.self)) expect(testProtocolStruct).toNot(beAKindOf(TestClassConformingToProtocol.self)) + + expect(testProtocolStruct as TestProtocol).to(beAKindOf(TestStructConformingToProtocol.self)) + expect(testProtocolStruct as TestProtocol).to(beAKindOf(TestProtocol.self)) + expect(testProtocolStruct as TestProtocol).toNot(beAKindOf(TestClassConformingToProtocol.self)) } func testNestedMatchers() { // This test is successful if it even compiles. - let result: Result = .success(1) - expect(result).to(beSuccess(beAKindOf(Int.self))) + expect(Result.success(1)).to(beSuccess(beAKindOf(Int.self))) + expect(Result.success(TestClassConformingToProtocol())).to(beSuccess(beAKindOf(TestClassConformingToProtocol.self))) + expect(Result.success(TestClassConformingToProtocol())).to(beSuccess(beAKindOf(TestProtocol.self))) } func testFailureMessages() { diff --git a/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift b/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift index 393fba1ae..0907c0796 100644 --- a/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift +++ b/Tests/NimbleTests/Matchers/BeAnInstanceOfTest.swift @@ -30,16 +30,25 @@ final class BeAnInstanceOfTest: XCTestCase { expect(testProtocolClass).toNot(beAnInstanceOf(TestProtocol.self)) expect(testProtocolClass).toNot(beAnInstanceOf(TestStructConformingToProtocol.self)) + expect(testProtocolClass as TestProtocol).to(beAnInstanceOf(TestClassConformingToProtocol.self)) + expect(testProtocolClass as TestProtocol).toNot(beAnInstanceOf(TestProtocol.self)) + expect(testProtocolClass as TestProtocol).toNot(beAnInstanceOf(TestStructConformingToProtocol.self)) + let testProtocolStruct = TestStructConformingToProtocol() expect(testProtocolStruct).to(beAnInstanceOf(TestStructConformingToProtocol.self)) expect(testProtocolStruct).toNot(beAnInstanceOf(TestProtocol.self)) expect(testProtocolStruct).toNot(beAnInstanceOf(TestClassConformingToProtocol.self)) + + expect(testProtocolStruct as TestProtocol).to(beAnInstanceOf(TestStructConformingToProtocol.self)) + expect(testProtocolStruct as TestProtocol).toNot(beAnInstanceOf(TestProtocol.self)) + expect(testProtocolStruct as TestProtocol).toNot(beAnInstanceOf(TestClassConformingToProtocol.self)) } func testNestedMatchers() { // This test is successful if it even compiles. - let result: Result = .success(1) - expect(result).to(beSuccess(beAnInstanceOf(Int.self))) + expect(Result.success(1)).to(beSuccess(beAnInstanceOf(Int.self))) + expect(Result.success(TestClassConformingToProtocol())).to(beSuccess(beAnInstanceOf(TestClassConformingToProtocol.self))) + expect(Result.success(TestClassConformingToProtocol())).toNot(beSuccess(beAnInstanceOf(TestProtocol.self))) } func testFailureMessages() { From f3bedd7c2a858f4513e7d004def200a69238db2f Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Tue, 5 Nov 2024 17:25:31 -0800 Subject: [PATCH 09/18] [v13.6.2] Update podspec --- Nimble.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nimble.podspec b/Nimble.podspec index e31a3eb7c..a2cf28ab5 100644 --- a/Nimble.podspec +++ b/Nimble.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Nimble" - s.version = "13.6.1" + s.version = "13.6.2" s.summary = "A Matcher Framework for Swift and Objective-C" s.description = <<-DESC Use Nimble to express the expected outcomes of Swift or Objective-C expressions. Inspired by Cedar. From cfe1d7f64fc50210821708434a83caf4d3128ada Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 8 Nov 2024 09:26:53 -0800 Subject: [PATCH 10/18] Bump the ruby version used in github workflows (#1175) --- .github/workflows/carthage.yml | 2 +- .github/workflows/cocoapods.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/carthage.yml b/.github/workflows/carthage.yml index 4087097d6..600eadda3 100644 --- a/.github/workflows/carthage.yml +++ b/.github/workflows/carthage.yml @@ -18,6 +18,6 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.2 bundler-cache: true - run: ./test carthage diff --git a/.github/workflows/cocoapods.yml b/.github/workflows/cocoapods.yml index 4a5b2eaae..65fae6e9e 100644 --- a/.github/workflows/cocoapods.yml +++ b/.github/workflows/cocoapods.yml @@ -18,6 +18,6 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.2 bundler-cache: true - run: ./test podspec From e48884171561b15bced131abf877d08d747a6680 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:01:17 -0800 Subject: [PATCH 11/18] Bump cocoapods from 1.15.2 to 1.16.2 (#1172) Bumps [cocoapods](https://github.com/CocoaPods/CocoaPods) from 1.15.2 to 1.16.2. - [Release notes](https://github.com/CocoaPods/CocoaPods/releases) - [Changelog](https://github.com/CocoaPods/CocoaPods/blob/1.16.2/CHANGELOG.md) - [Commits](https://github.com/CocoaPods/CocoaPods/compare/1.15.2...1.16.2) --- updated-dependencies: - dependency-name: cocoapods dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rachel Brindle --- Gemfile | 2 +- Gemfile.lock | 57 ++++++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Gemfile b/Gemfile index f722f6e1c..a37212dd4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ # A sample Gemfile source "https://rubygems.org" -gem 'cocoapods', '~> 1.15' +gem 'cocoapods', '~> 1.16' gem 'jazzy', '~> 0.14' diff --git a/Gemfile.lock b/Gemfile.lock index fc4d6cbdb..dec5cf3da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,36 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (7.1.3) + activesupport (7.2.2) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) base64 (0.2.0) - bigdecimal (3.1.6) + benchmark (0.3.0) + bigdecimal (3.1.8) claide (1.1.0) - cocoapods (1.15.2) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -39,8 +44,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -60,19 +65,18 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) - drb (2.2.0) - ruby2_keywords + drb (2.2.1) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.16.3) + ffi (1.17.0) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) jazzy (0.14.4) cocoapods (~> 1.5) @@ -84,25 +88,26 @@ GEM sassc (~> 2.1) sqlite3 (~> 1.3) xcinvoke (~> 0.3.0) - json (2.7.1) + json (2.7.5) liferaft (0.0.6) + logger (1.6.1) mini_portile2 (2.8.5) - minitest (5.22.2) + minitest (5.25.1) molinillo (0.8.0) mustache (1.1.1) - mutex_m (0.2.0) - nanaimo (0.3.0) + nanaimo (0.4.0) nap (1.1.0) netrc (0.11.0) + nkf (0.2.0) open4 (1.3.4) public_suffix (4.0.7) redcarpet (3.6.0) rexml (3.3.9) rouge (4.2.0) ruby-macho (2.5.1) - ruby2_keywords (0.0.5) sassc (2.4.0) ffi (~> 1.9) + securerandom (0.3.1) sqlite3 (1.6.9) mini_portile2 (~> 2.8.0) typhoeus (1.4.1) @@ -111,19 +116,19 @@ GEM concurrent-ruby (~> 1.0) xcinvoke (0.3.0) liferaft (~> 0.0.6) - xcodeproj (1.25.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.15) + cocoapods (~> 1.16) jazzy (~> 0.14) BUNDLED WITH From e7e32cf9e9277fe8f1f07d03d4793669300af9b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:03:43 -0800 Subject: [PATCH 12/18] Bump jazzy from 0.14.4 to 0.15.3 (#1170) Bumps [jazzy](https://github.com/realm/jazzy) from 0.14.4 to 0.15.3. - [Release notes](https://github.com/realm/jazzy/releases) - [Changelog](https://github.com/realm/jazzy/blob/master/CHANGELOG.md) - [Commits](https://github.com/realm/jazzy/compare/v0.14.4...v0.15.3) --- updated-dependencies: - dependency-name: jazzy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index a37212dd4..e4c56bccc 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source "https://rubygems.org" gem 'cocoapods', '~> 1.16' -gem 'jazzy', '~> 0.14' +gem 'jazzy', '~> 0.15' diff --git a/Gemfile.lock b/Gemfile.lock index dec5cf3da..762125067 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,7 +24,7 @@ GEM json (>= 1.5.1) atomos (0.1.3) base64 (0.2.0) - benchmark (0.3.0) + benchmark (0.4.0) bigdecimal (3.1.8) claide (1.1.0) cocoapods (1.16.2) @@ -78,20 +78,20 @@ GEM httpclient (2.8.3) i18n (1.14.6) concurrent-ruby (~> 1.0) - jazzy (0.14.4) + jazzy (0.15.3) cocoapods (~> 1.5) mustache (~> 1.1) open4 (~> 1.3) redcarpet (~> 3.4) - rexml (~> 3.2) + rexml (>= 3.2.7, < 4.0) rouge (>= 2.0.6, < 5.0) sassc (~> 2.1) sqlite3 (~> 1.3) xcinvoke (~> 0.3.0) - json (2.7.5) + json (2.8.1) liferaft (0.0.6) logger (1.6.1) - mini_portile2 (2.8.5) + mini_portile2 (2.8.7) minitest (5.25.1) molinillo (0.8.0) mustache (1.1.1) @@ -103,12 +103,12 @@ GEM public_suffix (4.0.7) redcarpet (3.6.0) rexml (3.3.9) - rouge (4.2.0) + rouge (4.4.0) ruby-macho (2.5.1) sassc (2.4.0) ffi (~> 1.9) securerandom (0.3.1) - sqlite3 (1.6.9) + sqlite3 (1.7.3) mini_portile2 (~> 2.8.0) typhoeus (1.4.1) ethon (>= 0.9.0) @@ -129,7 +129,7 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.16) - jazzy (~> 0.14) + jazzy (~> 0.15) BUNDLED WITH 2.1.4 From c2c106f5185a8990abe0778910be9b575e307655 Mon Sep 17 00:00:00 2001 From: Ian Rahman Date: Fri, 8 Nov 2024 18:54:35 -0500 Subject: [PATCH 13/18] Update DSL+Wait.swift (#1176) Fix error message grammar --- Sources/Nimble/DSL+Wait.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nimble/DSL+Wait.swift b/Sources/Nimble/DSL+Wait.swift index 8995bc964..a91e919a2 100644 --- a/Sources/Nimble/DSL+Wait.swift +++ b/Sources/Nimble/DSL+Wait.swift @@ -127,7 +127,7 @@ public class NMBWait: NSObject { internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTimeInterval) -> String { // swiftlint:disable:next line_length - return "\(fnName) timed out but was unable to run the timeout handler because the main thread is unresponsive (\(leeway.description) is allow after the wait times out). Conditions that may cause this include processing blocking IO on the main thread, calls to sleep(), deadlocks, and synchronous IPC. Nimble forcefully stopped run loop which may cause future failures in test run." + return "\(fnName) timed out but was unable to run the timeout handler because the main thread is unresponsive. (\(leeway.description) is allowed after the wait times out) Conditions that may cause this include processing blocking IO on the main thread, calls to sleep(), deadlocks, and synchronous IPC. Nimble forcefully stopped the run loop which may cause future failures in test runs." } /// Wait asynchronously until the done closure is called or the timeout has been reached. From e541b921ea75265e6e1f66f32a4c56012a2fcfe8 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 11 Dec 2024 16:10:57 -0800 Subject: [PATCH 14/18] Fix expected text in waitUntil stall test (#1179) --- Tests/NimbleTests/PollingTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index 7c6361298..a9d4d72ec 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -131,7 +131,7 @@ final class PollingTest: XCTestCase { } func testWaitUntilDetectsStalledMainThreadActivity() { - let msg = "-waitUntil() timed out but was unable to run the timeout handler because the main thread is unresponsive (0.5 seconds is allow after the wait times out). Conditions that may cause this include processing blocking IO on the main thread, calls to sleep(), deadlocks, and synchronous IPC. Nimble forcefully stopped run loop which may cause future failures in test run." + let msg = "-waitUntil() timed out but was unable to run the timeout handler because the main thread is unresponsive. (0.5 seconds is allowed after the wait times out) Conditions that may cause this include processing blocking IO on the main thread, calls to sleep(), deadlocks, and synchronous IPC. Nimble forcefully stopped the run loop which may cause future failures in test runs." failsWithErrorMessage(msg) { waitUntil(timeout: .seconds(1)) { done in Thread.sleep(forTimeInterval: 3.0) From 51d5438f79ebd8fbfdaab513e9136cdd6521afab Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 11 Dec 2024 16:17:04 -0800 Subject: [PATCH 15/18] Allow beIdenticalTo and be to be used as composed matchers (#1178) --- Sources/Nimble/Matchers/BeIdenticalTo.swift | 4 ++-- Tests/NimbleTests/Matchers/BeIdenticalToObjectTest.swift | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Nimble/Matchers/BeIdenticalTo.swift b/Sources/Nimble/Matchers/BeIdenticalTo.swift index 3f00b187f..85e5b50c8 100644 --- a/Sources/Nimble/Matchers/BeIdenticalTo.swift +++ b/Sources/Nimble/Matchers/BeIdenticalTo.swift @@ -1,6 +1,6 @@ /// A Nimble matcher that succeeds when the actual value is the same instance /// as the expected instance. -public func beIdenticalTo(_ expected: AnyObject?) -> Matcher { +public func beIdenticalTo(_ expected: T?) -> Matcher { return Matcher.define { actualExpression in let actual = try actualExpression.evaluate() @@ -35,7 +35,7 @@ public func !== (lhs: AsyncExpectation, rhs: AnyObject?) async { /// as the expected instance. /// /// Alias for "beIdenticalTo". -public func be(_ expected: AnyObject?) -> Matcher { +public func be(_ expected: T?) -> Matcher { return beIdenticalTo(expected) } diff --git a/Tests/NimbleTests/Matchers/BeIdenticalToObjectTest.swift b/Tests/NimbleTests/Matchers/BeIdenticalToObjectTest.swift index a82077b2e..0ba2316d0 100644 --- a/Tests/NimbleTests/Matchers/BeIdenticalToObjectTest.swift +++ b/Tests/NimbleTests/Matchers/BeIdenticalToObjectTest.swift @@ -12,6 +12,8 @@ final class BeIdenticalToObjectTest: XCTestCase { func testBeIdenticalToPositive() { expect(self.testObjectA).to(beIdenticalTo(testObjectA)) + // check that the typing works out when used as a submatcher. + expect(self.testObjectA).to(map({ $0 }, beIdenticalTo(testObjectA))) } func testBeIdenticalToNegative() { From 74185dbd1fe7637d91dc61d6ae1e1961bb98cf69 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 11 Dec 2024 16:21:00 -0800 Subject: [PATCH 16/18] Add an overload of map that allows you to fail the conversion by returning nil. (#1177) --- Sources/Nimble/Matchers/Map.swift | 46 +++++++++++++- Tests/NimbleTests/Matchers/MapTest.swift | 78 ++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/Sources/Nimble/Matchers/Map.swift b/Sources/Nimble/Matchers/Map.swift index 7132ab3c4..509a49799 100644 --- a/Sources/Nimble/Matchers/Map.swift +++ b/Sources/Nimble/Matchers/Map.swift @@ -1,7 +1,7 @@ /// `map` works by transforming the expression to a value that the given matcher uses. /// /// 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))`. +/// So, you could write `expect(myObject).to(map(\.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 { Matcher { (received: Expression) in @@ -15,7 +15,7 @@ public func map(_ transform: @escaping (T) throws -> U, _ matcher: Matcher /// `map` works by transforming the expression to a value that the given matcher uses. /// /// 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))`. +/// So, you could write `expect(myObject).to(map(\.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 { AsyncMatcher { (received: AsyncExpression) in @@ -25,3 +25,45 @@ public func map(_ transform: @escaping (T) async throws -> U, _ matcher: s }) } } + +/// `map` works by transforming the expression to a value that the given matcher uses. +/// +/// For example, you might only care that a particular property on a method equals some other value. +/// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`. +/// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type. +public func map(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher) -> Matcher { + Matcher { (received: Expression) in + let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") + + guard let value = try received.evaluate() else { + return MatcherResult(status: .fail, message: message.appendedBeNilHint()) + } + + guard let transformedValue = try transform(value) else { + return MatcherResult(status: .fail, message: message) + } + + return try matcher.satisfies(Expression(expression: { transformedValue }, location: received.location)) + } +} + +/// `map` works by transforming the expression to a value that the given matcher uses. +/// +/// For example, you might only care that a particular property on a method equals some other value. +/// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`. +/// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type. +public func map(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher) -> AsyncMatcher { + AsyncMatcher { (received: AsyncExpression) in + let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") + + guard let value = try await received.evaluate() else { + return MatcherResult(status: .fail, message: message.appendedBeNilHint()) + } + + guard let transformedValue = try await transform(value) else { + return MatcherResult(status: .fail, message: message) + } + + return try await matcher.satisfies(AsyncExpression(expression: { transformedValue }, location: received.location)) + } +} diff --git a/Tests/NimbleTests/Matchers/MapTest.swift b/Tests/NimbleTests/Matchers/MapTest.swift index 9b13c5f64..cc361f4df 100644 --- a/Tests/NimbleTests/Matchers/MapTest.swift +++ b/Tests/NimbleTests/Matchers/MapTest.swift @@ -5,6 +5,7 @@ import NimbleSharedTestHelpers #endif final class MapTest: XCTestCase { + // MARK: Map func testMap() { expect(1).to(map({ $0 }, equal(1))) @@ -80,4 +81,81 @@ final class MapTest: XCTestCase { map(\.string, equal("world")) )) } + + // MARK: Failable map + func testFailableMap() { + expect("1").to(map({ Int($0) }, equal(1))) + + struct Value { + let int: Int? + let string: String? + } + + expect(Value( + int: 1, + string: "hello" + )).to(satisfyAllOf( + map(\.int, equal(1)), + map(\.string, equal("hello")) + )) + + expect(Value( + int: 1, + string: "hello" + )).to(satisfyAnyOf( + map(\.int, equal(2)), + map(\.string, equal("hello")) + )) + + expect(Value( + int: 1, + string: "hello" + )).toNot(satisfyAllOf( + map(\.int, equal(2)), + map(\.string, equal("hello")) + )) + } + + func testFailableMapAsync() async { + struct Value { + let int: Int? + let string: String? + } + + await expect(Value( + int: 1, + string: "hello" + )).to(map(\.int, asyncEqual(1))) + + await expect(Value( + int: 1, + string: "hello" + )).toNot(map(\.int, asyncEqual(2))) + } + + func testFailableMapWithAsyncFunction() async { + func someOperation(_ value: Int) async -> String? { + "\(value)" + } + await expect(1).to(map(someOperation, equal("1"))) + } + + func testFailableMapWithActor() { + actor Box { + let int: Int? + let string: String? + + init(int: Int, string: String) { + self.int = int + self.string = string + } + } + + let box = Box(int: 3, string: "world") + + expect(box).to(satisfyAllOf( + map(\.int, equal(3)), + map(\.string, equal("world")) + )) + } } From 725e5a360b930f2b1f541dfaaa0de51d87b2b9bd Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 11 Dec 2024 16:52:13 -0800 Subject: [PATCH 17/18] Rename the failable map overload to compactMap (#1180) And also provide another overload of map that takes in an optional and behaves much more closely to what the original map matcher does --- Sources/Nimble/Matchers/Map.swift | 34 ++++++++- Tests/NimbleTests/Matchers/MapTest.swift | 87 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/Sources/Nimble/Matchers/Map.swift b/Sources/Nimble/Matchers/Map.swift index 509a49799..5029f98be 100644 --- a/Sources/Nimble/Matchers/Map.swift +++ b/Sources/Nimble/Matchers/Map.swift @@ -29,9 +29,37 @@ public func map(_ transform: @escaping (T) async throws -> U, _ matcher: s /// `map` works by transforming the expression to a value that the given matcher uses. /// /// For example, you might only care that a particular property on a method equals some other value. +/// So, you could write `expect(myObject).to(map(\.someOptionalIntValue, 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 { + Matcher { (received: Expression) in + try matcher.satisfies(received.cast { value in + guard let value else { return nil } + return try transform(value) + }) + } +} + +/// `map` works by transforming the expression to a value that the given matcher uses. +/// +/// For example, you might only care that a particular property on a method equals some other value. +/// So, you could write `expect(myObject).to(map(\.someOptionalIntValue, 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 { + AsyncMatcher { (received: AsyncExpression) in + try await matcher.satisfies(received.cast { value in + guard let value else { return nil } + return try await transform(value) + }) + } +} + +/// `compactMap` works by transforming the expression to a value that the given matcher uses. +/// +/// For example, you might only care that a particular property on a method equals some other value. /// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`. /// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type. -public func map(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher) -> Matcher { +public func compactMap(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher) -> Matcher { Matcher { (received: Expression) in let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") @@ -47,12 +75,12 @@ public func map(_ transform: @escaping (T) throws -> U?, _ matcher: Matche } } -/// `map` works by transforming the expression to a value that the given matcher uses. +/// `compactMap` works by transforming the expression to a value that the given matcher uses. /// /// For example, you might only care that a particular property on a method equals some other value. /// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`. /// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type. -public func map(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher) -> AsyncMatcher { +public func compactMap(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher) -> AsyncMatcher { AsyncMatcher { (received: AsyncExpression) in let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") diff --git a/Tests/NimbleTests/Matchers/MapTest.swift b/Tests/NimbleTests/Matchers/MapTest.swift index cc361f4df..e7413deaa 100644 --- a/Tests/NimbleTests/Matchers/MapTest.swift +++ b/Tests/NimbleTests/Matchers/MapTest.swift @@ -158,4 +158,91 @@ final class MapTest: XCTestCase { map(\.string, equal("world")) )) } + + // MARK: Compact map + func testCompactMap() { + expect("1").to(compactMap({ Int($0) }, equal(1))) + expect("1").toNot(compactMap({ Int($0) }, equal(2))) + + let assertions = gatherExpectations(silently: true) { + expect("not a number").to(compactMap({ Int($0) }, equal(1))) + expect("not a number").toNot(compactMap({ Int($0) }, equal(1))) + } + + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } + + func testCompactMapAsync() async { + struct Value { + let int: Int? + let string: String? + } + + await expect("1").to(compactMap({ Int($0) }, asyncEqual(1))) + await expect("1").toNot(compactMap({ Int($0) }, asyncEqual(2))) + + let assertions = await gatherExpectations(silently: true) { + await expect("not a number").to(compactMap({ Int($0) }, asyncEqual(1))) + await expect("not a number").toNot(compactMap({ Int($0) }, asyncEqual(1))) + } + + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } + + func testCompactMapWithAsyncFunction() async { + func someOperation(_ value: Int) async -> String? { + "\(value)" + } + await expect(1).to(compactMap(someOperation, equal("1"))) + + func someFailingOperation(_ value: Int) async -> String? { + nil + } + + let assertions = await gatherExpectations(silently: true) { + await expect(1).to(compactMap(someFailingOperation, equal("1"))) + await expect(1).toNot(compactMap(someFailingOperation, equal("1"))) + } + + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } + + func testCompactMapWithActor() { + actor Box { + let int: Int? + let string: String? + + init(int: Int?, string: String?) { + self.int = int + self.string = string + } + } + + let box = Box(int: 3, string: "world") + + expect(box).to(satisfyAllOf( + compactMap(\.int, equal(3)), + compactMap(\.string, equal("world")) + )) + + let failingBox = Box(int: nil, string: nil) + + let assertions = gatherExpectations(silently: true) { + expect(failingBox).to(satisfyAllOf( + compactMap(\.int, equal(3)) + )) + expect(failingBox).toNot(satisfyAllOf( + compactMap(\.int, equal(3)) + )) + } + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } } From 9aaef11742dabae7aa12dce91f6be0946c22ab5b Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 11 Dec 2024 16:53:37 -0800 Subject: [PATCH 18/18] [v13.7.0] Update podspec --- Nimble.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nimble.podspec b/Nimble.podspec index a2cf28ab5..9e703397e 100644 --- a/Nimble.podspec +++ b/Nimble.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Nimble" - s.version = "13.6.2" + s.version = "13.7.0" s.summary = "A Matcher Framework for Swift and Objective-C" s.description = <<-DESC Use Nimble to express the expected outcomes of Swift or Objective-C expressions. Inspired by Cedar.