From 3e1ee7eb6d225756ae274002b175cf959b2b1a42 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 14 Jul 2019 09:39:27 -0600 Subject: [PATCH 01/22] Initial code for basic change matcher --- spec/matchers/change_matcher_spec.cr | 81 ++++++++++++++++++++++++ src/spectator/dsl/matcher_dsl.cr | 32 ++++++++++ src/spectator/matchers/change_matcher.cr | 60 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 spec/matchers/change_matcher_spec.cr create mode 100644 src/spectator/matchers/change_matcher.cr diff --git a/spec/matchers/change_matcher_spec.cr b/spec/matchers/change_matcher_spec.cr new file mode 100644 index 0000000..a101537 --- /dev/null +++ b/spec/matchers/change_matcher_spec.cr @@ -0,0 +1,81 @@ +require "../spec_helper" + +describe Spectator::Matchers::ChangeMatcher do + describe "#match" do + context "returned MatchData" do + context "with changing expression" do + describe "#matched?" do + it "is true" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + describe "#values" do + context "before" do + it "is the initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :before).value.should eq(0) + end + end + + context "after" do + it "is the resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :after).value.should eq(5) + end + end + end + + describe "#message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + end + end +end diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 18b30ab..77c54dd 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -506,6 +506,38 @@ module Spectator::DSL ::Spectator::Matchers::AttributesMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}})) end + # Indicates that some expression's value should change after taking an action. + # + # Examples: + # ``` + # i = 0 + # expect { i += 1 }.to change { i } + # expect { i += 0 }.to_not change { i } + # ``` + # + # ``` + # i = 0 + # expect { i += 5 }.to change { i }.from(0).to(5) + # ``` + # + # ``` + # i = 0 + # expect { i += 5 }.to change { i }.to(5) + # ``` + # + # ``` + # i = 0 + # expect { i += 5 }.to change { i }.from(0) + # ``` + # + # ``` + # i = 0 + # expect { i += 42 }.to change { i }.by(42) + # ``` + macro change(&expression) + ::Spectator::Matchers::ChangeMatcher.new("`" + {{expression.body.stringify}} + "`") {{expression}} + end + # Indicates that some block should raise an error. # # Examples: diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr new file mode 100644 index 0000000..f8bcb8f --- /dev/null +++ b/src/spectator/matchers/change_matcher.cr @@ -0,0 +1,60 @@ +require "./value_matcher" + +module Spectator::Matchers + # Matcher that tests whether an expression changed. + struct ChangeMatcher(ExpressionType) < ValueMatcher(ExpressionType) + # Determines whether the matcher is satisfied with the value given to it. + private def match?(after) + expected != after + 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) + partial.actual # Invoke action that might change the expression's value. + after = @expression.call # Retrieve the expression's value. + MatchData.new(match?(after), expected, after, partial.label, label) + end + + # Creates a new change matcher with a custom label. + def initialize(label : String, &expression : -> ExpressionType) + super(yield, label) + @expression = expression + end + + # Creates a new change matcher. + def initialize(&expression : -> ExpressionType) + super(yield, expression.to_s) + @expression = expression + end + + # Match data specific to this matcher. + private struct MatchData(ExpressionType) < MatchData + # Creates the match data. + def initialize(matched, @before : ExpressionType, @after : ExpressionType, + @action_label : String, @expression_label : String) + super(matched) + end + + # Information about the match. + def named_tuple + { + before: @before, + after: @after + } + end + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@action_label} changes #{@expression_label}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@action_label} does not change #{@expression_label}" + end + end + end +end From 24066c46a0d7ff8b5711fc808ef188bb36a61c35 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 14 Jul 2019 11:20:43 -0600 Subject: [PATCH 02/22] Allow MatchData to control whether they can be negated Values are still negated, but message and matching status are not. --- src/spectator/expectations/expectation.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/expectations/expectation.cr b/src/spectator/expectations/expectation.cr index 300f134..526d1a5 100644 --- a/src/spectator/expectations/expectation.cr +++ b/src/spectator/expectations/expectation.cr @@ -66,5 +66,10 @@ module Spectator::Expectations end end end + + # Indicates whether the match data should be negated. + private def negated? + !@match_data.override? && @negated + end end end From 946604b737536d0c93622fa9b87356a4eded43a3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 14 Jul 2019 11:56:10 -0600 Subject: [PATCH 03/22] Add tests for override cases Fix negated? method. --- spec/expectations/expectation_spec.cr | 106 ++++++++++++++++++++++ src/spectator/expectations/expectation.cr | 5 - 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/spec/expectations/expectation_spec.cr b/spec/expectations/expectation_spec.cr index 06540fc..eaac487 100644 --- a/spec/expectations/expectation_spec.cr +++ b/spec/expectations/expectation_spec.cr @@ -48,4 +48,110 @@ describe Spectator::Expectations::Expectation do end end end + + describe "#values" do + it "is the same as the match data values" do + value = 42 + match_data = new_matcher(value).match(new_partial(value)) + expectation = Spectator::Expectations::Expectation.new(match_data, false) + expectation_values = expectation.values + match_data.values.zip(expectation_values).each do |m, e| + m.label.should eq(e.label) + m.value.value.should eq(e.value.value) + end + end + + context "when negated" do + it "negates all negatable values" do + value = 42 + match_data = new_matcher(value).match(new_partial(value)) + expectation = Spectator::Expectations::Expectation.new(match_data, true) + expectation.values.each do |labeled_value| + label = labeled_value.label + value = labeled_value.value + value.to_s.should start_with(/not/i) if label == :expected + end + end + end + end + + describe "#actual_message" do + context "with a successful match" do + it "equals the matcher's #message" do + value = 42 + match_data = new_matcher(value).match(new_partial(value)) + match_data.matched?.should be_true # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, false) + expectation.actual_message.should eq(match_data.message) + end + + context "when negated" do + it "equals the matcher's #message" do + value = 42 + match_data = new_matcher(value).match(new_partial(value)) + match_data.matched?.should be_true # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, true) + expectation.actual_message.should eq(match_data.message) + end + end + end + + context "with an unsuccessful match" do + it "equals the matcher's #negated_message" do + match_data = new_matcher(42).match(new_partial(777)) + match_data.matched?.should be_false # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, false) + expectation.actual_message.should eq(match_data.negated_message) + end + + context "when negated" do + it "equals the matcher's #negated_message" do + match_data = new_matcher(42).match(new_partial(777)) + match_data.matched?.should be_false # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, true) + expectation.actual_message.should eq(match_data.negated_message) + end + end + end + end + + describe "#expected_message" do + context "with a successful match" do + it "equals the matcher's #message" do + value = 42 + match_data = new_matcher(value).match(new_partial(value)) + match_data.matched?.should be_true # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, false) + expectation.expected_message.should eq(match_data.message) + end + + context "when negated" do + it "equals the matcher's #negated_message" do + value = 42 + match_data = new_matcher(value).match(new_partial(value)) + match_data.matched?.should be_true # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, true) + expectation.expected_message.should eq(match_data.negated_message) + end + end + end + + context "with an unsuccessful match" do + it "equals the matcher's #message" do + match_data = new_matcher(42).match(new_partial(777)) + match_data.matched?.should be_false # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, false) + expectation.expected_message.should eq(match_data.message) + end + + context "when negated" do + it "equals the matcher's #negated_message" do + match_data = new_matcher(42).match(new_partial(777)) + match_data.matched?.should be_false # Sanity check. + expectation = Spectator::Expectations::Expectation.new(match_data, true) + expectation.expected_message.should eq(match_data.negated_message) + end + end + end + end end diff --git a/src/spectator/expectations/expectation.cr b/src/spectator/expectations/expectation.cr index 526d1a5..300f134 100644 --- a/src/spectator/expectations/expectation.cr +++ b/src/spectator/expectations/expectation.cr @@ -66,10 +66,5 @@ module Spectator::Expectations end end end - - # Indicates whether the match data should be negated. - private def negated? - !@match_data.override? && @negated - end end end From 770100891c588c52f52a0b43c5ac626d417241d5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 14 Jul 2019 12:24:43 -0600 Subject: [PATCH 04/22] Add change.from variant --- src/spectator/matchers/change_from_matcher.cr | 97 +++++++++++++++++++ src/spectator/matchers/change_matcher.cr | 6 ++ 2 files changed, 103 insertions(+) create mode 100644 src/spectator/matchers/change_from_matcher.cr diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr new file mode 100644 index 0000000..858fa6a --- /dev/null +++ b/src/spectator/matchers/change_from_matcher.cr @@ -0,0 +1,97 @@ +require "./value_matcher" + +module Spectator::Matchers + # Matcher that tests whether an expression changed from a specific value. + struct ChangeFromMatcher(ExpressionType, FromType) < ValueMatcher(ExpressionType) + # Determines whether the matcher is satisfied with the partial given to it. + # `MatchData` is returned that contains information about the match. + def match(partial) + partial.actual # Invoke action that might change the expression's value. + after = @expression.call # Retrieve the expression's value. + if expected != @actual_before + # Initial value isn't what was expected. + InitialMatchData.new(expected, @actual_before, after, partial.label, label) + else + # Check if the expression's value changed. + matched = expected != after + ChangeMatchData.new(matched, expected, @actual_before, after, partial.label, label) + end + end + + # Creates a new change matcher with a custom label. + def initialize(expression_label : String, expected_before : FromType, @actual_before : ExpressionType, &expression : -> ExpressionType) + super(expected_before, expression_label) + @expression = expression + end + + # Creates a new change matcher. + def initialize(expected_before : FromType, @actual_before : ExpressionType, &expression : -> ExpressionType) + super(expected_before, expression.to_s) + @expression = expression + end + + # Match data for when the initial value isn't the expected value. + private struct InitialMatchData(ExpressionType, FromType) < MatchData + # Creates the match data. + def initialize(@expected_before : FromType, @actual_before : ExpressionType, @after : ExpressionType, + @action_label : String, @expression_label : String) + super(false) + end + + # Do not allow negation of this match data. + def override? + true + end + + # Information about the match. + def named_tuple + { + "expected before": @expected_before, + "actual before": @actual_before, + "expected after": NegatableMatchDataValue.new(@expected_before, true), + "actual after": @after, + } + end + + # This is informational and displayed to the end-user. + def message + "#{@expression_label} is initially #{@expected_before}" + end + + # This is informational and displayed to the end-user. + def negated_message + "#{@expression_label} is not initially #{@expected_before}" + end + end + + private struct ChangeMatchData(ExpressionType, FromType) < MatchData + # Creates the match data. + def initialize(matched, @expected_before : FromType, @actual_before : ExpressionType, + @after : ExpressionType, @action_label : String, @expression_label : String) + super(matched) + end + + # Information about the match. + def named_tuple + { + "expected before": @expected_before, + "actual before": @actual_before, + "expected after": NegatableMatchDataValue.new(@expected_before, true), + "actual after": @after, + } + end + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@action_label} changed #{@expression_label} from #{@expected_before}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@action_label} did not change #{@expression_label} from #{@expected_before}" + end + end + end +end diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index f8bcb8f..64dda8e 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -1,3 +1,4 @@ +require "./change_from_matcher" require "./value_matcher" module Spectator::Matchers @@ -28,6 +29,11 @@ module Spectator::Matchers @expression = expression end + # Specifies what the initial value of the expression must be. + def from(value : T) forall T + ChangeFromMatcher.new(label, value, expected, &@expression) + end + # Match data specific to this matcher. private struct MatchData(ExpressionType) < MatchData # Creates the match data. From 59cf9395362060504eb6d220a9646bbbccc34557 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 19 Jul 2019 12:07:50 -0600 Subject: [PATCH 05/22] Don't store initial value in matcher --- src/spectator/matchers/change_from_matcher.cr | 27 +++++++++++-------- src/spectator/matchers/change_matcher.cr | 25 ++++++++--------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index 858fa6a..b41fdf3 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -2,31 +2,36 @@ require "./value_matcher" module Spectator::Matchers # Matcher that tests whether an expression changed from a specific value. - struct ChangeFromMatcher(ExpressionType, FromType) < ValueMatcher(ExpressionType) + struct ChangeFromMatcher(ExpressionType, FromType) < Matcher + # Textual representation of what the matcher expects. + # This shouldn't be used in the conditional logic, + # but for verbose output to help the end-user. + getter label : String + # Determines whether the matcher is satisfied with the partial given to it. # `MatchData` is returned that contains information about the match. def match(partial) - partial.actual # Invoke action that might change the expression's value. - after = @expression.call # Retrieve the expression's value. - if expected != @actual_before + before = @expression.call # Retrieve the expression's initial value. + partial.actual # Invoke action that might change the expression's value. + after = @expression.call # Retrieve the expression's value again. + if @expected_before != before # Initial value isn't what was expected. - InitialMatchData.new(expected, @actual_before, after, partial.label, label) + InitialMatchData.new(@expected_before, before, after, partial.label, label) else # Check if the expression's value changed. - matched = expected != after - ChangeMatchData.new(matched, expected, @actual_before, after, partial.label, label) + same = before == after + ChangeMatchData.new(!same, @expected_before, before, after, partial.label, label) end end # Creates a new change matcher with a custom label. - def initialize(expression_label : String, expected_before : FromType, @actual_before : ExpressionType, &expression : -> ExpressionType) - super(expected_before, expression_label) + def initialize(@label, @expected_before : FromType, &expression : -> ExpressionType) @expression = expression end # Creates a new change matcher. - def initialize(expected_before : FromType, @actual_before : ExpressionType, &expression : -> ExpressionType) - super(expected_before, expression.to_s) + def initialize(@expected_before : FromType, &expression : -> ExpressionType) + @label = expression.to_s @expression = expression end diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 64dda8e..72aa1f0 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -3,35 +3,36 @@ require "./value_matcher" module Spectator::Matchers # Matcher that tests whether an expression changed. - struct ChangeMatcher(ExpressionType) < ValueMatcher(ExpressionType) - # Determines whether the matcher is satisfied with the value given to it. - private def match?(after) - expected != after - end + struct ChangeMatcher(ExpressionType) < Matcher + # Textual representation of what the matcher expects. + # This shouldn't be used in the conditional logic, + # but for verbose output to help the end-user. + getter label : String # Determines whether the matcher is satisfied with the partial given to it. # `MatchData` is returned that contains information about the match. def match(partial) - partial.actual # Invoke action that might change the expression's value. - after = @expression.call # Retrieve the expression's value. - MatchData.new(match?(after), expected, after, partial.label, label) + before = @expression.call # Retrieve the expression's initial value. + partial.actual # Invoke action that might change the expression's value. + after = @expression.call # Retrieve the expression's value again. + same = before == after # Did the value change? + MatchData.new(!same, before, after, partial.label, label) end # Creates a new change matcher with a custom label. - def initialize(label : String, &expression : -> ExpressionType) - super(yield, label) + def initialize(@label, &expression : -> ExpressionType) @expression = expression end # Creates a new change matcher. def initialize(&expression : -> ExpressionType) - super(yield, expression.to_s) + @label = expression.to_s @expression = expression end # Specifies what the initial value of the expression must be. def from(value : T) forall T - ChangeFromMatcher.new(label, value, expected, &@expression) + ChangeFromMatcher.new(label, value, &@expression) end # Match data specific to this matcher. From 817128b2869d4ba3b03314ebb412a1e230a01bd7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 19 Jul 2019 12:27:50 -0600 Subject: [PATCH 06/22] Add tests for change.from --- spec/matchers/change_from_matcher_spec.cr | 191 ++++++++++++++++++++++ spec/matchers/change_matcher_spec.cr | 37 +++++ 2 files changed, 228 insertions(+) create mode 100644 spec/matchers/change_from_matcher_spec.cr diff --git a/spec/matchers/change_from_matcher_spec.cr b/spec/matchers/change_from_matcher_spec.cr new file mode 100644 index 0000000..521c117 --- /dev/null +++ b/spec/matchers/change_from_matcher_spec.cr @@ -0,0 +1,191 @@ +require "../spec_helper" + +describe Spectator::Matchers::ChangeFromMatcher do + describe "#match" do + context "returned MatchData" do + context "with a static expression" do + describe "#matched?" do + it "is false" do + i = 0 + partial = new_block_partial { i += 0 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + end + + context "with changing expression" do + describe "#matched?" do + it "is true" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + describe "#values" do + context "expected before" do + it "is the expected value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"expected before").value.should eq(0) + end + end + + context "actual before" do + it "is the initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) + end + end + + context "expected after" do + it "is the negated initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } + match_data = matcher.match(partial) + match_data_value_sans_prefix(match_data.values, :"expected after")[:value].should eq(0) + match_data_value_sans_prefix(match_data.values, :"expected after")[:to_s].should start_with("Not ") + end + end + + context "actual after" do + it "is the resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) + end + end + end + + describe "#message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(label, i) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(label, i) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + + context "with the wrong initial value" do + describe "#matched?" do + it "is false" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + describe "#values" do + context "expected before" do + it "is the expected value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"expected before").value.should eq(2) + end + end + + context "actual before" do + it "is the initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) + end + end + + context "expected after" do + it "is the negated initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_sans_prefix(match_data.values, :"expected after")[:value].should eq(2) + match_data_value_sans_prefix(match_data.values, :"expected after")[:to_s].should start_with("Not ") + end + end + + context "actual after" do + it "is the resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) + end + end + end + + describe "#message" do + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(label, 2) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeFromMatcher.new(label, 2) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + end + end +end diff --git a/spec/matchers/change_matcher_spec.cr b/spec/matchers/change_matcher_spec.cr index a101537..21788ec 100644 --- a/spec/matchers/change_matcher_spec.cr +++ b/spec/matchers/change_matcher_spec.cr @@ -3,6 +3,18 @@ require "../spec_helper" describe Spectator::Matchers::ChangeMatcher do describe "#match" do context "returned MatchData" do + context "with a static expression" do + describe "#matched?" do + it "is false" do + i = 0 + partial = new_block_partial { i += 0 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + end + context "with changing expression" do describe "#matched?" do it "is true" do @@ -78,4 +90,29 @@ describe Spectator::Matchers::ChangeMatcher do end end end + + describe "#from" do + it "returns a ChangeFromMatcher" do + i = 0 + matcher = Spectator::Matchers::ChangeMatcher.new { i } + matcher.from(0).should be_a(Spectator::Matchers::ChangeFromMatcher(Int32, Int32)) + end + + it "passes along the expression" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.from(0).match(partial) + i.should eq(5) # Local scope `i` will be updated if the expression (closure) was passed on. + end + + it "passes along the label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } + match_data = matcher.from(0).match(partial) + match_data.message.should contain(label) + end + end end From d2823398ff75ce81c040b84ef886a1a3733c9ac3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 19 Jul 2019 12:33:56 -0600 Subject: [PATCH 07/22] Remove useless assignment (Ameba find) --- spec/matchers/change_matcher_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/matchers/change_matcher_spec.cr b/spec/matchers/change_matcher_spec.cr index 21788ec..af7fea5 100644 --- a/spec/matchers/change_matcher_spec.cr +++ b/spec/matchers/change_matcher_spec.cr @@ -102,7 +102,7 @@ describe Spectator::Matchers::ChangeMatcher do i = 0 partial = new_block_partial { i += 5 } matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.from(0).match(partial) + matcher.from(0).match(partial) i.should eq(5) # Local scope `i` will be updated if the expression (closure) was passed on. end From 535dc6e9233430ad862386357c21df68f4fce66e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 19 Jul 2019 13:09:17 -0600 Subject: [PATCH 08/22] Add change.to variant --- spec/matchers/change_matcher_spec.cr | 41 +++++ spec/matchers/change_to_matcher_spec.cr | 191 ++++++++++++++++++++ src/spectator/matchers/change_matcher.cr | 8 +- src/spectator/matchers/change_to_matcher.cr | 102 +++++++++++ 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 spec/matchers/change_to_matcher_spec.cr create mode 100644 src/spectator/matchers/change_to_matcher.cr diff --git a/spec/matchers/change_matcher_spec.cr b/spec/matchers/change_matcher_spec.cr index af7fea5..d153b61 100644 --- a/spec/matchers/change_matcher_spec.cr +++ b/spec/matchers/change_matcher_spec.cr @@ -98,6 +98,14 @@ describe Spectator::Matchers::ChangeMatcher do matcher.from(0).should be_a(Spectator::Matchers::ChangeFromMatcher(Int32, Int32)) end + it "passes along the expected from value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.from(0).match(partial) + match_data_value_with_key(match_data.values, :"expected before").value.should eq(0) + end + it "passes along the expression" do i = 0 partial = new_block_partial { i += 5 } @@ -115,4 +123,37 @@ describe Spectator::Matchers::ChangeMatcher do match_data.message.should contain(label) end end + + describe "#to" do + it "returns a ChangeToMatcher" do + i = 0 + matcher = Spectator::Matchers::ChangeMatcher.new { i } + matcher.to(0).should be_a(Spectator::Matchers::ChangeToMatcher(Int32, Int32)) + end + + it "passes along the expected to value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + match_data = matcher.to(5).match(partial) + match_data_value_with_key(match_data.values, :"expected after").value.should eq(5) + end + + it "passes along the expression" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new { i } + matcher.to(5).match(partial) + i.should eq(5) # Local scope `i` will be updated if the expression (closure) was passed on. + end + + it "passes along the label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } + match_data = matcher.to(5).match(partial) + match_data.message.should contain(label) + end + end end diff --git a/spec/matchers/change_to_matcher_spec.cr b/spec/matchers/change_to_matcher_spec.cr new file mode 100644 index 0000000..34ab71f --- /dev/null +++ b/spec/matchers/change_to_matcher_spec.cr @@ -0,0 +1,191 @@ +require "../spec_helper" + +describe Spectator::Matchers::ChangeToMatcher do + describe "#match" do + context "returned MatchData" do + context "with a static expression" do + describe "#matched?" do + it "is false" do + i = 0 + partial = new_block_partial { i += 0 } + matcher = Spectator::Matchers::ChangeToMatcher.new(i) { i } + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + end + + context "with changing expression" do + describe "#matched?" do + it "is true" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(i + 5) { i } + match_data = matcher.match(partial) + match_data.matched?.should be_true + end + end + + describe "#values" do + context "expected before" do + it "is the negated resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } + match_data = matcher.match(partial) + match_data_value_sans_prefix(match_data.values, :"expected before")[:value].should eq(5) + match_data_value_sans_prefix(match_data.values, :"expected before")[:to_s].should start_with("Not ") + end + end + + context "actual before" do + it "is the initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) + end + end + + context "expected after" do + it "is the expected value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"expected after").value.should eq(5) + end + end + + context "actual after" do + it "is the resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) + end + end + end + + describe "#message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(i + 5) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(label, i + 5) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the action label" do + i = 0 + label = "ACTION" + partial = new_block_partial(label) { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(i + 5) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(label, i + 5) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + + context "with the wrong final value" do + describe "#matched?" do + it "is false" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data.matched?.should be_false + end + end + + describe "#values" do + context "expected before" do + it "is the negated resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_sans_prefix(match_data.values, :"expected before")[:value].should eq(2) + match_data_value_sans_prefix(match_data.values, :"expected before")[:to_s].should start_with("Not ") + end + end + + context "actual before" do + it "is the initial value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) + end + end + + context "expected after" do + it "is the expected value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"expected after").value.should eq(2) + end + end + + context "actual after" do + it "is the resulting value" do + i = 0 + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } + match_data = matcher.match(partial) + match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) + end + end + end + + describe "#message" do + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(label, 2) { i } + match_data = matcher.match(partial) + match_data.message.should contain(label) + end + end + + describe "#negated_message" do + it "contains the expression label" do + i = 0 + label = "EXPRESSION" + partial = new_block_partial { i += 5 } + matcher = Spectator::Matchers::ChangeToMatcher.new(label, 2) { i } + match_data = matcher.match(partial) + match_data.negated_message.should contain(label) + end + end + end + end + end +end diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 72aa1f0..0b56951 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -1,5 +1,6 @@ require "./change_from_matcher" -require "./value_matcher" +require "./change_to_matcher" +require "./matcher" module Spectator::Matchers # Matcher that tests whether an expression changed. @@ -35,6 +36,11 @@ module Spectator::Matchers ChangeFromMatcher.new(label, value, &@expression) end + # Specifies what the resulting value of the expression must be. + def to(value : T) forall T + ChangeToMatcher.new(label, value, &@expression) + end + # Match data specific to this matcher. private struct MatchData(ExpressionType) < MatchData # Creates the match data. diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr new file mode 100644 index 0000000..0a8d1aa --- /dev/null +++ b/src/spectator/matchers/change_to_matcher.cr @@ -0,0 +1,102 @@ +require "./value_matcher" + +module Spectator::Matchers + # Matcher that tests whether an expression changed to a specific value. + struct ChangeToMatcher(ExpressionType, ToType) < Matcher + # Textual representation of what the matcher expects. + # This shouldn't be used in the conditional logic, + # but for verbose output to help the end-user. + getter label : String + + # Determines whether the matcher is satisfied with the partial given to it. + # `MatchData` is returned that contains information about the match. + def match(partial) + before = @expression.call # Retrieve the expression's initial value. + partial.actual # Invoke action that might change the expression's value. + after = @expression.call # Retrieve the expression's value again. + if @expected_after != after + # Resulting value isn't what was expected. + ResultingMatchData.new(before, @expected_after, after, partial.label, label) + else + # Check if the expression's value changed. + same = before == after + ChangeMatchData.new(!same, before, @expected_after, after, partial.label, label) + end + end + + # Creates a new change matcher with a custom label. + def initialize(@label, @expected_after : ToType, &expression : -> ExpressionType) + @expression = expression + end + + # Creates a new change matcher. + def initialize(@expected_after : ToType, &expression : -> ExpressionType) + @label = expression.to_s + @expression = expression + end + + # Match data for when the resulting value isn't the expected value. + private struct ResultingMatchData(ExpressionType, ToType) < MatchData + # Creates the match data. + def initialize(@before : ExpressionType, @expected_after : ToType, @actual_after : ExpressionType, + @action_label : String, @expression_label : String) + super(false) + end + + # Do not allow negation of this match data. + def override? + true + end + + # Information about the match. + def named_tuple + { + "expected before": NegatableMatchDataValue.new(@expected_after, true), + "actual before": @before, + "expected after": @expected_after, + "actual after": @actual_after, + } + end + + # This is informational and displayed to the end-user. + def message + "#{@expression_label} changes to #{@expected_after}" + end + + # This is informational and displayed to the end-user. + def negated_message + "#{@expression_label} did not change to #{@expected_after}" + end + end + + private struct ChangeMatchData(ExpressionType, ToType) < MatchData + # Creates the match data. + def initialize(matched, @before : ToType, @expected_after : ToType, @actual_after : ExpressionType, + @action_label : String, @expression_label : String) + super(matched) + end + + # Information about the match. + def named_tuple + { + "expected before": NegatableMatchDataValue.new(@expected_after, true), + "actual before": @before, + "expected after": @expected_after, + "actual after": @actual_after, + } + end + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@action_label} changed #{@expression_label} to #{@expected_after}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@action_label} did not change #{@expression_label} to #{@expected_after}" + end + end + end +end From a56d6762c0625ebcf460d516f3ecfd3128949d6e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 11:38:13 -0600 Subject: [PATCH 09/22] Formatting --- src/spectator/matchers/change_matcher.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 0b56951..12bf2b7 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -45,7 +45,7 @@ module Spectator::Matchers private struct MatchData(ExpressionType) < MatchData # Creates the match data. def initialize(matched, @before : ExpressionType, @after : ExpressionType, - @action_label : String, @expression_label : String) + @action_label : String, @expression_label : String) super(matched) end @@ -53,7 +53,7 @@ module Spectator::Matchers def named_tuple { before: @before, - after: @after + after: @after, } end From e6599d5fe062fc4b6927c39ce0d2c9c685aff034 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 11:40:33 -0600 Subject: [PATCH 10/22] Remove specs for change matchers (for now) --- spec/matchers/change_from_matcher_spec.cr | 191 ---------------------- spec/matchers/change_matcher_spec.cr | 159 ------------------ spec/matchers/change_to_matcher_spec.cr | 191 ---------------------- 3 files changed, 541 deletions(-) delete mode 100644 spec/matchers/change_from_matcher_spec.cr delete mode 100644 spec/matchers/change_matcher_spec.cr delete mode 100644 spec/matchers/change_to_matcher_spec.cr diff --git a/spec/matchers/change_from_matcher_spec.cr b/spec/matchers/change_from_matcher_spec.cr deleted file mode 100644 index 521c117..0000000 --- a/spec/matchers/change_from_matcher_spec.cr +++ /dev/null @@ -1,191 +0,0 @@ -require "../spec_helper" - -describe Spectator::Matchers::ChangeFromMatcher do - describe "#match" do - context "returned MatchData" do - context "with a static expression" do - describe "#matched?" do - it "is false" do - i = 0 - partial = new_block_partial { i += 0 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } - match_data = matcher.match(partial) - match_data.matched?.should be_false - end - end - end - - context "with changing expression" do - describe "#matched?" do - it "is true" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } - match_data = matcher.match(partial) - match_data.matched?.should be_true - end - end - - describe "#values" do - context "expected before" do - it "is the expected value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"expected before").value.should eq(0) - end - end - - context "actual before" do - it "is the initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) - end - end - - context "expected after" do - it "is the negated initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } - match_data = matcher.match(partial) - match_data_value_sans_prefix(match_data.values, :"expected after")[:value].should eq(0) - match_data_value_sans_prefix(match_data.values, :"expected after")[:to_s].should start_with("Not ") - end - end - - context "actual after" do - it "is the resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(0) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) - end - end - end - - describe "#message" do - it "contains the action label" do - i = 0 - label = "ACTION" - partial = new_block_partial(label) { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(label, i) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - end - - describe "#negated_message" do - it "contains the action label" do - i = 0 - label = "ACTION" - partial = new_block_partial(label) { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(i) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(label, i) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - end - end - - context "with the wrong initial value" do - describe "#matched?" do - it "is false" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data.matched?.should be_false - end - end - - describe "#values" do - context "expected before" do - it "is the expected value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"expected before").value.should eq(2) - end - end - - context "actual before" do - it "is the initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) - end - end - - context "expected after" do - it "is the negated initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_sans_prefix(match_data.values, :"expected after")[:value].should eq(2) - match_data_value_sans_prefix(match_data.values, :"expected after")[:to_s].should start_with("Not ") - end - end - - context "actual after" do - it "is the resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) - end - end - end - - describe "#message" do - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(label, 2) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - end - - describe "#negated_message" do - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeFromMatcher.new(label, 2) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - end - end - end - end -end diff --git a/spec/matchers/change_matcher_spec.cr b/spec/matchers/change_matcher_spec.cr deleted file mode 100644 index d153b61..0000000 --- a/spec/matchers/change_matcher_spec.cr +++ /dev/null @@ -1,159 +0,0 @@ -require "../spec_helper" - -describe Spectator::Matchers::ChangeMatcher do - describe "#match" do - context "returned MatchData" do - context "with a static expression" do - describe "#matched?" do - it "is false" do - i = 0 - partial = new_block_partial { i += 0 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.match(partial) - match_data.matched?.should be_false - end - end - end - - context "with changing expression" do - describe "#matched?" do - it "is true" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.match(partial) - match_data.matched?.should be_true - end - end - - describe "#values" do - context "before" do - it "is the initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :before).value.should eq(0) - end - end - - context "after" do - it "is the resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :after).value.should eq(5) - end - end - end - - describe "#message" do - it "contains the action label" do - i = 0 - label = "ACTION" - partial = new_block_partial(label) { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - end - - describe "#negated_message" do - it "contains the action label" do - i = 0 - label = "ACTION" - partial = new_block_partial(label) { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - end - end - end - end - - describe "#from" do - it "returns a ChangeFromMatcher" do - i = 0 - matcher = Spectator::Matchers::ChangeMatcher.new { i } - matcher.from(0).should be_a(Spectator::Matchers::ChangeFromMatcher(Int32, Int32)) - end - - it "passes along the expected from value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.from(0).match(partial) - match_data_value_with_key(match_data.values, :"expected before").value.should eq(0) - end - - it "passes along the expression" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - matcher.from(0).match(partial) - i.should eq(5) # Local scope `i` will be updated if the expression (closure) was passed on. - end - - it "passes along the label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } - match_data = matcher.from(0).match(partial) - match_data.message.should contain(label) - end - end - - describe "#to" do - it "returns a ChangeToMatcher" do - i = 0 - matcher = Spectator::Matchers::ChangeMatcher.new { i } - matcher.to(0).should be_a(Spectator::Matchers::ChangeToMatcher(Int32, Int32)) - end - - it "passes along the expected to value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - match_data = matcher.to(5).match(partial) - match_data_value_with_key(match_data.values, :"expected after").value.should eq(5) - end - - it "passes along the expression" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new { i } - matcher.to(5).match(partial) - i.should eq(5) # Local scope `i` will be updated if the expression (closure) was passed on. - end - - it "passes along the label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeMatcher.new(label) { i } - match_data = matcher.to(5).match(partial) - match_data.message.should contain(label) - end - end -end diff --git a/spec/matchers/change_to_matcher_spec.cr b/spec/matchers/change_to_matcher_spec.cr deleted file mode 100644 index 34ab71f..0000000 --- a/spec/matchers/change_to_matcher_spec.cr +++ /dev/null @@ -1,191 +0,0 @@ -require "../spec_helper" - -describe Spectator::Matchers::ChangeToMatcher do - describe "#match" do - context "returned MatchData" do - context "with a static expression" do - describe "#matched?" do - it "is false" do - i = 0 - partial = new_block_partial { i += 0 } - matcher = Spectator::Matchers::ChangeToMatcher.new(i) { i } - match_data = matcher.match(partial) - match_data.matched?.should be_false - end - end - end - - context "with changing expression" do - describe "#matched?" do - it "is true" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(i + 5) { i } - match_data = matcher.match(partial) - match_data.matched?.should be_true - end - end - - describe "#values" do - context "expected before" do - it "is the negated resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } - match_data = matcher.match(partial) - match_data_value_sans_prefix(match_data.values, :"expected before")[:value].should eq(5) - match_data_value_sans_prefix(match_data.values, :"expected before")[:to_s].should start_with("Not ") - end - end - - context "actual before" do - it "is the initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) - end - end - - context "expected after" do - it "is the expected value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"expected after").value.should eq(5) - end - end - - context "actual after" do - it "is the resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(5) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) - end - end - end - - describe "#message" do - it "contains the action label" do - i = 0 - label = "ACTION" - partial = new_block_partial(label) { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(i + 5) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(label, i + 5) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - end - - describe "#negated_message" do - it "contains the action label" do - i = 0 - label = "ACTION" - partial = new_block_partial(label) { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(i + 5) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(label, i + 5) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - end - end - - context "with the wrong final value" do - describe "#matched?" do - it "is false" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data.matched?.should be_false - end - end - - describe "#values" do - context "expected before" do - it "is the negated resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_sans_prefix(match_data.values, :"expected before")[:value].should eq(2) - match_data_value_sans_prefix(match_data.values, :"expected before")[:to_s].should start_with("Not ") - end - end - - context "actual before" do - it "is the initial value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual before").value.should eq(0) - end - end - - context "expected after" do - it "is the expected value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"expected after").value.should eq(2) - end - end - - context "actual after" do - it "is the resulting value" do - i = 0 - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(2) { i } - match_data = matcher.match(partial) - match_data_value_with_key(match_data.values, :"actual after").value.should eq(5) - end - end - end - - describe "#message" do - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(label, 2) { i } - match_data = matcher.match(partial) - match_data.message.should contain(label) - end - end - - describe "#negated_message" do - it "contains the expression label" do - i = 0 - label = "EXPRESSION" - partial = new_block_partial { i += 5 } - matcher = Spectator::Matchers::ChangeToMatcher.new(label, 2) { i } - match_data = matcher.match(partial) - match_data.negated_message.should contain(label) - end - end - end - end - end -end From db1118dac1ce995a323e77ab462db4e68de556d8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 12:26:00 -0600 Subject: [PATCH 11/22] Fix spec after rebase --- spec/expectations/expectation_spec.cr | 106 -------------------------- 1 file changed, 106 deletions(-) diff --git a/spec/expectations/expectation_spec.cr b/spec/expectations/expectation_spec.cr index eaac487..06540fc 100644 --- a/spec/expectations/expectation_spec.cr +++ b/spec/expectations/expectation_spec.cr @@ -48,110 +48,4 @@ describe Spectator::Expectations::Expectation do end end end - - describe "#values" do - it "is the same as the match data values" do - value = 42 - match_data = new_matcher(value).match(new_partial(value)) - expectation = Spectator::Expectations::Expectation.new(match_data, false) - expectation_values = expectation.values - match_data.values.zip(expectation_values).each do |m, e| - m.label.should eq(e.label) - m.value.value.should eq(e.value.value) - end - end - - context "when negated" do - it "negates all negatable values" do - value = 42 - match_data = new_matcher(value).match(new_partial(value)) - expectation = Spectator::Expectations::Expectation.new(match_data, true) - expectation.values.each do |labeled_value| - label = labeled_value.label - value = labeled_value.value - value.to_s.should start_with(/not/i) if label == :expected - end - end - end - end - - describe "#actual_message" do - context "with a successful match" do - it "equals the matcher's #message" do - value = 42 - match_data = new_matcher(value).match(new_partial(value)) - match_data.matched?.should be_true # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, false) - expectation.actual_message.should eq(match_data.message) - end - - context "when negated" do - it "equals the matcher's #message" do - value = 42 - match_data = new_matcher(value).match(new_partial(value)) - match_data.matched?.should be_true # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, true) - expectation.actual_message.should eq(match_data.message) - end - end - end - - context "with an unsuccessful match" do - it "equals the matcher's #negated_message" do - match_data = new_matcher(42).match(new_partial(777)) - match_data.matched?.should be_false # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, false) - expectation.actual_message.should eq(match_data.negated_message) - end - - context "when negated" do - it "equals the matcher's #negated_message" do - match_data = new_matcher(42).match(new_partial(777)) - match_data.matched?.should be_false # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, true) - expectation.actual_message.should eq(match_data.negated_message) - end - end - end - end - - describe "#expected_message" do - context "with a successful match" do - it "equals the matcher's #message" do - value = 42 - match_data = new_matcher(value).match(new_partial(value)) - match_data.matched?.should be_true # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, false) - expectation.expected_message.should eq(match_data.message) - end - - context "when negated" do - it "equals the matcher's #negated_message" do - value = 42 - match_data = new_matcher(value).match(new_partial(value)) - match_data.matched?.should be_true # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, true) - expectation.expected_message.should eq(match_data.negated_message) - end - end - end - - context "with an unsuccessful match" do - it "equals the matcher's #message" do - match_data = new_matcher(42).match(new_partial(777)) - match_data.matched?.should be_false # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, false) - expectation.expected_message.should eq(match_data.message) - end - - context "when negated" do - it "equals the matcher's #negated_message" do - match_data = new_matcher(42).match(new_partial(777)) - match_data.matched?.should be_false # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, true) - expectation.expected_message.should eq(match_data.negated_message) - end - end - end - end end From 214b2e171e346e1b72e939e39cdc7e9886386fe8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 12:42:57 -0600 Subject: [PATCH 12/22] Refactor existing change matchers to use new format --- src/spectator/dsl/matcher_dsl.cr | 3 +- src/spectator/matchers/change_from_matcher.cr | 136 +++++++----------- src/spectator/matchers/change_matcher.cr | 96 ++++++------- src/spectator/matchers/change_to_matcher.cr | 134 +++++++---------- 4 files changed, 153 insertions(+), 216 deletions(-) diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 77c54dd..fb9eae6 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -535,7 +535,8 @@ module Spectator::DSL # expect { i += 42 }.to change { i }.by(42) # ``` macro change(&expression) - ::Spectator::Matchers::ChangeMatcher.new("`" + {{expression.body.stringify}} + "`") {{expression}} + %proc = ->({{expression.args.splat}}) {{expression}} + ::Spectator::Matchers::ChangeMatcher.new(::Spectator::TestBlock.new(%proc, "`" + {{expression.body.stringify}} + "`")) end # Indicates that some block should raise an error. diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index b41fdf3..9a937c6 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -3,100 +3,74 @@ require "./value_matcher" module Spectator::Matchers # Matcher that tests whether an expression changed from a specific value. struct ChangeFromMatcher(ExpressionType, FromType) < Matcher - # Textual representation of what the matcher expects. - # This shouldn't be used in the conditional logic, - # but for verbose output to help the end-user. - getter label : String + # The expression that is expected to (not) change. + private getter expression - # Determines whether the matcher is satisfied with the partial given to it. - # `MatchData` is returned that contains information about the match. - def match(partial) - before = @expression.call # Retrieve the expression's initial value. - partial.actual # Invoke action that might change the expression's value. - after = @expression.call # Retrieve the expression's value again. - if @expected_before != before - # Initial value isn't what was expected. - InitialMatchData.new(@expected_before, before, after, partial.label, label) - else - # Check if the expression's value changed. - same = before == after - ChangeMatchData.new(!same, @expected_before, before, after, partial.label, label) - end - end - - # Creates a new change matcher with a custom label. - def initialize(@label, @expected_before : FromType, &expression : -> ExpressionType) - @expression = expression - end + # The expected value of the expression before the change. + private getter expected # Creates a new change matcher. - def initialize(@expected_before : FromType, &expression : -> ExpressionType) - @label = expression.to_s - @expression = expression + def initialize(@expression : TestBlock(ExpressionType), @expected : FromType) end - # Match data for when the initial value isn't the expected value. - private struct InitialMatchData(ExpressionType, FromType) < MatchData - # Creates the match data. - def initialize(@expected_before : FromType, @actual_before : ExpressionType, @after : ExpressionType, - @action_label : String, @expression_label : String) - super(false) - end + # 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 + "changes #{expression.label} from #{expected}" + end - # Do not allow negation of this match data. - def override? - true - end - - # Information about the match. - def named_tuple - { - "expected before": @expected_before, - "actual before": @actual_before, - "expected after": NegatableMatchDataValue.new(@expected_before, true), - "actual after": @after, - } - end - - # This is informational and displayed to the end-user. - def message - "#{@expression_label} is initially #{@expected_before}" - end - - # This is informational and displayed to the end-user. - def negated_message - "#{@expression_label} is not initially #{@expected_before}" + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before != expected + FailedMatchData.new("#{expression.label} was not initially #{expected}", + expected: expected.inspect, + actual: before.inspect, + ) + elsif before == after + FailedMatchData.new("#{actual.label} did not change #{expression.label} from #{expected}", + before: before.inspect, + after: after.inspect, + expected: "Not #{expected.inspect}" + ) + else + SuccessfulMatchData.new end end - private struct ChangeMatchData(ExpressionType, FromType) < MatchData - # Creates the match data. - def initialize(matched, @expected_before : FromType, @actual_before : ExpressionType, - @after : ExpressionType, @action_label : String, @expression_label : String) - super(matched) + # Performs the test against the expression, but inverted. + # A successful match with `#match` should normally fail for this method, and vice-versa. + def negated_match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before != expected + FailedMatchData.new("#{expression.label} was not initially #{expected}", + expected: expected.inspect, + actual: before.inspect + ) + elsif before == after + SuccessfulMatchData.new + else + FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected}", + before: before.inspect, + after: after.inspect, + expected: expected.inspect + ) end + end - # Information about the match. - def named_tuple - { - "expected before": @expected_before, - "actual before": @actual_before, - "expected after": NegatableMatchDataValue.new(@expected_before, true), - "actual after": @after, - } - end + # Specifies what the resulting value of the expression must be. + def to(value : T) forall T + raise NotImplementedError.new("ChangeFromMatcher#to") + end - # Describes the condition that satisfies the matcher. - # This is informational and displayed to the end-user. - def message - "#{@action_label} changed #{@expression_label} from #{@expected_before}" - end + # Performs the change and reports the before and after values. + private def change(actual) + before = expression.value # Retrieve the expression's initial value. + actual.value # Invoke action that might change the expression's value. + after = expression.value # Retrieve the expression's value again. - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message - "#{@action_label} did not change #{@expression_label} from #{@expected_before}" - end + {before, after} end end end diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 12bf2b7..e4c24fa 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -1,73 +1,67 @@ require "./change_from_matcher" require "./change_to_matcher" -require "./matcher" +require "./standard_matcher" module Spectator::Matchers # Matcher that tests whether an expression changed. struct ChangeMatcher(ExpressionType) < Matcher - # Textual representation of what the matcher expects. - # This shouldn't be used in the conditional logic, - # but for verbose output to help the end-user. - getter label : String - - # Determines whether the matcher is satisfied with the partial given to it. - # `MatchData` is returned that contains information about the match. - def match(partial) - before = @expression.call # Retrieve the expression's initial value. - partial.actual # Invoke action that might change the expression's value. - after = @expression.call # Retrieve the expression's value again. - same = before == after # Did the value change? - MatchData.new(!same, before, after, partial.label, label) - end - - # Creates a new change matcher with a custom label. - def initialize(@label, &expression : -> ExpressionType) - @expression = expression - end + private getter expression # Creates a new change matcher. - def initialize(&expression : -> ExpressionType) - @label = expression.to_s - @expression = expression + def initialize(@expression : TestBlock(ExpressionType)) + end + + # 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 + "changes #{expression.label}" + end + + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before == after + FailedMatchData.new("#{actual.label} did not change #{expression.label}", + before: before.inspect, + after: after.inspect + ) + else + SuccessfulMatchData.new + end + end + + # Performs the test against the expression, but inverted. + # A successful match with `#match` should normally fail for this method, and vice-versa. + def negated_match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before == after + SuccessfulMatchData.new + else + FailedMatchData.new("#{actual.label} changed #{expression.label}", + before: before.inspect, + after: after.inspect + ) + end end # Specifies what the initial value of the expression must be. def from(value : T) forall T - ChangeFromMatcher.new(label, value, &@expression) + ChangeFromMatcher.new(@expression, value) end # Specifies what the resulting value of the expression must be. def to(value : T) forall T - ChangeToMatcher.new(label, value, &@expression) + ChangeToMatcher.new(@expression, value) end - # Match data specific to this matcher. - private struct MatchData(ExpressionType) < MatchData - # Creates the match data. - def initialize(matched, @before : ExpressionType, @after : ExpressionType, - @action_label : String, @expression_label : String) - super(matched) - end + # Performs the change and reports the before and after values. + private def change(actual) + before = expression.value # Retrieve the expression's initial value. + actual.value # Invoke action that might change the expression's value. + after = expression.value # Retrieve the expression's value again. - # Information about the match. - def named_tuple - { - before: @before, - after: @after, - } - end - - # Describes the condition that satisfies the matcher. - # This is informational and displayed to the end-user. - def message - "#{@action_label} changes #{@expression_label}" - end - - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message - "#{@action_label} does not change #{@expression_label}" - end + {before, after} end end end diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index 0a8d1aa..9b53c6d 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -1,102 +1,70 @@ -require "./value_matcher" +require "./change_matcher" module Spectator::Matchers # Matcher that tests whether an expression changed to a specific value. struct ChangeToMatcher(ExpressionType, ToType) < Matcher - # Textual representation of what the matcher expects. - # This shouldn't be used in the conditional logic, - # but for verbose output to help the end-user. - getter label : String + # The expression that is expected to (not) change. + private getter expression - # Determines whether the matcher is satisfied with the partial given to it. - # `MatchData` is returned that contains information about the match. - def match(partial) - before = @expression.call # Retrieve the expression's initial value. - partial.actual # Invoke action that might change the expression's value. - after = @expression.call # Retrieve the expression's value again. - if @expected_after != after - # Resulting value isn't what was expected. - ResultingMatchData.new(before, @expected_after, after, partial.label, label) - else - # Check if the expression's value changed. - same = before == after - ChangeMatchData.new(!same, before, @expected_after, after, partial.label, label) - end - end - - # Creates a new change matcher with a custom label. - def initialize(@label, @expected_after : ToType, &expression : -> ExpressionType) - @expression = expression - end + # The expected value of the expression after the change. + private getter expected # Creates a new change matcher. - def initialize(@expected_after : ToType, &expression : -> ExpressionType) - @label = expression.to_s - @expression = expression + def initialize(@expression : TestBlock(ExpressionType), @expected : ToType) end - # Match data for when the resulting value isn't the expected value. - private struct ResultingMatchData(ExpressionType, ToType) < MatchData - # Creates the match data. - def initialize(@before : ExpressionType, @expected_after : ToType, @actual_after : ExpressionType, - @action_label : String, @expression_label : String) - super(false) - end + # 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 + "changes #{expression.label} to #{expected}" + end - # Do not allow negation of this match data. - def override? - true - end - - # Information about the match. - def named_tuple - { - "expected before": NegatableMatchDataValue.new(@expected_after, true), - "actual before": @before, - "expected after": @expected_after, - "actual after": @actual_after, - } - end - - # This is informational and displayed to the end-user. - def message - "#{@expression_label} changes to #{@expected_after}" - end - - # This is informational and displayed to the end-user. - def negated_message - "#{@expression_label} did not change to #{@expected_after}" + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before == after + FailedMatchData.new("#{actual.label} did not change #{expression.label}", + before: before.inspect, + after: after.inspect, + expected: expected.inspect + ) + elsif expected == after + SuccessfulMatchData.new + else + FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected}", + before: before.inspect, + after: after.inspect, + expected: expected.inspect + ) end end - private struct ChangeMatchData(ExpressionType, ToType) < MatchData - # Creates the match data. - def initialize(matched, @before : ToType, @expected_after : ToType, @actual_after : ExpressionType, - @action_label : String, @expression_label : String) - super(matched) - end + # Negated matching for this matcher is not supported. + # Attempting to call this method will result in a compilation error. + # + # This syntax has a logical problem. + # "The action does not change the expression to some value." + # Is it a failure if the value is not changed, + # but it is the expected value? + # + # RSpec doesn't support this syntax either. + def negated_match(actual : TestExpression(T)) : MatchData forall T + {% raise "The `expect { }.to_not change { }.to()` syntax is not supported (ambiguous)." %} + end - # Information about the match. - def named_tuple - { - "expected before": NegatableMatchDataValue.new(@expected_after, true), - "actual before": @before, - "expected after": @expected_after, - "actual after": @actual_after, - } - end + # Specifies what the initial value of the expression must be. + def from(value : T) forall T + raise NotImplementedError.new("ChangeToMatcher#from") + end - # Describes the condition that satisfies the matcher. - # This is informational and displayed to the end-user. - def message - "#{@action_label} changed #{@expression_label} to #{@expected_after}" - end + # Performs the change and reports the before and after values. + private def change(actual) + before = expression.value # Retrieve the expression's initial value. + actual.value # Invoke action that might change the expression's value. + after = expression.value # Retrieve the expression's value again. - # Describes the condition that won't satsify the matcher. - # This is informational and displayed to the end-user. - def negated_message - "#{@action_label} did not change #{@expression_label} to #{@expected_after}" - end + {before, after} end end end From 1d205b6ee98ffbd95614e3e5a819fe0f529f5f8f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 12:49:54 -0600 Subject: [PATCH 13/22] Put test expression on its own line --- src/spectator/dsl/matcher_dsl.cr | 74 +++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index fb9eae6..3b43edb 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -14,7 +14,8 @@ module Spectator::DSL # expect(1 + 2).to eq(3) # ``` macro eq(expected) - ::Spectator::Matchers::EqualityMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::EqualityMatcher.new(%test_value) end # Indicates that some value should not equal another. @@ -26,7 +27,8 @@ module Spectator::DSL # expect(1 + 2).to ne(5) # ``` macro ne(expected) - ::Spectator::Matchers::InequalityMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::InequalityMatcher.new(%test_value) end # Indicates that some value when compared to another satisfies an operator. @@ -60,7 +62,8 @@ module Spectator::DSL # expect(obj.dup).to_not be(obj) # ``` macro be(expected) - ::Spectator::Matchers::ReferenceMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::ReferenceMatcher.new(%test_value) end # Indicates that some value should be of a specified type. @@ -117,7 +120,8 @@ module Spectator::DSL # expect(3 - 1).to be_lt(3) # ``` macro be_lt(expected) - ::Spectator::Matchers::LessThanMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::LessThanMatcher.new(%test_value) end # Indicates that some value should be less than or equal to another. @@ -129,7 +133,8 @@ module Spectator::DSL # expect(3 - 1).to be_le(3) # ``` macro be_le(expected) - ::Spectator::Matchers::LessThanEqualMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::LessThanEqualMatcher.new(%test_value) end # Indicates that some value should be greater than another. @@ -141,7 +146,8 @@ module Spectator::DSL # expect(3 + 1).to be_gt(3) # ``` macro be_gt(expected) - ::Spectator::Matchers::GreaterThanMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::GreaterThanMatcher.new(%test_value) end # Indicates that some value should be greater than or equal to another. @@ -153,7 +159,8 @@ module Spectator::DSL # expect(3 + 1).to be_ge(3) # ``` macro be_ge(expected) - ::Spectator::Matchers::GreaterThanEqualMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::GreaterThanEqualMatcher.new(%test_value) end # Indicates that some value should match another. @@ -170,7 +177,8 @@ module Spectator::DSL # expect({:foo, 5}).to match({Symbol, Int32}) # ``` macro match(expected) - ::Spectator::Matchers::CaseMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::CaseMatcher.new(%test_value) end # Indicates that some value should be true. @@ -260,7 +268,8 @@ module Spectator::DSL # NOTE: Do not attempt to mix the two use cases. # It likely won't work and will result in a compilation error. macro be_within(expected) - ::Spectator::Matchers::CollectionMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::CollectionMatcher.new(%test_value) end # Indicates that some value should be between a lower and upper-bound. @@ -280,10 +289,10 @@ module Spectator::DSL # expect(100).to be_between(97, 101).exclusive # 97, 98, 99, or 100 (not 101) # ``` macro be_between(min, max) - :Spectator::Matchers::RangeMatcher.new( - ::Spectator::TestValue.new(Range.new({{min}}, {{max}})), - [{{min.stringify}}, {{max.stringify}}].join(" to ") - ) + %range = Range.new({{min}}, {{max}})) + %label = [{{min.stringify}}, {{max.stringify}}].join(" to ") + %test_value = ::Spectator::TestValue.new(%range, %label) + :Spectator::Matchers::RangeMatcher.new(%test_value) end # Indicates that some value should be within a delta of an expected value. @@ -341,7 +350,8 @@ module Spectator::DSL # expect(%w[foo bar]).to start_with(/foo/) # ``` macro start_with(expected) - ::Spectator::Matchers::StartWithMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::StartWithMatcher.new(%test_value) end # Indicates that some value or set should end with another value. @@ -363,7 +373,8 @@ module Spectator::DSL # expect(%w[foo bar]).to end_with(/bar/) # ``` macro end_with(expected) - ::Spectator::Matchers::EndWithMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::EndWithMatcher.new(%test_value) end # Indicates that some value or set should contain another value. @@ -386,7 +397,8 @@ module Spectator::DSL # expect(%i[a b c]).to contain(:a, :b) # ``` macro contain(*expected) - ::Spectator::Matchers::ContainMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%test_value) end # Indicates that some value or set should contain another value. @@ -415,7 +427,8 @@ module Spectator::DSL # expect(%w[FOO BAR BAZ]).to have(/foo/i, String) # ``` macro have(*expected) - ::Spectator::Matchers::HaveMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::HaveMatcher.new(%test_value) end # Indicates that some set, such as a `Hash`, has a given key. @@ -427,7 +440,8 @@ module Spectator::DSL # expect({"lucky" => 7}).to have_key("lucky") # ``` macro have_key(expected) - ::Spectator::Matchers::HaveKeyMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::HaveKeyMatcher.new(%test_value) end # ditto @@ -444,7 +458,8 @@ module Spectator::DSL # expect({"lucky" => 7}).to have_value(7) # ``` macro have_value(expected) - ::Spectator::Matchers::HaveValueMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::HaveValueMatcher.new(%test_value) end # ditto @@ -459,7 +474,8 @@ module Spectator::DSL # expect([1, 2, 3]).to contain_exactly(1, 2, 3) # ``` macro contain_exactly(*expected) - ::Spectator::Matchers::ArrayMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::ArrayMatcher.new(%test_value) end # Indicates that some set should contain the same values in exact order as another set. @@ -469,7 +485,8 @@ module Spectator::DSL # expect([1, 2, 3]).to match_array([1, 2, 3]) # ``` macro match_array(expected) - ::Spectator::Matchers::ArrayMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::ArrayMatcher.new(%test_value) end # Indicates that some set should have a specified size. @@ -479,7 +496,8 @@ module Spectator::DSL # expect([1, 2, 3]).to have_size(3) # ``` macro have_size(expected) - ::Spectator::Matchers::SizeMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::SizeMatcher.new(%test_value) end # Indicates that some set should have the same size (number of elements) as another set. @@ -489,7 +507,8 @@ module Spectator::DSL # expect([1, 2, 3]).to have_size_of(%i[x y z]) # ``` macro have_size_of(expected) - ::Spectator::Matchers::SizeOfMatcher.new(::Spectator::TestValue.new(({{expected}}), {{expected.stringify}})) + %test_value = ::Spectator::TestValue.new(({{expected}}), {{expected.stringify}}) + ::Spectator::Matchers::SizeOfMatcher.new(%test_value) end # Indicates that some value should have a set of attributes matching some conditions. @@ -503,7 +522,8 @@ module Spectator::DSL # expect(%i[a b c]).to have_attributes(size: 1..5, first: Symbol) # ``` macro have_attributes(**expected) - ::Spectator::Matchers::AttributesMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}})) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}}) + ::Spectator::Matchers::AttributesMatcher.new(%test_value) end # Indicates that some expression's value should change after taking an action. @@ -536,7 +556,8 @@ module Spectator::DSL # ``` macro change(&expression) %proc = ->({{expression.args.splat}}) {{expression}} - ::Spectator::Matchers::ChangeMatcher.new(::Spectator::TestBlock.new(%proc, "`" + {{expression.body.stringify}} + "`")) + %test_block = ::Spectator::TestBlock.new(%proc, "`" + {{expression.body.stringify}} + "`") + ::Spectator::Matchers::ChangeMatcher.new(%test_block) end # Indicates that some block should raise an error. @@ -655,7 +676,8 @@ module Spectator::DSL {% end %} label << ')' {% end %} - ::Spectator::Matchers::{{matcher.id}}.new(::Spectator::TestValue.new(descriptor, label.to_s)) + test_value = ::Spectator::TestValue.new(descriptor, label.to_s) + ::Spectator::Matchers::{{matcher.id}}.new(test_value) end end end From 3c539b3517b6b6ee910edcfbd714e98f475f09aa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 12:54:46 -0600 Subject: [PATCH 14/22] Fix require statements --- src/spectator/matchers/change_from_matcher.cr | 4 +++- src/spectator/matchers/change_matcher.cr | 4 +++- src/spectator/matchers/change_to_matcher.cr | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index 9a937c6..008afaf 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -1,4 +1,6 @@ -require "./value_matcher" +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" module Spectator::Matchers # Matcher that tests whether an expression changed from a specific value. diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index e4c24fa..d30469c 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -1,6 +1,8 @@ require "./change_from_matcher" require "./change_to_matcher" -require "./standard_matcher" +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" module Spectator::Matchers # Matcher that tests whether an expression changed. diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index 9b53c6d..19ad365 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -1,4 +1,6 @@ -require "./change_matcher" +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" module Spectator::Matchers # Matcher that tests whether an expression changed to a specific value. From e1a119639f081e2503babb48decd657c623eb86f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 12:39:33 -0600 Subject: [PATCH 15/22] Add change exact matcher --- .../matchers/change_exact_matcher.cr | 84 +++++++++++++++++++ src/spectator/matchers/change_from_matcher.cr | 3 +- src/spectator/matchers/change_to_matcher.cr | 3 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/spectator/matchers/change_exact_matcher.cr diff --git a/src/spectator/matchers/change_exact_matcher.cr b/src/spectator/matchers/change_exact_matcher.cr new file mode 100644 index 0000000..48dcf9f --- /dev/null +++ b/src/spectator/matchers/change_exact_matcher.cr @@ -0,0 +1,84 @@ +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" + +module Spectator::Matchers + # Matcher that tests whether an expression changed from and to specific values. + struct ChangeExactMatcher(ExpressionType, FromType, ToType) < Matcher + # The expression that is expected to (not) change. + private getter expression + + # The expected value of the expression before the change. + private getter expected_before + + # The expected value of the expression after the change. + private getter expected_after + + # Creates a new change matcher. + def initialize(@expression : TestBlock(ExpressionType), @expected_before : FromType, @expected_after : ToType) + end + + # 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 + "changes #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}" + end + + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if expected_before == before + if before == after + FailedMatchData.new("#{actual.label} did not change #{expression.label}", + before: before.inspect, + after: after.inspect + ) + elsif expected_after == after + SuccessfulMatchData.new + else + FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", + before: before.inspect, + after: after.inspect, + expected: expected_after.inspect + ) + end + else + FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}", + expected: expected_before.inspect, + actual: before.inspect, + ) + end + end + + # Performs the test against the expression, but inverted. + # A successful match with `#match` should normally fail for this method, and vice-versa. + def negated_match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if expected_before == before + if expected_after == after + FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", + before: before.inspect, + after: after.inspect + ) + else + SuccessfulMatchData.new + end + else + FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}", + expected: expected_before.inspect, + actual: before.inspect, + ) + end + end + + # Performs the change and reports the before and after values. + private def change(actual) + before = expression.value # Retrieve the expression's initial value. + actual.value # Invoke action that might change the expression's value. + after = expression.value # Retrieve the expression's value again. + + {before, after} + end + end +end diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index 008afaf..de60630 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -1,3 +1,4 @@ +require "./change_exact_matcher" require "./failed_match_data" require "./matcher" require "./successful_match_data" @@ -63,7 +64,7 @@ module Spectator::Matchers # Specifies what the resulting value of the expression must be. def to(value : T) forall T - raise NotImplementedError.new("ChangeFromMatcher#to") + ChangeExactMatcher.new(@expression, @expected, value) end # Performs the change and reports the before and after values. diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index 19ad365..e95c1d9 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -1,3 +1,4 @@ +require "./change_exact_matcher" require "./failed_match_data" require "./matcher" require "./successful_match_data" @@ -57,7 +58,7 @@ module Spectator::Matchers # Specifies what the initial value of the expression must be. def from(value : T) forall T - raise NotImplementedError.new("ChangeToMatcher#from") + ChangeExactMatcher.new(@expression, value, @expected) end # Performs the change and reports the before and after values. From 5dfcad3cb44f0325deb76e4b9f894787722f9a5e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 12:40:27 -0600 Subject: [PATCH 16/22] Flip operands of != in case expected has custom logic --- src/spectator/matchers/change_from_matcher.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index de60630..75e81db 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -26,7 +26,7 @@ module Spectator::Matchers # Actually performs the test against the expression. def match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) - if before != expected + if expected != before FailedMatchData.new("#{expression.label} was not initially #{expected}", expected: expected.inspect, actual: before.inspect, @@ -46,7 +46,7 @@ module Spectator::Matchers # A successful match with `#match` should normally fail for this method, and vice-versa. def negated_match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) - if before != expected + if expected != before FailedMatchData.new("#{expression.label} was not initially #{expected}", expected: expected.inspect, actual: before.inspect From 27436b4e09ae66995c291757f3ae3d973ba10845 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 12:55:38 -0600 Subject: [PATCH 17/22] Need to use fresh variables in this macro --- src/spectator/dsl/example_dsl.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/example_dsl.cr b/src/spectator/dsl/example_dsl.cr index dfc3b2b..da90e2e 100644 --- a/src/spectator/dsl/example_dsl.cr +++ b/src/spectator/dsl/example_dsl.cr @@ -74,15 +74,15 @@ module Spectator::DSL # The raw block can't be used because it's not clear to the user. {% method_name = block.body.id.split('.')[1..-1].join('.') %} %partial = %proc.partial(subject) - test_block = ::Spectator::TestBlock.create(%partial, {{"#" + method_name}}) + %test_block = ::Spectator::TestBlock.create(%partial, {{"#" + method_name}}) {% else %} # In this case, it looks like the short-hand method syntax wasn't used. # Just drop in the proc as-is. - test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}}) + %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}}) {% end %} - source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Expectations::ExpectationPartial.new(test_block, source) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::Expectations::ExpectationPartial.new(%test_block, %source) end # Starts an expectation. From 2666f806531ac3407b72c1dfd5068a82d77a3c05 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 13:16:18 -0600 Subject: [PATCH 18/22] Hackish fix for block short-hand syntax Dunno when this broke or how it ever worked. :neutral_face: --- src/spectator/dsl/example_dsl.cr | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spectator/dsl/example_dsl.cr b/src/spectator/dsl/example_dsl.cr index da90e2e..5fea5a7 100644 --- a/src/spectator/dsl/example_dsl.cr +++ b/src/spectator/dsl/example_dsl.cr @@ -55,9 +55,6 @@ module Spectator::DSL {% raise "Argument or block must be provided to expect" %} {% end %} - # Create a proc to capture the block. - %proc = ->({{block.args.splat}}) {{block}} - # Check if the short-hand method syntax is used. # This is a hack, since macros don't get this as a "literal" or something similar. # The Crystal compiler will translate: @@ -73,12 +70,15 @@ module Spectator::DSL # Extract the method name to make it clear to the user what is tested. # The raw block can't be used because it's not clear to the user. {% method_name = block.body.id.split('.')[1..-1].join('.') %} - %partial = %proc.partial(subject) - %test_block = ::Spectator::TestBlock.create(%partial, {{"#" + method_name}}) - {% else %} + %proc = ->{ subject.{{method_name.id}} } + %test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) + {% elsif block.args.empty? %} # In this case, it looks like the short-hand method syntax wasn't used. - # Just drop in the proc as-is. + # Capture the block as a proc and pass along. + %proc = ->{{block}} %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}}) + {% else %} + {% raise "Unexpected block arguments in expect call" %} {% end %} %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) From 848f80ddf894d1f8a9634d648771be9a062d9e0b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 13:16:40 -0600 Subject: [PATCH 19/22] Add block short-hand syntax support to change matcher --- src/spectator/dsl/matcher_dsl.cr | 39 ++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 3b43edb..043ece4 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -554,9 +554,44 @@ module Spectator::DSL # i = 0 # expect { i += 42 }.to change { i }.by(42) # ``` + # + # The block short-hand syntax can be used here. + # It will reference the current subject. + # + # ``` + # expect { subject << :foo }.to change(&.size).by(1) + # ``` macro change(&expression) - %proc = ->({{expression.args.splat}}) {{expression}} - %test_block = ::Spectator::TestBlock.new(%proc, "`" + {{expression.body.stringify}} + "`") + {% if expression.is_a?(Nop) %} + {% raise "Block must be provided to change matcher" %} + {% end %} + + # Check if the short-hand method syntax is used. + # This is a hack, since macros don't get this as a "literal" or something similar. + # The Crystal compiler will translate: + # ``` + # &.foo + # ``` + # to: + # ``` + # { |__arg0| __arg0.foo } + # ``` + # The hack used here is to check if it looks like a compiler-generated block. + {% if expression.args == ["__arg0".id] && expression.body.is_a?(Call) && expression.body.id =~ /^__arg0\./ %} + # Extract the method name to make it clear to the user what is tested. + # The raw block can't be used because it's not clear to the user. + {% method_name = expression.body.id.split('.')[1..-1].join('.') %} + %proc = ->{ subject.{{method_name.id}} } + %test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) + {% elsif expression.args.empty? %} + # In this case, it looks like the short-hand method syntax wasn't used. + # Capture the block as a proc and pass along. + %proc = ->{{expression}} + %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + expression.body.stringify + "`"}}) + {% else %} + {% raise "Unexpected block arguments in change matcher" %} + {% end %} + ::Spectator::Matchers::ChangeMatcher.new(%test_block) end From c19f442e6cc6f455e310778408ac2699152bb177 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 13:52:22 -0600 Subject: [PATCH 20/22] Add #by modifier methods --- src/spectator/matchers/change_from_matcher.cr | 5 +++++ src/spectator/matchers/change_to_matcher.cr | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index 75e81db..82a9267 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -67,6 +67,11 @@ module Spectator::Matchers ChangeExactMatcher.new(@expression, @expected, value) end + # Specifies what the resulting value of the expression should change by. + def by(amount : T) forall T + ChangeExactMatcher.new(@expression, @expected, @expected + value) + end + # Performs the change and reports the before and after values. private def change(actual) before = expression.value # Retrieve the expression's initial value. diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index e95c1d9..65f1ce5 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -61,6 +61,11 @@ module Spectator::Matchers ChangeExactMatcher.new(@expression, value, @expected) end + # Specifies how much the initial value should change by. + def by(amount : T) forall T + ChangeExactMatcher.new(@expression, @expected - amount, @expected) + end + # Performs the change and reports the before and after values. private def change(actual) before = expression.value # Retrieve the expression's initial value. From 4e15487a0f50952e10f6e1459824513921df3937 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 23:08:45 -0600 Subject: [PATCH 21/22] Add relative change matcher --- src/spectator/matchers/change_matcher.cr | 16 ++++++ .../matchers/change_relative_matcher.cr | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/spectator/matchers/change_relative_matcher.cr diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index d30469c..ad8ca89 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -7,6 +7,7 @@ require "./successful_match_data" module Spectator::Matchers # Matcher that tests whether an expression changed. struct ChangeMatcher(ExpressionType) < Matcher + # The expression that is expected to (not) change. private getter expression # Creates a new change matcher. @@ -57,6 +58,21 @@ module Spectator::Matchers ChangeToMatcher.new(@expression, value) end + # Specifies that t he resulting value must be some amount different. + def by(amount : T) forall T + ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before } + end + + # Specifies that the resulting value must be at least some amount different. + def by_at_least(minimum : T) forall T + ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before } + end + + # Specifies that the resulting value must be at most some amount different. + def by_at_most(maximum : T) forall T + ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before } + end + # Performs the change and reports the before and after values. private def change(actual) before = expression.value # Retrieve the expression's initial value. diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr new file mode 100644 index 0000000..adb4cdc --- /dev/null +++ b/src/spectator/matchers/change_relative_matcher.cr @@ -0,0 +1,57 @@ +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" + +module Spectator::Matchers + # Matcher that tests whether an expression changed by an amount. + struct ChangeRelativeMatcher(ExpressionType) < Matcher + # The expression that is expected to (not) change. + private getter expression + + # Creates a new change matcher. + def initialize(@expression : TestBlock(ExpressionType), @relativity : String, + &evaluator : ExpressionType, ExpressionType -> Bool) + @evaluator = evaluator + end + + # 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 + "changes #{expression.label} #{@relativity}" + end + + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + before, after = change(actual) + if before == after + FailedMatchData.new("#{actual.label} did not change #{expression.label}", + before: before.inspect, + after: after.inspect + ) + elsif @evaluator.call(before, after) + SuccessfulMatchData.new + else + FailedMatchData.new("#{actual.label} did not change #{expression.label} #{@relativity}", + before: before.inspect, + after: after.inspect + ) + end + end + + # Performs the test against the expression, but inverted. + # A successful match with `#match` should normally fail for this method, and vice-versa. + def negated_match(actual : TestExpression(T)) : MatchData forall T + {% raise "The `expect { }.to_not change { }.by_...()` syntax is not supported (ambiguous)." %} + end + + # Performs the change and reports the before and after values. + private def change(actual) + before = expression.value # Retrieve the expression's initial value. + actual.value # Invoke action that might change the expression's value. + after = expression.value # Retrieve the expression's value again. + + {before, after} + end + end +end From 41580acee2a12ec55b8b1be65c92323e5bc7ee3f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 11 Aug 2019 23:11:15 -0600 Subject: [PATCH 22/22] Mark change matchers as complete --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b071104..0f07377 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ Items not marked as completed may have partial implementations. - [ ] Misc. matchers - [X] `match` - [ ] `satisfy` - - [ ] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]` + - [X] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]` - [X] `have_attributes` - [ ] Compound - `and`, `or` - [ ] Runner