111 lines
3.9 KiB
Crystal
111 lines
3.9 KiB
Crystal
require "./value_matcher"
|
|
|
|
module Spectator::Matchers
|
|
# Matcher that tests whether a value is in a given range.
|
|
# The `Range#includes?` method is used for this check.
|
|
struct RangeMatcher(ExpectedType) < ValueMatcher(ExpectedType)
|
|
# Short text about the matcher's purpose.
|
|
# This explains what condition satisfies the matcher.
|
|
# The description is used when the one-liner syntax is used.
|
|
def description : String
|
|
"is in #{expected.label}"
|
|
end
|
|
|
|
# Returns a new matcher, with the same bounds, but uses an inclusive range.
|
|
def inclusive
|
|
label = expected.label
|
|
new_range = Range.new(range.begin, range.end, exclusive: false)
|
|
expected = Value.new(new_range, label)
|
|
RangeMatcher.new(expected)
|
|
end
|
|
|
|
# Returns a new matcher, with the same bounds, but uses an exclusive range.
|
|
def exclusive
|
|
label = expected.label
|
|
new_range = Range.new(range.begin, range.end, exclusive: true)
|
|
expected = Value.new(new_range, label)
|
|
RangeMatcher.new(expected)
|
|
end
|
|
|
|
# Checks whether the matcher is satisfied with the expression given to it.
|
|
private def match?(actual : Expression(T)) : Bool forall T
|
|
actual_value = actual.value
|
|
expected_value = expected.value
|
|
if expected_value.is_a?(Range) && actual_value.is_a?(Comparable)
|
|
return match_impl?(expected_value, actual_value)
|
|
end
|
|
return false unless actual_value.is_a?(Comparable(typeof(expected_value.begin)))
|
|
expected_value.includes?(actual_value)
|
|
end
|
|
|
|
private def match_impl?(expected_value : Range(B, E), actual_value : Comparable(B)) : Bool forall B, E
|
|
expected_value.includes?(actual_value)
|
|
end
|
|
|
|
private def match_impl?(expected_value : Range(B, E), actual_value : T) : Bool forall B, E, T
|
|
return false unless actual_value.is_a?(B) || actual_value.is_a?(Comparable(B))
|
|
expected_value.includes?(actual_value)
|
|
end
|
|
|
|
private def match_impl?(expected_value : Range(Number, Number), actual_value : Number) : Bool
|
|
expected_value.includes?(actual_value)
|
|
end
|
|
|
|
# Message displayed when the matcher isn't satisfied.
|
|
#
|
|
# This is only called when `#match?` returns false.
|
|
#
|
|
# The message should typically only contain the test expression labels.
|
|
# Actual values should be returned by `#values`.
|
|
private def failure_message(actual) : String
|
|
"#{actual.label} is not in #{expected.label} (#{exclusivity})"
|
|
end
|
|
|
|
# Message displayed when the matcher isn't satisfied and is negated.
|
|
# This is essentially what would satisfy the matcher if it wasn't negated.
|
|
#
|
|
# This is only called when `#does_not_match?` returns false.
|
|
#
|
|
# The message should typically only contain the test expression labels.
|
|
# Actual values should be returned by `#values`.
|
|
private def failure_message_when_negated(actual) : String
|
|
"#{actual.label} is in #{expected.label} (#{exclusivity})"
|
|
end
|
|
|
|
# Additional information about the match failure.
|
|
# The return value is a NamedTuple with Strings for each value.
|
|
private def values(actual)
|
|
{
|
|
lower: ">= #{range.begin.inspect}",
|
|
upper: "#{exclusive? ? "<" : "<="} #{range.end.inspect}",
|
|
actual: actual.value.inspect,
|
|
}
|
|
end
|
|
|
|
# Additional information about the match failure when negated.
|
|
# The return value is a NamedTuple with Strings for each value.
|
|
private def negated_values(actual)
|
|
{
|
|
lower: "< #{range.begin.inspect}",
|
|
upper: "#{exclusive? ? ">=" : ">"} #{range.end.inspect}",
|
|
actual: actual.value.inspect,
|
|
}
|
|
end
|
|
|
|
# Gets the expected range.
|
|
private def range
|
|
expected.value
|
|
end
|
|
|
|
# Indicates whether the range is inclusive or exclusive.
|
|
private def exclusive?
|
|
range.exclusive?
|
|
end
|
|
|
|
# Produces a string "inclusive" or "exclusive" based on the range.
|
|
private def exclusivity
|
|
exclusive? ? "exclusive" : "inclusive"
|
|
end
|
|
end
|
|
end
|