From 214b2e171e346e1b72e939e39cdc7e9886386fe8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Aug 2019 12:42:57 -0600 Subject: [PATCH] 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