Add contain matcher improvements to have matcher

This commit is contained in:
Michael Miller 2020-12-23 14:38:30 -07:00
parent edf8ae36df
commit 20caed9262
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD

View file

@ -4,7 +4,14 @@ module Spectator::Matchers
# Matcher that tests whether a value, such as a `String` or `Array`, matches one or more values. # Matcher that tests whether a value, such as a `String` or `Array`, matches one or more values.
# For a `String`, the `includes?` method is used. # For a `String`, the `includes?` method is used.
# Otherwise, it expects an `Enumerable` and iterates over each item until === is true. # Otherwise, it expects an `Enumerable` and iterates over each item until === is true.
struct HaveMatcher(ExpectedType) < ValueMatcher(ExpectedType) struct HaveMatcher(ExpectedType) < Matcher
# Expected value and label.
private getter expected
# Creates the matcher with an expected value.
def initialize(@expected : TestValue(ExpectedType))
end
# Short text about the matcher's purpose. # Short text about the matcher's purpose.
# This explains what condition satisfies the matcher. # This explains what condition satisfies the matcher.
# The description is used when the one-liner syntax is used. # The description is used when the one-liner syntax is used.
@ -12,62 +19,102 @@ module Spectator::Matchers
"includes #{expected.label}" "includes #{expected.label}"
end end
# Checks whether the matcher is satisifed with the expression given to it. # Entrypoint for the matcher, forwards to the correct method for string or enumerable.
private def match?(actual : TestExpression(T)) : Bool forall T def match(actual : TestExpression(T)) : MatchData forall T
if (value = actual.value).is_a?(String) if (value = actual.value).is_a?(String)
match_string?(value) match_string(value, actual.label)
else else
match_enumerable?(value) match_enumerable(value, actual.label)
end
end
# Actually performs the test against the expression.
private def match_enumerable(actual_value, actual_label)
array = actual_value.to_a
missing = expected.value.reject do |item|
array.any? do |element|
item === element
end
end
if missing.empty?
# Contents are present.
SuccessfulMatchData.new(description)
else
# Content is missing.
FailedMatchData.new(description, "#{actual_label} does not include #{expected.label}",
expected: expected.value.inspect,
actual: actual_value.inspect,
missing: missing.inspect,
)
end end
end end
# Checks if a `String` matches the expected values. # Checks if a `String` matches the expected values.
# The `includes?` method is used for this check. # The `includes?` method is used for this check.
private def match_string?(value) private def match_string(actual_value, actual_label)
expected.value.all? do |item| missing = expected.value.reject do |item|
value.includes?(item) actual_value.includes?(item)
end
if missing.empty?
SuccessfulMatchData.new(description)
else
FailedMatchData.new(description, "#{actual_label} does not include #{expected.label}",
expected: expected.value.inspect,
actual: actual_value.inspect,
missing: missing.inspect,
)
end end
end end
# Checks if an `Enumerable` matches the expected values. # Performs the test against the expression, but inverted.
# The `===` operator is used on every item. # A successful match with `#match` should normally fail for this method, and vice-versa.
private def match_enumerable?(value) def negated_match(actual : TestExpression(T)) : MatchData forall T
array = value.to_a if (value = actual.value).is_a?(String)
expected.value.all? do |item| negated_match_string(value, actual.label)
else
negated_match_enumerable(value, actual.label)
end
end
# Actually performs the negated test against the expression.
private def negated_match_enumerable(actual_value, actual_label)
array = actual_value.to_a
satisfied = expected.value.any? do |item|
array.any? do |element| array.any? do |element|
item === element item === element
end end
end end
if satisfied
# Contents are present.
FailedMatchData.new(description, "#{actual_label} includes #{expected.label}",
expected: "Not #{expected.value.inspect}",
actual: actual_value.inspect
)
else
# Content is missing.
SuccessfulMatchData.new(description)
end
end end
# Message displayed when the matcher isn't satisifed. # Checks if a `String` doesn't match the expected values.
# # The `includes?` method is used for this check.
# This is only called when `#match?` returns false. private def negated_match_string(actual_value, actual_label)
# satisfied = expected.value.any? do |item|
# The message should typically only contain the test expression labels. actual_value.includes?(item)
# Actual values should be returned by `#values`.
private def failure_message(actual) : String
"#{actual.label} does not include #{expected.label}"
end end
# Message displayed when the matcher isn't satisifed and is negated. if satisfied
# This is essentially what would satisfy the matcher if it wasn't negated. SuccessfulMatchData.new(description)
# else
# This is only called when `#does_not_match?` returns false. FailedMatchData.new(description, "#{actual_label} does not include #{expected.label}",
# expected: expected.value.inspect,
# The message should typically only contain the test expression labels. actual: actual_value.inspect,
# Actual values should be returned by `#values`. missing: missing.inspect,
private def failure_message_when_negated(actual) : String )
"#{actual.label} includes #{expected.label}" end
end
# Additional information about the match failure.
# The return value is a NamedTuple with Strings for each value.
private def values(actual)
{
subset: expected.value.inspect,
superset: actual.value.inspect,
}
end end
end end
end end