Skip to content

Commit

Permalink
WIP refactor matcher interface
Browse files Browse the repository at this point in the history
Change MatchData to union of MatchFailure and nil.
  • Loading branch information
icy-arctic-fox committed Nov 27, 2024
1 parent 359edfe commit 6452cb6
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 263 deletions.
12 changes: 5 additions & 7 deletions src/spectator/assertion_failed.cr
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
require "./error"
require "./core/location_range"
require "./matchers/match_data"
require "./matchers/match_failure"

module Spectator
# Error raised when an assertion fails.
# This typically occurs when a matcher isn't satisfied and it's expectation isn't met.
class AssertionFailed < Error
getter match_data : Matchers::MatchData?
getter match_failure : Matchers::MatchFailure?

getter message : String? do
@match_data.try &.to_s
@match_failure.try &.to_s
end

getter location : Core::LocationRange? do
@match_data.try &.location
end
getter location : Core::LocationRange?

def initialize(@match_data : Matchers::MatchData)
def initialize(@match_failure : Matchers::MatchFailure, @location : Core::LocationRange? = nil)
super(nil)
end

Expand Down
51 changes: 51 additions & 0 deletions src/spectator/matchers/built_in/have_attributes_matcher.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Spectator::Matchers::BuiltIn
class HaveAttributesMatcher(Attributes)
private struct Attribute(T)
getter expected_value : T
getter expected_string : String
getter actual_string : String?
getter? matched : Bool

def initialize(@expected_value : T, @matched : Bool, @actual_string : String?)
@expected_string = @expected_value.inspect
end

def self.missing(expected_value) : self
new(expected_value, false, nil)
end
end

def initialize(attributes : Attributes)
{% begin %}
# @attributes = NamedTuple.new({% for key in Attributes %}
# {{key.stringify}}: 42, # Attribute.missing(attributes[{{key.symbolize}}]),
# {% end %})
{% debug %}
{% end %}
end

def matches?(actual_value)
@attributes = capture_attributes(actual_value)
@attributes.all? &.matched?
end

private def capture_attributes(actual_value)
{% for key in Attributes %}
%expected{key} = @attributes[{{key.stringify}}].expected_value
%attribute{key} = if actual_value.responds_to?({{key.symbolize}})
value = actual_value.{{key.symbolize}}
Attribute.new(%expected{key}, value == %expected{key}, value.inspect)
else
Attribute.missing(%expected{key})
end
{% end %}

NamedTuple.new({% for key in Attributes %}
{{key.stringify}}: %attribute{key},
{% end %})
end

def failure_message(actual_value)
end
end
end
64 changes: 40 additions & 24 deletions src/spectator/matchers/expect.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "../core/location_range"
require "./matcher"
require "./built_in/be_a_matcher"
require "./built_in/raise_error_matcher"

module Spectator::Matchers
Expand All @@ -11,28 +12,48 @@ module Spectator::Matchers
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Nil
return unless failure = Matcher.match(matcher, @actual_value, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
match_data = Matcher.match(matcher, @actual_value,
failure_message: failure_message,
location: location)
match_data.try_raise
failure.raise(location)
end

def to(matcher : BuiltIn::BeAMatcher(U), failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : U forall U
if failure = Matcher.match(matcher, @actual_value, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
failure.raise(location)
else
return actual_value if (actual_value = @actual_value).is_a?(U)
raise FrameworkError.new("Bug: Expected #{@actual_value} to be a #{U}")
end
end

def not_to(matcher, failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Nil
return unless failure = Matcher.match_negated(matcher, @actual_value, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
match_data = Matcher.match_negated(matcher, @actual_value,
failure_message: failure_message,
location: location)
match_data.try_raise
failure.raise(location)
end

def not_to(matcher : BuiltIn::BeNilMatcher, failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__)
if failure = Matcher.match_negated(matcher, @actual_value, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
failure.raise(location)
end
@actual_value.not_nil!("Bug: Expected #{@actual_value} to not be nil")
end

def to_not(matcher, failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Nil
source_end_line = __END_LINE__)
not_to(matcher, failure_message,
source_file: source_file,
source_line: source_line,
Expand All @@ -48,40 +69,35 @@ module Spectator::Matchers
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Nil
return unless failure = Matcher.match_block(matcher, @block, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
match_data = Matcher.match_block(matcher, @block,
failure_message: failure_message,
location: location)
match_data.try_raise
failure.raise(location)
end

def to(matcher : BuiltIn::RaiseErrorMatcher, failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Exception
location = Core::LocationRange.new(source_file, source_line, source_end_line)
match_data = Matcher.match_block(matcher, @block,
failure_message: failure_message,
location: location)
match_data.try_raise
matcher.rescued_error.not_nil!("BUG: Error should have been captured")
if failure = Matcher.match_block(matcher, @block, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
failure.raise(location)
end
matcher.rescued_error.not_nil!("Bug: Rescued error should have been captured by matcher")
end

def not_to(matcher, failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Nil
return unless failure = Matcher.match_negated_block(matcher, @block, failure_message)
location = Core::LocationRange.new(source_file, source_line, source_end_line)
match_data = Matcher.match_block_negated(matcher, @block,
failure_message: failure_message,
location: location)
match_data.try_raise
failure.raise(location)
end

def to_not(matcher, failure_message : String? = nil, *,
source_file = __FILE__,
source_line = __LINE__,
source_end_line = __END_LINE__) : Nil
source_end_line = __END_LINE__)
not_to(matcher, failure_message,
source_file: source_file,
source_line: source_line,
Expand Down
30 changes: 9 additions & 21 deletions src/spectator/matchers/formatting.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,20 @@ require "../formatters/printer"

module Spectator::Matchers
module Formatting
def description_of(value)
# TODO: Actually format the value.
value.inspect
end
struct DescriptionOf(T)
def initialize(@value : T)
end

def description_of(matchable : Matchable)
matchable.description
def apply(printer : FormattingPrinter)
end
end
end

struct FormattingPrinter
forward_missing_to @printer

def initialize(@printer : Formatters::Printer)
end

def description_of(value) : Nil
@printer.value(value)
end

def description_of(value : T.class) : Nil forall T
@printer.type(value)
def description_of(value)
DescriptionOf.new(value)
end

def description_of(matchable : Matchable) : Nil
@printer << matchable.description
def description_of(matchable : Matchable)
matchable.description
end
end
end
98 changes: 0 additions & 98 deletions src/spectator/matchers/match_data.cr

This file was deleted.

35 changes: 35 additions & 0 deletions src/spectator/matchers/match_failure.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require "../assertion_failed"
require "../formatters/plain_printer"
require "../formatters/printer"

module Spectator::Matchers
alias FailureMessagePrinter = Formatters::Printer ->

struct MatchFailure
@failure_message : String | FailureMessagePrinter

def initialize(failure_message : String)
@failure_message = failure_message
end

def initialize(&block : FailureMessagePrinter)
@failure_message = block
end

def raise(location)
raise AssertionFailed.new(self, location)
end

def print_failure_message(printer : Formatters::Printer) : Nil
case failure_message = @failure_message
in FailureMessagePrinter then failure_message.call(printer)
in String then printer << failure_message
end
end

def to_s(io : IO) : Nil
printer = Formatters::PlainPrinter.new(io)
print_failure_message(printer)
end
end
end
Loading

0 comments on commit 6452cb6

Please sign in to comment.