diff --git a/src/spectator/expectations/block_expectation_partial.cr b/src/spectator/expectations/block_expectation_partial.cr deleted file mode 100644 index 3adbec7..0000000 --- a/src/spectator/expectations/block_expectation_partial.cr +++ /dev/null @@ -1,25 +0,0 @@ -require "./expectation_partial" - -module Spectator::Expectations - # Expectation partial variation that operates on a block. - struct BlockExpectationPartial(ReturnType) < ExpectationPartial - # Actual value produced by calling the block. - def actual - @block.call - end - - # Creates the expectation partial. - # The label should be a string representation of the block. - # The block is stored for later use. - def initialize(@block : Proc(ReturnType), label, source_file, source_line) - super(label, source_file, source_line) - end - - # Creates the expectation partial. - # The label is generated by calling to_s on the block. - # The block is stored for later use. - def initialize(@block : Proc(ReturnType), source_file, source_line) - super("", source_file, source_line) - end - end -end diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 0238a8e..177bc94 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -1,39 +1,34 @@ +require "../matchers/match_data" +require "../source" +require "./actual" + module Spectator::Expectations - # Base class for all expectation partials. - # An "expectation partial" stores part of an expectation (obviously). - # The part of the expectation this class covers is the actual value. + # Stores part of an expectation (obviously). + # The part of the expectation this type covers is the actual value and source. # This can also cover a block's behavior. - # Sub-types of this class are returned by the `DSL::ExampleDSL.expect` call. - abstract struct ExpectationPartial - # User-friendly string displayed for the actual expression being tested. - # For instance, in the expectation: - # ``` - # expect(foo).to eq(bar) - # ``` - # This property will be "foo". - # It will be the literal string "foo", - # and not the actual value of the foo. - getter label : String + struct ExpectationPartial + # The actual value being tested. + # This also contains its label. + getter actual : Actual - # Source file the expectation originated from. - getter source_file : String + # Location where this expectation was defined. + getter source : Source - # Line number in the source file the expectation originated from. - getter source_line : Int32 - - # Creates the base of the partial. - private def initialize(@label, @source_file, @source_line) + # Creates the partial. + def initialize(@actual, @source) end # Asserts that some criteria defined by the matcher is satisfied. def to(matcher) : Nil - report(eval(matcher)) + match_data = matcher.match(@actual) + report(match_data) end # Asserts that some criteria defined by the matcher is not satisfied. # This is effectively the opposite of `#to`. def to_not(matcher) : Nil - report(eval(matcher, true)) + match_data = matcher.negated_match(@actual) + report(match_data) end # ditto @@ -42,14 +37,9 @@ module Spectator::Expectations to_not(matcher) end - # Evaluates the expectation and returns it. - private def eval(matcher, negated = false) - match_data = matcher.match(self) - Expectation.new(match_data, negated) - end - # Reports an expectation to the current harness. - private def report(expectation : Expectation) + private def report(match_data : Matchers::MatchData) + expectation = Expectation.new(match_data, @source) Internals::Harness.current.report_expectation(expectation) end end diff --git a/src/spectator/expectations/value_expectation_partial.cr b/src/spectator/expectations/value_expectation_partial.cr deleted file mode 100644 index 791bce5..0000000 --- a/src/spectator/expectations/value_expectation_partial.cr +++ /dev/null @@ -1,24 +0,0 @@ -require "./expectation_partial" - -module Spectator::Expectations - # Expectation partial variation that operates on a value. - struct ValueExpectationPartial(ActualType) < ExpectationPartial - # Actual value produced by the test. - # This is the value passed to the `Spectator::DSL::ExampleDSL#expect` macro. - getter actual - - # Creates the expectation partial. - # The label should be a string representation of the actual value. - # The actual value is stored for later use. - def initialize(@actual : ActualType, label, source_file, source_line) - super(label, source_file, source_line) - end - - # Creates the expectation partial. - # The label is generated by calling `to_s` on the actual value. - # The actual value is stored for later use. - def initialize(@actual : ActualType, source_file, source_line) - super(@actual.to_s, source_file, source_line) - end - end -end diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr index 8586918..a054de1 100644 --- a/src/spectator/matchers/matcher.cr +++ b/src/spectator/matchers/matcher.cr @@ -5,13 +5,47 @@ module Spectator::Matchers # A matcher looks at something produced by the SUT # and evaluates whether it is correct or not. abstract struct 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. - abstract def label : String + # 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. + # ``` + # it { is_expected.to do_something } + # ``` + # The phrasing should be such that it reads "it ___." + abstract def description : String - # Determines whether the matcher is satisfied with the value given to it. - # True is returned if the match was successful, false otherwise. - abstract def match(partial) : MatchData + # Message displayed when the matcher isn't satisifed. + # This is only called when `#matches?` returns false. + abstract def failure_message : String + + # Message displayed when the matcher isn't satisifed and is negated. + # This is only called when `#does_not_match?` returns false. + # + # A default implementation of this method is provided, + # which causes compilation to fail. + # If the matcher supports negation, it must override this method. + def failure_message_when_negated : String + {% raise "Negation with #{@type.name} is not supported."} + end + + # Checks whether the matcher is satisifed. + private abstract def match?(actual) : Bool + + # If the expectation is negated, then this method is called instead of `#match?`. + # The default implementation of this method is to invert the result of `#match?`. + # If the matcher requires custom handling of negated matches, + # then this method should be overriden. + # Remember to override `#failure_message_when_negated` as well. + private def does_not_match?(actual) : Bool + !matches?(actual) + end + + def match(actual) + matched = match?(actual) + end + + def negated_match(actual) + matched = does_not_match?(actual) + end end end