From 592ea7e851a2738d93c47db984d79930d9f9b71d Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sat, 24 Feb 2024 10:03:53 -0800 Subject: [PATCH] Fix bug where expect(nil).toAlways(equal(0)) would erroneously pass --- Sources/Nimble/Polling+AsyncAwait.swift | 31 +++++++++++++------ Sources/Nimble/Polling+Require.swift | 20 ++++++------ Sources/Nimble/Polling.swift | 26 ++++++++++++++-- Sources/Nimble/Utils/AsyncAwait.swift | 10 +++--- Sources/Nimble/Utils/PollAwait.swift | 11 +++++-- .../NimbleTests/AsyncAwaitTest+Require.swift | 6 ++++ Tests/NimbleTests/AsyncAwaitTest.swift | 3 ++ Tests/NimbleTests/PollingTest+Require.swift | 6 ++++ Tests/NimbleTests/PollingTest.swift | 6 ++++ 9 files changed, 88 insertions(+), 31 deletions(-) diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index 20e9bb520..6adb153c0 100644 --- a/Sources/Nimble/Polling+AsyncAwait.swift +++ b/Sources/Nimble/Polling+AsyncAwait.swift @@ -41,8 +41,19 @@ internal actor Poller { file: expression.location.file, line: expression.location.line, fnName: fnName) { - self.updateMatcherResult(result: try await matcherRunner()) - .toBoolean(expectation: style) + if self.updateMatcherResult(result: try await matcherRunner()) + .toBoolean(expectation: style) { + if matchStyle.isContinous { + return .incomplete + } + return .finished(true) + } else { + if matchStyle.isContinous { + return .finished(false) + } else { + return .incomplete + } + } } return processPollResult(result.toPollResult(), matchStyle: matchStyle, lastMatcherResult: lastMatcherResult, fnName: fnName) } @@ -152,7 +163,7 @@ extension SyncExpectation { description: description) { await poll( expression: asyncExpression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -186,7 +197,7 @@ extension SyncExpectation { description: description) { await poll( expression: asyncExpression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, @@ -282,7 +293,7 @@ extension SyncExpectation { description: description) { await poll( expression: asyncExpression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -316,7 +327,7 @@ extension SyncExpectation { description: description) { await poll( expression: asyncExpression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, @@ -409,7 +420,7 @@ extension AsyncExpectation { description: description) { await poll( expression: expression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -442,7 +453,7 @@ extension AsyncExpectation { description: description) { await poll( expression: expression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, @@ -533,7 +544,7 @@ extension AsyncExpectation { description: description) { await poll( expression: expression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -566,7 +577,7 @@ extension AsyncExpectation { description: description) { await poll( expression: expression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, diff --git a/Sources/Nimble/Polling+Require.swift b/Sources/Nimble/Polling+Require.swift index 83e42d004..7f9c9268d 100644 --- a/Sources/Nimble/Polling+Require.swift +++ b/Sources/Nimble/Polling+Require.swift @@ -107,7 +107,7 @@ extension SyncRequirement { expression, .toNotMatch, poll( - style: .toMatch, + style: .toNotMatch, matchStyle: .never, matcher: matcher, timeout: until, @@ -158,7 +158,7 @@ extension SyncRequirement { expression, .toMatch, poll( - style: .toNotMatch, + style: .toMatch, matchStyle: .always, matcher: matcher, timeout: until, @@ -266,7 +266,7 @@ extension SyncRequirement { description: description) { await poll( expression: asyncExpression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -300,7 +300,7 @@ extension SyncRequirement { description: description) { await poll( expression: asyncExpression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, @@ -396,7 +396,7 @@ extension SyncRequirement { description: description) { await poll( expression: asyncExpression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -430,7 +430,7 @@ extension SyncRequirement { description: description) { await poll( expression: asyncExpression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, @@ -523,7 +523,7 @@ extension AsyncRequirement { description: description) { await poll( expression: expression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -556,7 +556,7 @@ extension AsyncRequirement { description: description) { await poll( expression: expression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, @@ -647,7 +647,7 @@ extension AsyncRequirement { description: description) { await poll( expression: expression, - style: .toMatch, + style: .toNotMatch, matchStyle: .never, timeout: until, poll: pollInterval, @@ -680,7 +680,7 @@ extension AsyncRequirement { description: description) { await poll( expression: expression, - style: .toNotMatch, + style: .toMatch, matchStyle: .always, timeout: until, poll: pollInterval, diff --git a/Sources/Nimble/Polling.swift b/Sources/Nimble/Polling.swift index 9a57224d7..dcf5d148e 100644 --- a/Sources/Nimble/Polling.swift +++ b/Sources/Nimble/Polling.swift @@ -41,6 +41,15 @@ public struct PollingDefaults { internal enum AsyncMatchStyle { case eventually, never, always + + var isContinous: Bool { + switch self { + case .eventually: + false + case .never, .always: + true + } + } } // swiftlint:disable:next function_parameter_count @@ -63,7 +72,18 @@ internal func poll( line: actualExpression.location.line, fnName: fnName) { lastMatcherResult = try matcher.satisfies(uncachedExpression) - return lastMatcherResult!.toBoolean(expectation: style) + if lastMatcherResult!.toBoolean(expectation: style) { + if matchStyle.isContinous { + return .incomplete + } + return .finished(true) + } else { + if matchStyle.isContinous { + return .finished(false) + } else { + return .incomplete + } + } } return processPollResult(result, matchStyle: matchStyle, lastMatcherResult: lastMatcherResult, fnName: fnName) } @@ -220,7 +240,7 @@ extension SyncExpectation { expression, .toNotMatch, poll( - style: .toMatch, + style: .toNotMatch, matchStyle: .never, matcher: matcher, timeout: until, @@ -271,7 +291,7 @@ extension SyncExpectation { expression, .toMatch, poll( - style: .toNotMatch, + style: .toMatch, matchStyle: .always, matcher: matcher, timeout: until, diff --git a/Sources/Nimble/Utils/AsyncAwait.swift b/Sources/Nimble/Utils/AsyncAwait.swift index a21dfd2cc..1e5884370 100644 --- a/Sources/Nimble/Utils/AsyncAwait.swift +++ b/Sources/Nimble/Utils/AsyncAwait.swift @@ -165,11 +165,11 @@ private func timeout(timeoutQueue: DispatchQueue, timeoutInterval: NimbleTime return await promise.value } -private func poll(_ pollInterval: NimbleTimeInterval, expression: @escaping () async throws -> Bool) async -> AsyncPollResult { +private func poll(_ pollInterval: NimbleTimeInterval, expression: @escaping () async throws -> PollStatus) async -> AsyncPollResult { for try await _ in AsyncTimerSequence(interval: pollInterval) { do { - if try await expression() { - return .completed(true) + if case .finished(let result) = try await expression() { + return .completed(result) } } catch { return .errorThrown(error) @@ -199,7 +199,7 @@ private func runPoller( pollInterval: NimbleTimeInterval, awaiter: Awaiter, fnName: String = #function, file: FileString = #file, line: UInt = #line, - expression: @escaping () async throws -> Bool + expression: @escaping () async throws -> PollStatus ) async -> AsyncPollResult { awaiter.waitLock.acquireWaitingLock( fnName, @@ -324,7 +324,7 @@ internal func pollBlock( file: FileString, line: UInt, fnName: String = #function, - expression: @escaping () async throws -> Bool) async -> AsyncPollResult { + expression: @escaping () async throws -> PollStatus) async -> AsyncPollResult { await runPoller( timeoutInterval: timeoutInterval, pollInterval: pollInterval, diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index 161e9552a..9c045c464 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -98,6 +98,11 @@ internal enum PollResult { } } +internal enum PollStatus { + case finished(Bool) + case incomplete +} + /// Holds the resulting value from an asynchronous expectation. /// This class is thread-safe at receiving a "response" to this promise. internal final class AwaitPromise { @@ -399,11 +404,11 @@ internal func pollBlock( file: FileString, line: UInt, fnName: String = #function, - expression: @escaping () throws -> Bool) -> PollResult { + expression: @escaping () throws -> PollStatus) -> PollResult { let awaiter = NimbleEnvironment.activeInstance.awaiter let result = awaiter.poll(pollInterval) { () throws -> Bool? in - if try expression() { - return true + if case .finished(let result) = try expression() { + return result } return nil } diff --git a/Tests/NimbleTests/AsyncAwaitTest+Require.swift b/Tests/NimbleTests/AsyncAwaitTest+Require.swift index fc1011e04..7925cc36d 100644 --- a/Tests/NimbleTests/AsyncAwaitTest+Require.swift +++ b/Tests/NimbleTests/AsyncAwaitTest+Require.swift @@ -243,6 +243,9 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b await failsWithErrorMessage("unexpected error thrown: <\(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)) + } } func testToAlwaysPositiveMatches() async throws { @@ -276,6 +279,9 @@ final class AsyncAwaitRequireTest: XCTestCase { // swiftlint:disable:this type_b await failsWithErrorMessage("unexpected error thrown: <\(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/AsyncAwaitTest.swift b/Tests/NimbleTests/AsyncAwaitTest.swift index ff80b8df0..8fcbcbc3e 100644 --- a/Tests/NimbleTests/AsyncAwaitTest.swift +++ b/Tests/NimbleTests/AsyncAwaitTest.swift @@ -372,6 +372,9 @@ final class AsyncAwaitTest: XCTestCase { // swiftlint:disable:this type_body_len await failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { await expect { try self.doThrowError() }.alwaysTo(equal(0)) } + await failsWithErrorMessage("expected to always equal <0>, got (use beNil() to match nils)") { + await expect(nil).toAlways(equal(0)) + } } } diff --git a/Tests/NimbleTests/PollingTest+Require.swift b/Tests/NimbleTests/PollingTest+Require.swift index c47bf72c1..c91f2c590 100644 --- a/Tests/NimbleTests/PollingTest+Require.swift +++ b/Tests/NimbleTests/PollingTest+Require.swift @@ -174,6 +174,9 @@ final class PollingRequireTest: XCTestCase { failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { try require { try self.doThrowError() }.neverTo(equal(0)) } + failsWithErrorMessage("expected to never equal <1>, got <1>") { + try require(1).toNever(equal(1)) + } } func testToAlwaysPositiveMatches() throws { @@ -207,6 +210,9 @@ final class PollingRequireTest: XCTestCase { failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { try require { try self.doThrowError() }.alwaysTo(equal(0)) } + failsWithErrorMessage("expected to always equal <1>, got (use beNil() to match nils)") { + try require(nil).toAlways(equal(1)) + } } } diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index 0b7a7c394..cb0de8d9b 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -295,6 +295,9 @@ final class PollingTest: XCTestCase { failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { expect { try self.doThrowError() }.neverTo(equal(0)) } + failsWithErrorMessage("expected to never equal <0>, got <0>") { + expect(0).toNever(equal(0)) + } } func testToAlwaysPositiveMatches() { @@ -328,6 +331,9 @@ final class PollingTest: XCTestCase { failsWithErrorMessage("unexpected error thrown: <\(errorToThrow)>") { expect { try self.doThrowError() }.alwaysTo(equal(0)) } + failsWithErrorMessage("expected to always equal <0>, got (use beNil() to match nils)") { + expect(nil).toAlways(equal(0)) + } } }