diff --git a/Sources/Nimble/Expression.swift b/Sources/Nimble/Expression.swift index 1ac29b254..bae39e451 100644 --- a/Sources/Nimble/Expression.swift +++ b/Sources/Nimble/Expression.swift @@ -94,4 +94,13 @@ public struct Expression { isClosure: isClosure ) } + + public func withCaching() -> Expression { + return Expression( + memoizedExpression: memoizedClosure { try self.evaluate() }, + location: self.location, + withoutCaching: false, + isClosure: isClosure + ) + } } diff --git a/Sources/Nimble/Matchers/PostNotification.swift b/Sources/Nimble/Matchers/PostNotification.swift index e04bf556e..750b46a88 100644 --- a/Sources/Nimble/Matchers/PostNotification.swift +++ b/Sources/Nimble/Matchers/PostNotification.swift @@ -61,7 +61,7 @@ private func _postNotifications( withoutCaching: true ) - assert(pthread_equal(mainThread, pthread_self()) != 0, "Only expecting closure to be evaluated on main thread.") + assert(Thread.isMainThread, "Only expecting closure to be evaluated on main thread.") if !once { once = true _ = try actualExpression.evaluate() diff --git a/Sources/Nimble/Matchers/SatisfyAllOf.swift b/Sources/Nimble/Matchers/SatisfyAllOf.swift index 50ab64150..2df6a7091 100644 --- a/Sources/Nimble/Matchers/SatisfyAllOf.swift +++ b/Sources/Nimble/Matchers/SatisfyAllOf.swift @@ -8,10 +8,11 @@ public func satisfyAllOf(_ predicates: Predicate...) -> Predicate { /// provided in the array of matchers. public func satisfyAllOf(_ predicates: [Predicate]) -> Predicate { return Predicate.define { actualExpression in + let cachedExpression = actualExpression.withCaching() var postfixMessages = [String]() var status: PredicateStatus = .matches for predicate in predicates { - let result = try predicate.satisfies(actualExpression) + let result = try predicate.satisfies(cachedExpression) if result.status == .fail { status = .fail } else if result.status == .doesNotMatch, status != .fail { @@ -21,7 +22,7 @@ public func satisfyAllOf(_ predicates: [Predicate]) -> Predicate { } var msg: ExpectationMessage - if let actualValue = try actualExpression.evaluate() { + if let actualValue = try cachedExpression.evaluate() { msg = .expectedCustomValueTo( "match all of: " + postfixMessages.joined(separator: ", and "), actual: "\(actualValue)" diff --git a/Sources/Nimble/Matchers/SatisfyAnyOf.swift b/Sources/Nimble/Matchers/SatisfyAnyOf.swift index bd027d2f8..a8fadf8b6 100644 --- a/Sources/Nimble/Matchers/SatisfyAnyOf.swift +++ b/Sources/Nimble/Matchers/SatisfyAnyOf.swift @@ -8,10 +8,11 @@ public func satisfyAnyOf(_ predicates: Predicate...) -> Predicate { /// provided in the array of matchers. public func satisfyAnyOf(_ predicates: [Predicate]) -> Predicate { return Predicate.define { actualExpression in + let cachedExpression = actualExpression.withCaching() var postfixMessages = [String]() var status: PredicateStatus = .doesNotMatch for predicate in predicates { - let result = try predicate.satisfies(actualExpression) + let result = try predicate.satisfies(cachedExpression) if result.status == .fail { status = .fail } else if result.status == .matches, status != .fail { @@ -21,7 +22,7 @@ public func satisfyAnyOf(_ predicates: [Predicate]) -> Predicate { } var msg: ExpectationMessage - if let actualValue = try actualExpression.evaluate() { + if let actualValue = try cachedExpression.evaluate() { msg = .expectedCustomValueTo( "match one of: " + postfixMessages.joined(separator: ", or "), actual: "\(actualValue)" diff --git a/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift b/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift index caae73501..e57626329 100644 --- a/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift +++ b/Tests/NimbleTests/Matchers/SatisfyAllOfTest.swift @@ -48,4 +48,16 @@ final class SatisfyAllOfTest: XCTestCase { expect(false).toNot(beTrue() && beFalse()) expect(true).toNot(beTruthy() && beFalsy()) } + + func testSatisfyAllOfCachesExpressionBeforePassingToPredicates() { + // This is not a great example of assertion writing - functions being asserted on in Expressions should not have side effects. + // But we should still handle those cases anyway. + var value: Int = 0 + func testFunction() -> Int { + value += 1 + return value + } + + expect(testFunction()).toEventually(satisfyAllOf(equal(1), equal(1))) + } } diff --git a/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift b/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift index 098e3a3a8..b8b47a2a0 100644 --- a/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift +++ b/Tests/NimbleTests/Matchers/SatisfyAnyOfTest.swift @@ -46,4 +46,18 @@ final class SatisfyAnyOfTest: XCTestCase { expect(false).to(beTrue() || beFalse()) expect(true).to(beTruthy() || beFalsy()) } + + func testSatisfyAllOfCachesExpressionBeforePassingToPredicates() { + // This is not a great example of assertion writing - functions being asserted on in Expressions should not have side effects. + // But we should still handle those cases anyway. + var value: Int = 0 + func testFunction() -> Int { + value += 1 + return value + } + + // This demonstrates caching because the first time this is evaluated, the function should return 1, which doesn't pass the `equal(0)`. + // Next time, it'll return 2, which doesn't pass the `equal(1)`. + expect(testFunction()).toEventually(satisfyAnyOf(equal(0), equal(1))) + } }