2021-06-03 04:48:48 +00:00
require " json "
2021-01-16 17:22:23 +00:00
require " ./expression "
2021-02-13 05:46:22 +00:00
require " ./location "
2021-01-16 17:22:23 +00:00
module Spectator
# Result of evaluating a matcher on a target.
# Contains information about the match,
# such as whether it was successful and a description of the operation.
struct Expectation
# Location of the expectation in source code.
2021-11-28 22:45:17 +00:00
# This can be nil if the location can't be captured,
2021-01-16 17:22:23 +00:00
# for instance using the *should* syntax or dynamically created expectations.
2021-07-10 09:31:22 +00:00
getter ! location : Location
2021-01-16 17:22:23 +00:00
2021-01-21 07:03:57 +00:00
# Indicates whether the expectation was met.
def satisfied?
@match_data . matched?
end
# Indicates whether the expectation was not met.
def failed?
! satisfied?
end
# If nil, then the match was successful.
def failure_message?
2021-07-17 23:42:25 +00:00
return unless match_data = @match_data . as? ( Matchers :: FailedMatchData )
case message = @message
2021-07-31 20:18:59 +00:00
when String then message
2021-07-17 23:42:25 +00:00
when Proc ( String ) then @message = message . call # Cache result of call.
2021-07-31 20:18:59 +00:00
else match_data . failure_message
2021-07-17 23:42:25 +00:00
end
2021-01-21 07:03:57 +00:00
end
# Description of why the match failed.
def failure_message
failure_message? . not_nil!
end
# Additional information about the match, useful for debug.
# If nil, then the match was successful.
def values?
@match_data . as? ( Matchers :: FailedMatchData ) . try ( & . values )
end
# Additional information about the match, useful for debug.
def values
values? . not_nil!
end
def description
@match_data . description
end
2021-01-16 17:22:23 +00:00
# Creates the expectation.
# The *match_data* comes from the result of calling `Matcher#match`.
2021-02-13 05:46:22 +00:00
# The *location* is the location of the expectation in source code, if available.
2021-07-17 23:42:25 +00:00
# A custom *message* can be used in case of a failure.
def initialize ( @match_data : Matchers :: MatchData , @location : Location? = nil ,
2021-07-31 20:18:59 +00:00
@message : String ? | Proc ( String ) = nil )
2021-01-16 17:22:23 +00:00
end
2021-01-31 03:07:36 +00:00
# Creates the JSON representation of the expectation.
2021-06-03 04:48:48 +00:00
def to_json ( json : JSON :: Builder )
2021-01-31 03:07:36 +00:00
json . object do
2021-06-03 05:35:41 +00:00
if location = @location
json . field ( " file_path " , location . path )
json . field ( " line_number " , location . line )
end
2021-01-31 03:07:36 +00:00
json . field ( " satisfied " , satisfied? )
if ( failed = @match_data . as? ( Matchers :: FailedMatchData ) )
failed_to_json ( failed , json )
end
end
end
# Adds failure information to a JSON structure.
2021-06-03 04:48:48 +00:00
private def failed_to_json ( failed : Matchers :: FailedMatchData , json : JSON :: Builder )
2021-01-31 03:07:36 +00:00
json . field ( " failure " , failed . failure_message )
json . field ( " values " ) do
json . object do
failed . values . each do | pair |
json . field ( pair . first , pair . last )
end
end
end
end
2021-01-16 17:22:23 +00:00
# Stores part of an expectation.
2021-02-13 05:46:22 +00:00
# This covers the actual value (or block) being inspected and its location.
2021-01-16 17:22:23 +00:00
# This is the type returned by an `expect` block in the DSL.
# It is not intended to be used directly, but instead by chaining methods.
# Typically `#to` and `#not_to` are used.
struct Target ( T )
# Creates the expectation target.
# The *expression* is the actual value being tested and its label.
2021-02-13 05:46:22 +00:00
# The *location* is the location of where this expectation was defined.
def initialize ( @expression : Expression ( T ) , @location : Location )
2021-01-16 17:22:23 +00:00
end
2022-07-13 01:11:44 +00:00
# Asserts that a method is called some point before the example completes.
@[ AlwaysInline ]
def to ( stub : Stub , message = nil ) : Nil
{% raise " The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double) " unless T < :: Spectator :: Stubbable || T < :: Spectator :: StubbedType %}
to_eventually ( stub , message )
end
2021-01-16 17:22:23 +00:00
# Asserts that some criteria defined by the matcher is satisfied.
2021-07-17 23:42:25 +00:00
# Allows a custom message to be used.
def to ( matcher , message = nil ) : Nil
2021-01-16 17:22:23 +00:00
match_data = matcher . match ( @expression )
2021-07-17 23:42:25 +00:00
report ( match_data , message )
2021-01-16 17:22:23 +00:00
end
2022-12-20 04:29:21 +00:00
# Asserts that some criteria defined by the matcher is satisfied.
# Allows a custom message to be used.
# Returns the expected value cast as the expected type, if the matcher is satisfied.
def to ( matcher : Matchers :: TypeMatcher ( U ) , message = nil ) forall U
match_data = matcher . match ( @expression )
value = @expression . value
if report ( match_data , message )
return value if value . is_a? ( U )
raise " Spectator bug: expected value should have cast to #{ U } "
else
raise TypeCastError . new ( " #{ @expression . label } is expected to be a #{ U } , but was actually #{ value . class } " )
end
end
2022-07-13 01:11:44 +00:00
# Asserts that a method is not called before the example completes.
@[ AlwaysInline ]
def to_not ( stub : Stub , message = nil ) : Nil
{% raise " The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double) " unless T < :: Spectator :: Stubbable || T < :: Spectator :: StubbedType %}
to_never ( stub , message )
end
# :ditto:
@[ AlwaysInline ]
def not_to ( stub : Stub , message = nil ) : Nil
to_not ( stub , message )
end
2021-01-16 17:22:23 +00:00
# Asserts that some criteria defined by the matcher is not satisfied.
# This is effectively the opposite of `#to`.
2021-07-17 23:42:25 +00:00
# Allows a custom message to be used.
def to_not ( matcher , message = nil ) : Nil
2021-01-16 17:22:23 +00:00
match_data = matcher . negated_match ( @expression )
2021-07-17 23:42:25 +00:00
report ( match_data , message )
end
2022-12-20 04:29:21 +00:00
# Asserts that some criteria defined by the matcher is not satisfied.
# Allows a custom message to be used.
# Returns the expected value cast without the unexpected type, if the matcher is satisfied.
def to_not ( matcher : Matchers :: TypeMatcher ( U ) , message = nil ) forall U
match_data = matcher . negated_match ( @expression )
value = @expression . value
if report ( match_data , message )
return value unless value . is_a? ( U )
raise " Spectator bug: expected value should not be #{ U } "
else
raise TypeCastError . new ( " #{ @expression . label } is not expected to be a #{ U } , but was actually #{ value . class } " )
end
end
# Asserts that some criteria defined by the matcher is not satisfied.
# Allows a custom message to be used.
# Returns the expected value cast as a non-nillable type, if the matcher is satisfied.
def to_not ( matcher : Matchers :: NilMatcher , message = nil )
match_data = matcher . negated_match ( @expression )
if report ( match_data , message )
value = @expression . value
return value unless value . nil?
raise " Spectator bug: expected value should not be nil "
else
raise NilAssertionError . new ( " #{ @expression . label } is not expected to be nil. " )
end
end
2021-07-17 23:42:25 +00:00
# :ditto:
@[ AlwaysInline ]
def not_to ( matcher , message = nil ) : Nil
to_not ( matcher , message )
2021-01-16 17:22:23 +00:00
end
2022-07-13 01:11:44 +00:00
# Asserts that a method is called some point before the example completes.
def to_eventually ( stub : Stub , message = nil ) : Nil
{% raise " The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double) " unless T < :: Spectator :: Stubbable || T < :: Spectator :: StubbedType %}
stubbable = @expression . value
2022-07-13 01:23:13 +00:00
unless stubbable . _spectator_stub_for_method? ( stub . method )
# Add stub without an argument constraint.
# Avoids confusing logic like this:
# ```
# expect(dbl).to receive(:foo).with(:bar)
# dbl.foo(:baz)
# ```
# Notice that `#foo` is called, but with different arguments.
# Normally this would raise an error, but that should be prevented.
unconstrained_stub = stub . with ( Arguments . any )
stubbable . _spectator_define_stub ( unconstrained_stub )
end
2022-10-09 19:57:28 +00:00
# Apply the stub that is expected to be called.
2022-07-13 01:11:44 +00:00
stubbable . _spectator_define_stub ( stub )
2022-10-09 19:57:28 +00:00
# Check if the stub was invoked after the test completes.
2022-10-09 21:32:32 +00:00
matcher = Matchers :: ReceiveMatcher . new ( stub )
Harness . current . defer { to ( matcher , message ) }
# Prevent leaking stubs between tests.
Harness . current . cleanup { stubbable . _spectator_remove_stub ( stub ) }
2022-07-13 01:11:44 +00:00
end
2021-01-16 17:22:23 +00:00
# Asserts that some criteria defined by the matcher is eventually satisfied.
# The expectation is checked after the example finishes and all hooks have run.
2021-07-17 23:42:25 +00:00
# Allows a custom message to be used.
def to_eventually ( matcher , message = nil ) : Nil
Harness . current . defer { to ( matcher , message ) }
2021-01-16 17:22:23 +00:00
end
2022-07-13 01:11:44 +00:00
# Asserts that a method is not called before the example completes.
def to_never ( stub : Stub , message = nil ) : Nil
{% raise " The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double) " unless T < :: Spectator :: Stubbable || T < :: Spectator :: StubbedType %}
stubbable = @expression . value
2022-07-13 01:23:13 +00:00
unless stubbable . _spectator_stub_for_method? ( stub . method )
# Add stub without an argument constraint.
# Avoids confusing logic like this:
# ```
# expect(dbl).to receive(:foo).with(:bar)
# dbl.foo(:baz)
# ```
# Notice that `#foo` is called, but with different arguments.
# Normally this would raise an error, but that should be prevented.
unconstrained_stub = stub . with ( Arguments . any )
stubbable . _spectator_define_stub ( unconstrained_stub )
end
2022-10-09 19:57:28 +00:00
# Apply the stub that could be called in case it is.
2022-07-13 01:11:44 +00:00
stubbable . _spectator_define_stub ( stub )
2022-10-09 19:57:28 +00:00
# Check if the stub was invoked after the test completes.
2022-10-09 21:32:32 +00:00
matcher = Matchers :: ReceiveMatcher . new ( stub )
Harness . current . defer { to_not ( matcher , message ) }
# Prevent leaking stubs between tests.
Harness . current . cleanup { stubbable . _spectator_remove_stub ( stub ) }
2022-07-13 01:11:44 +00:00
end
# :ditto:
@[ AlwaysInline ]
def never_to ( stub : Stub , message = nil ) : Nil
to_never ( stub , message )
end
2021-01-16 17:22:23 +00:00
# Asserts that some criteria defined by the matcher is never satisfied.
# The expectation is checked after the example finishes and all hooks have run.
2021-07-17 23:42:25 +00:00
# Allows a custom message to be used.
def to_never ( matcher , message = nil ) : Nil
Harness . current . defer { to_not ( matcher , message ) }
end
# :ditto:
@[ AlwaysInline ]
def never_to ( matcher , message = nil ) : Nil
to_never ( matcher , message )
2021-01-16 17:22:23 +00:00
end
# Reports an expectation to the current harness.
2021-07-17 23:42:25 +00:00
private def report ( match_data : Matchers :: MatchData , message : String ? | Proc ( String ) = nil )
expectation = Expectation . new ( match_data , @location , message )
2021-01-16 18:02:29 +00:00
Harness . current . report ( expectation )
2021-01-16 17:22:23 +00:00
end
end
end
end