diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index 3857806..1efef4d 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -5,129 +5,72 @@ module Spectator::Matchers # Matcher for checking that the contents of one array (or similar type) # has the exact same contents as another and in the same order. struct ArrayMatcher(ExpectedType) < ValueMatcher(Enumerable(ExpectedType)) - # Determines whether the matcher is satisfied with the partial given to it. - def match(partial, negated = false) - actual = partial.actual.to_a - expected_elements = expected.to_a - values = ExpectedActual.new(expected_elements, label, actual, partial.label) - if values.expected.size == values.actual.size + def description + "contains exactly #{expected.label}" + end + + private def failure_message(actual) + {% raise "This method should never be called" %} + end + + private def failure_message_when_negated(actual) + {% raise "This method should never be called" %} + end + + private def match?(actual) + {% raise "This method should never be called" %} + end + + private def does_not_match?(actual) + {% raise "This method should never be called" %} + end + + def match(actual) + actual_elements = actual.value.to_a + expected_elements = expected.value.to_a + if expected_elements.size == actual_elements.size index = 0 - values.expected.zip(values.actual) do |expected, element| - return ContentMatchData.new(index, values) unless expected == element + expected_elements.zip(actual_elements) do |expected_element, actual_element| + unless expected_element == actual_element + return FailedMatchData.new("#{actual.label} does not contain exactly #{expected.label} (element mismatch)", + [ + LabeledValue.new(expected_element.inspect, "expected"), + LabeledValue.new(actual_element.inspect, "actual"), + LabeledValue.new(index.to_s, "index"), + ]) + end index += 1 end - IdenticalMatchData.new(values) + # Success. + SuccessfulMatchData.new else - SizeMatchData.new(values) + # Size mismatch. + FailedMatchData.new("#{actual.label} does not contain exactly #{expected.label} (size mismatch)", + [ + LabeledValue.new(expected_elements.inspect, "expected"), + LabeledValue.new(actual_elements.inspect, "actual"), + LabeledValue.new(expected_elements.size, "expected size"), + LabeledValue.new(actual_elements.size, "actual size"), + ]) end end - # Creates the value matcher. - # The label should be a string representation of the expectation. - # The expected value is stored for later use. - def initialize(expected : Enumerable(ExpectedType), label : String) - super - end - - # Creates the value matcher. - # The label is generated by calling `#to_s` on the expected value. - # The expected value is stored for later use. - def initialize(expected : Enumerable(ExpectedType)) - super - end - - # Returns a matcher that uses the same expected array, but allows unordered items. - def in_any_order - UnorderedArrayMatcher.new(@expected, @label) - end - - # Returns self. - # Exists for syntax to ensure in-order matching is performed. - def in_order - self - end - - # Common functionality for all match data for this matcher. - private abstract struct CommonMatchData(ExpectedType, ActualType) < MatchData - # Creates the match data. - def initialize(matched, @values : ExpectedActual(Array(ExpectedType), Array(ActualType))) - super(matched) - end - - # Basic information about the match. - def named_tuple - { - expected: NegatableMatchDataValue.new(@values.expected), - actual: @values.actual, - } - end - - # Describes the condition that satisfies the matcher. - # This is informational and displayed to the end-user. - def message - "#{@values.actual_label} contains exactly #{@values.expected_label}" - end - end - - # Match data specific to this matcher. - # This type is used when the actual value matches the expected value. - private struct IdenticalMatchData(ExpectedType, ActualType) < CommonMatchData(ExpectedType, ActualType) - # Creates the match data. - def initialize(values : ExpectedActual(Array(ExpectedType), Array(ActualType))) - super(true, values) - end - - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message - "#{@values.actual_label} does not contain exactly #{@values.expected_label}" - end - end - - # Match data specific to this matcher. - # This type is used when the actual size differs from the expected size. - private struct SizeMatchData(ExpectedType, ActualType) < CommonMatchData(ExpectedType, ActualType) - # Creates the match data. - def initialize(values : ExpectedActual(Array(ExpectedType), Array(ActualType))) - super(false, values) - end - - # Information about the match. - def named_tuple - super.merge({ - "expected size": NegatableMatchDataValue.new(@values.expected.size), - "actual size": @values.actual.size, - }) - end - - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message - "#{@values.actual_label} does not contain exactly #{@values.expected_label} (size differs)" - end - end - - # Match data specific to this matcher. - # This type is used when the actual contents differs from the expected contents. - private struct ContentMatchData(ExpectedType, ActualType) < CommonMatchData(ExpectedType, ActualType) - # Creates the match data. - def initialize(@index : Int32, values : ExpectedActual(Array(ExpectedType), Array(ActualType))) - super(false, values) - end - - # Information about the match. - def named_tuple - super.merge({ - index: @index, - "expected element": NegatableMatchDataValue.new(@values.expected[@index]), - "actual element": @values.actual[@index], - }) - end - - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message - "#{@values.actual_label} does not contain exactly #{@values.expected_label} (content differs)" + def negated_match(actual) + actual_elements = actual.value.to_a + expected_elements = expected.value.to_a + if expected_elements.size == actual_elements.size + index = 0 + expected_elements.zip(actual_elements) do |expected_element, actual_element| + return SuccessfulMatchData.new unless expected_element == actual_element + index += 1 + end + FailedMatchData.new("#{actual.label} contains exactly #{expected.label}", + [ + LabeledValue.new("Not #{expected_elements.inspect}", "expected"), + LabeledValue.new(actual_elements.inspect, "actual"), + ]) + else + SuccessfulMatchData.new end end end