From 7e73ec2fe1f8168702a40bfbfecf4c5ccfcf5153 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 22:46:06 -0600 Subject: [PATCH 1/7] Allow passing arguments to predicate matcher --- src/spectator/dsl/matcher_dsl.cr | 2 +- src/spectator/matchers/predicate_matcher.cr | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 7d14334..803b18e 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -574,7 +574,7 @@ module Spectator::DSL macro method_missing(call) {% if call.name.starts_with?("be_") %} {% method_name = call.name[3..-1] %} # Remove be_ prefix. - ::Spectator::Matchers::PredicateMatcher(NamedTuple({{method_name}}: Nil)).new + ::Spectator::Matchers::PredicateMatcher.new({ {{method_name}}: Tuple.new({{call.args.splat}}) }, {{method_name.stringify}}) {% else %} {% raise "Undefined local variable or method '#{call}'" %} {% end %} diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr index 41f9f44..71d0229 100644 --- a/src/spectator/matchers/predicate_matcher.cr +++ b/src/spectator/matchers/predicate_matcher.cr @@ -4,7 +4,8 @@ module Spectator::Matchers # Matcher that tests one or more predicates (methods ending in '?'). # The `ExpectedType` type param should be a `NamedTuple`. # Each key in the tuple is a predicate (without the '?') to test. - struct PredicateMatcher(ExpectedType) < Matcher + # Each value is a a `Tuple` of arguments to pass to the predicate method. + struct PredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType) # Textual representation of what the matcher expects. # Constructs the label from the type parameters. def label @@ -36,7 +37,7 @@ module Spectator::Matchers {% begin %} { {% for attribute in ExpectedType.keys %} - {{attribute}}: actual.{{attribute}}?, + {{attribute}}: actual.{{attribute}}?(*@expected[{{attribute.symbolize}}]), {% end %} } {% end %} From 36f53d82db04cd4b16c8771a5ee8fb6441afd8b1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 22:53:12 -0600 Subject: [PATCH 2/7] Use label from matcher macro --- src/spectator/matchers/predicate_matcher.cr | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr index 71d0229..1abd791 100644 --- a/src/spectator/matchers/predicate_matcher.cr +++ b/src/spectator/matchers/predicate_matcher.cr @@ -6,12 +6,6 @@ module Spectator::Matchers # Each key in the tuple is a predicate (without the '?') to test. # Each value is a a `Tuple` of arguments to pass to the predicate method. struct PredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # Textual representation of what the matcher expects. - # Constructs the label from the type parameters. - def label - {{ExpectedType.keys.splat.stringify}} - end - # Determines whether the matcher is satisfied with the value given to it. private def match?(values) # Test each predicate and immediately return false if one is false. @@ -27,7 +21,7 @@ module Spectator::Matchers # `MatchData` is returned that contains information about the match. def match(partial) : MatchData values = snapshot_values(partial.actual) - MatchData.new(match?(values), values, partial.label) + MatchData.new(match?(values), values, partial.label, label) end # Captures all of the actual values. @@ -46,7 +40,7 @@ module Spectator::Matchers # Match data specific to this matcher. private struct MatchData(ActualType) < MatchData # Creates the match data. - def initialize(matched, @named_tuple : ActualType, @actual_label : String) + def initialize(matched, @named_tuple : ActualType, @actual_label : String, @expected_label : String) super(matched) end @@ -56,13 +50,13 @@ module Spectator::Matchers # Describes the condition that satisfies the matcher. # This is informational and displayed to the end-user. def message - "#{@actual_label} is " + {{ActualType.keys.splat.stringify}} + "#{@actual_label} is #{@expected_label}" end # Describes the condition that won't satsify the matcher. # This is informational and displayed to the end-user. def negated_message - "#{@actual_label} is not " + {{ActualType.keys.splat.stringify}} + "#{@actual_label} is not #{@expected_label}" end end end From 45f0f7f6d112258244cbcda7055d5611e05eca45 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 23:06:39 -0600 Subject: [PATCH 3/7] Include predicate arguments in label --- src/spectator/dsl/matcher_dsl.cr | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 803b18e..0225a6d 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -573,10 +573,22 @@ module Spectator::DSL # ``` macro method_missing(call) {% if call.name.starts_with?("be_") %} - {% method_name = call.name[3..-1] %} # Remove be_ prefix. - ::Spectator::Matchers::PredicateMatcher.new({ {{method_name}}: Tuple.new({{call.args.splat}}) }, {{method_name.stringify}}) + {% method_name = call.name[3..-1] %} # Remove be_ prefix. + descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } + label = String::Builder.new({{method_name.stringify}}) + {% unless call.args.empty? %} + label << '(' + {% for arg, index in call.args %} + label << {{arg}} + {% if index < call.args.size - 1 %} + label << ", " + {% end %} + {% end %} + label << ')' + {% end %} + ::Spectator::Matchers::PredicateMatcher.new(descriptor, label.to_s) {% else %} - {% raise "Undefined local variable or method '#{call}'" %} + {% raise "Undefined local variable or method '#{call}'" %} {% end %} end end From 16bcce59ae378a8da8ec482b9ae1c0ae1dc68ed5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 23:14:58 -0600 Subject: [PATCH 4/7] Handle `have_` prefix for matcher --- src/spectator/dsl/matcher_dsl.cr | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 0225a6d..4136c27 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -561,7 +561,7 @@ module Spectator::DSL end # Used to create predicate matchers. - # Any missing method that starts with 'be_' will be handled. + # Any missing method that starts with 'be_' or 'have_' will be handled. # All other method names will be ignored and raise a compile-time error. # # This can be used to simply check a predicate method that ends in '?'. @@ -570,26 +570,35 @@ module Spectator::DSL # expect("foobar").to be_ascii_only # # Is equivalent to: # expect("foobar".ascii_only?).to be_true + # + # expect("foobar").to_not have_back_references + # # Is equivalent to: + # expect("foobar".has_back_references?).to_not be_true # ``` macro method_missing(call) {% if call.name.starts_with?("be_") %} - {% method_name = call.name[3..-1] %} # Remove be_ prefix. - descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } - label = String::Builder.new({{method_name.stringify}}) - {% unless call.args.empty? %} - label << '(' - {% for arg, index in call.args %} - label << {{arg}} - {% if index < call.args.size - 1 %} - label << ", " - {% end %} - {% end %} - label << ')' - {% end %} - ::Spectator::Matchers::PredicateMatcher.new(descriptor, label.to_s) + # Remove `be_` prefix. + {% method_name = call.name[3..-1] %} + {% elsif call.name.starts_with?("have_") %} + # Swap `have_` with `has_`. + {% method_name = ("has_" + call.name[5..-1].stringify).id %} {% else %} {% raise "Undefined local variable or method '#{call}'" %} {% end %} + + descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } + label = String::Builder.new({{method_name.stringify}}) + {% unless call.args.empty? %} + label << '(' + {% for arg, index in call.args %} + label << {{arg}} + {% if index < call.args.size - 1 %} + label << ", " + {% end %} + {% end %} + label << ')' + {% end %} + ::Spectator::Matchers::PredicateMatcher.new(descriptor, label.to_s) end end end From 091cbaa81a9eddc88349153b81697904857e8a54 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 23:27:16 -0600 Subject: [PATCH 5/7] Cleanup have_ variant by using a new matcher --- src/spectator/dsl/matcher_dsl.cr | 8 ++- .../matchers/have_predicate_matcher.cr | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/spectator/matchers/have_predicate_matcher.cr diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 4136c27..665e44f 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -579,9 +579,11 @@ module Spectator::DSL {% if call.name.starts_with?("be_") %} # Remove `be_` prefix. {% method_name = call.name[3..-1] %} + {% matcher = "PredicateMatcher" %} {% elsif call.name.starts_with?("have_") %} - # Swap `have_` with `has_`. - {% method_name = ("has_" + call.name[5..-1].stringify).id %} + # Remove `have_` prefix. + {% method_name = call.name[5..-1] %} + {% matcher = "HavePredicateMatcher" %} {% else %} {% raise "Undefined local variable or method '#{call}'" %} {% end %} @@ -598,7 +600,7 @@ module Spectator::DSL {% end %} label << ')' {% end %} - ::Spectator::Matchers::PredicateMatcher.new(descriptor, label.to_s) + ::Spectator::Matchers::{{matcher.id}}.new(descriptor, label.to_s) end end end diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr new file mode 100644 index 0000000..bba32ae --- /dev/null +++ b/src/spectator/matchers/have_predicate_matcher.cr @@ -0,0 +1,64 @@ +require "./value_matcher" + +module Spectator::Matchers + # Matcher that tests one or more "has" predicates + # (methods ending in '?' and starting with 'has_'). + # The `ExpectedType` type param should be a `NamedTuple`. + # Each key in the tuple is a predicate (without the '?' and 'has_' prefix) to test. + # Each value is a a `Tuple` of arguments to pass to the predicate method. + struct HavePredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType) + # Determines whether the matcher is satisfied with the value given to it. + private def match?(values) + # Test each predicate and immediately return false if one is false. + {% for attribute in ExpectedType.keys %} + return false unless values[{{attribute.symbolize}}] + {% end %} + + # All checks passed if this point is reached. + true + end + + # Determines whether the matcher is satisfied with the partial given to it. + # `MatchData` is returned that contains information about the match. + def match(partial) : MatchData + values = snapshot_values(partial.actual) + MatchData.new(match?(values), values, partial.label, label) + end + + # Captures all of the actual values. + # A `NamedTuple` is returned, + # with each key being the attribute. + private def snapshot_values(actual) + {% begin %} + { + {% for attribute in ExpectedType.keys %} + {{attribute}}: actual.has_{{attribute}}?(*@expected[{{attribute.symbolize}}]), + {% end %} + } + {% end %} + end + + # Match data specific to this matcher. + private struct MatchData(ActualType) < MatchData + # Creates the match data. + def initialize(matched, @named_tuple : ActualType, @actual_label : String, @expected_label : String) + super(matched) + end + + # Information about the match. + getter named_tuple + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@actual_label} has #{@expected_label}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@actual_label} does not have #{@expected_label}" + end + end + end +end From 0164d2973f3963c163aa5406017c1b24d46043c0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 23:39:23 -0600 Subject: [PATCH 6/7] Fix predicate tests and add new ones for have_ variant --- spec/matchers/have_predicate_matcher_spec.cr | 87 ++++++++++++++++++++ spec/matchers/predicate_matcher_spec.cr | 26 +++--- 2 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 spec/matchers/have_predicate_matcher_spec.cr diff --git a/spec/matchers/have_predicate_matcher_spec.cr b/spec/matchers/have_predicate_matcher_spec.cr new file mode 100644 index 0000000..5b49382 --- /dev/null +++ b/spec/matchers/have_predicate_matcher_spec.cr @@ -0,0 +1,87 @@ +require "../spec_helper" + +describe Spectator::Matchers::HavePredicateMatcher do + describe "#match" do + context "returned MatchData" do + describe "#match?" do + context "with a true predicate" do + it "is true" do + value = "foo\\bar" + partial = new_partial(value) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references") + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + context "with a false predicate" do + it "is false" do + value = "foobar" + partial = new_partial(value) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references") + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + end + + describe "#values" do + it "contains a key for each expected attribute" do + value = "foobar" + partial = new_partial(value) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references") + match_data = matcher.match(partial) + match_data_has_key?(match_data.values, :back_references).should be_true + end + + it "has the actual values" do + value = "foobar" + partial = new_partial(value) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references") + match_data = matcher.match(partial) + match_data_value_sans_prefix(match_data.values, :back_references)[:value].should eq(value.has_back_references?) + end + end + + describe "#message" do + it "contains the actual label" do + value = "foobar" + label = "blah" + partial = new_partial(value, label) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references") + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + it "contains the expected label" do + value = "foobar" + label = "blah" + partial = new_partial(value) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, label) + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the actual label" do + value = "foobar" + label = "blah" + partial = new_partial(value, label) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references") + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + it "contains the expected label" do + value = "foobar" + label = "blah" + partial = new_partial(value) + matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, label) + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + end +end diff --git a/spec/matchers/predicate_matcher_spec.cr b/spec/matchers/predicate_matcher_spec.cr index 5a9b2c2..031556b 100644 --- a/spec/matchers/predicate_matcher_spec.cr +++ b/spec/matchers/predicate_matcher_spec.cr @@ -8,7 +8,7 @@ describe Spectator::Matchers::PredicateMatcher do it "is true" do value = "foobar" partial = new_partial(value) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, "ascii_only") match_data = matcher.match(partial) match_data.matched?.should be_true end @@ -18,7 +18,7 @@ describe Spectator::Matchers::PredicateMatcher do it "is false" do value = "foobar" partial = new_partial(value) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(empty: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({empty: Tuple.new}, "empty") match_data = matcher.match(partial) match_data.matched?.should be_false end @@ -29,7 +29,7 @@ describe Spectator::Matchers::PredicateMatcher do it "contains a key for each expected attribute" do value = "foobar" partial = new_partial(value) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(empty: Nil, ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({empty: Tuple.new, ascii_only: Tuple.new}, "empty, ascii_only") match_data = matcher.match(partial) match_data_has_key?(match_data.values, :empty).should be_true match_data_has_key?(match_data.values, :ascii_only).should be_true @@ -38,7 +38,7 @@ describe Spectator::Matchers::PredicateMatcher do it "has the actual values" do value = "foobar" partial = new_partial(value) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(empty: Nil, ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({empty: Tuple.new, ascii_only: Tuple.new}, "empty, ascii_only") match_data = matcher.match(partial) match_data_value_sans_prefix(match_data.values, :empty)[:value].should eq(value.empty?) match_data_value_sans_prefix(match_data.values, :ascii_only)[:value].should eq(value.ascii_only?) @@ -50,17 +50,18 @@ describe Spectator::Matchers::PredicateMatcher do value = "foobar" label = "blah" partial = new_partial(value, label) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, "ascii_only") match_data = matcher.match(partial) match_data.message.should contain(label) end - it "contains stringified form of predicate" do + it "contains the expected label" do value = "foobar" + label = "blah" partial = new_partial(value) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, label) match_data = matcher.match(partial) - match_data.message.should contain("ascii_only") + match_data.message.should contain(label) end end @@ -69,17 +70,18 @@ describe Spectator::Matchers::PredicateMatcher do value = "foobar" label = "blah" partial = new_partial(value, label) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, "ascii_only") match_data = matcher.match(partial) match_data.negated_message.should contain(label) end - it "contains stringified form of predicate" do + it "contains the expected label" do value = "foobar" + label = "blah" partial = new_partial(value) - matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new + matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, label) match_data = matcher.match(partial) - match_data.negated_message.should contain("ascii_only") + match_data.negated_message.should contain(label) end end end From 7a8cf08c73f1620e385e908cb510e286ff504f22 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 1 Jun 2019 23:41:06 -0600 Subject: [PATCH 7/7] Update README (matchers) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e98ccd..ba71627 100644 --- a/README.md +++ b/README.md @@ -302,8 +302,8 @@ Items not marked as completed may have partial implementations. - [X] Error matchers - `raise_error` - [ ] Yield matchers - `yield_control[.times]`, `yield_with_args[.times]`, `yield_with_no_args[.times]`, `yield_successive_args` - [ ] Output matchers - `output[.to_stdout|.to_stderr]` + - [X] Predicate matchers - `be_x`, `have_x` - [ ] Misc. matchers - - [ ] `exist` - [X] `match` - [ ] `satisfy` - [ ] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]`