From 27ec27a3f3b4ccc2e02d041724072a4c4bcf879b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Mar 2019 15:07:04 -0600 Subject: [PATCH] Add array matcher --- src/spectator/dsl/matcher_dsl.cr | 20 +++++ src/spectator/matchers/array_matcher.cr | 106 ++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/spectator/matchers/array_matcher.cr diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 6285c33..8311005 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -447,6 +447,26 @@ module Spectator::DSL have_value({{expected}}) end + # Indicates that some set should contain some values in exact order. + # + # Example: + # ``` + # expect([1, 2, 3]).to contain_exactly(1, 2, 3) + # ``` + macro contain_exactly(*expected) + ::Spectator::Matchers::ArrayMatcher.new({{expected}}, {{expected.stringify}}) + end + + # Indicates that some set should contain the same values in exact order as another set. + # + # Example: + # ``` + # expect([1, 2, 3]).to match_array([1, 2, 3]) + # ``` + macro match_array(expected) + ::Spectator::Matchers::ArrayMatcher.new({{expected}}, {{expected.stringify}}) + end + # Indicates that some value should have a set of attributes matching some conditions. # A list of named arguments are expected. # The names correspond to the attributes in the instance to check. diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr new file mode 100644 index 0000000..f5fa171 --- /dev/null +++ b/src/spectator/matchers/array_matcher.cr @@ -0,0 +1,106 @@ +require "./value_matcher" + +module Spectator::Matchers + struct ArrayMatcher(ExpectedType) < ValueMatcher(ExpectedType) + # Determines whether the matcher is satisfied with the partial given to it. + # `MatchData` is returned that contains information about the match. + def match(partial) + actual = partial.actual.to_a + values = ExpectedActual.new(expected, label, actual, partial.label) + if values.expected.size == values.actual.size + index = 0 + values.expected.zip(values.actual) do |expected, actual| + return ContentMatchData.new(index, values) unless expected == actual + index += 1 + end + IdenticalMatchData.new(values) + else + SizeMatchData.new(values) + end + 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(ExpectedType, 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(ExpectedType, 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(ExpectedType, 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(ExpectedType, 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)" + end + end + end +end