Pushdown::SpecHelpers::
StateTransitionMatcher
class
RSpec matcher for matching transitions of Pushdown::States
- additional_expectations R
- callback R
- expected_type R
- failure_description R
- result R
- state R
- target_state R
Create a new matcher that expects a transition to occur.
def initialize
@expected_type = nil
@target_state = nil
@callback = nil
@additional_expectations = []
@state = nil
@result = nil
@failure_description = nil
end
RSpec matcher API – return a message describing an expectation failure.
def failure_message
return "%p: %s" % [ self.state, self.describe_failure ]
end
failure_message_when_negated()
RSpec matcher API – return a message describing an expectation being met when the matcher was used in a negated context.
def failure_message_when_negated
return "%p: %s" % [ self.state, self.describe_negated_failure ]
end
RSpec matcher API – returns true
if all expectations are met after calling
update on the specified state
.
def matches?( state )
@state = state
return self.update_ran_without_error? &&
self.update_returned_transition? &&
self.correct_transition_type? &&
self.correct_target_state? &&
self.matches_additional_expectations?
end
on_an_event( event, *args )
Specify that the operation that should cause the transition is the on_event
callback.
def on_an_event( event, *args )
raise ScriptError, "can't specify more than one callback" if self.callback
@callback = [ :on_event, event, *args ]
return self
end
Specify that the operation that should cause the transition is the update callback. This is the default.
def on_update
raise ScriptError, "can't specify more than one callback" if self.callback
@callback = [ :update ]
return self
end
Add an additional expectation that the state that is transitioned to be of the given state_name
. This only applies to transitions that take a target state type. Expecting a particular state_name in transitions which do not take a state is undefined behavior.
def to_state( state_name )
@target_state = state_name
return self
end
via_transition_type( transition_type )
Add an additional expectation that the transition that occurs be of the specified transition_type
.
def via_transition_type( transition_type )
@expected_type = transition_type
return self
end
Protected Instance Methods
Returns true
if a target state was specified and the transition which occurred was to that state.
def correct_target_state?
state_name = self.target_state or return true
case self.result
when Pushdown::Transition
return self.result.respond_to?( :state_class ) &&
self.result.state_class.type_name == state_name
when Symbol
self.state.class.transitions[ self.result ][ 1 ] == state_name
end
end
correct_transition_type?()
Returns true
if a transition type was specified and the transition which occurred was of that type.
def correct_transition_type?
type = self.expected_type or return true
case self.result
when Pushdown::Transition
self.result.type_name == type
when Symbol
self.state.class.transitions[ self.result ].first == type
else
raise "unexpected transition result type %p" % [ self.result ]
end
end
describe_additional_expectations()
Return an Array of descriptions of the members that were expected to be included in the state body, if any were specified. If none were specified, returns an empty Array.
def describe_additional_expectations
return self.additional_expectations.map( &:description )
end
Build an appropriate failure messages for the matcher.
def describe_failure
desc = String.new( "expected to transition" )
desc << " via %s" % [ self.expected_type ] if self.expected_type
desc << " to %s" % [ self.target_state ] if self.target_state
if self.callback
methname, *args = self.callback
desc << " when #%s is called" % [ methname ]
desc << " with %s" % [ args.map(&:inspect).join(', ') ] if !args.empty?
end
desc << ', but '
case self.result
when Exception
err = self.result
desc << "got %p: %s" % [ err.class, err.message ]
when Symbol
transition = self.state.class.transitions[ self.result ]
if transition
desc << "it returned a %s transition " % [ transition.first ]
desc << "to %s " % [ transition[1] ] if transition[1]
desc << " instead"
else
desc << "it returned an unmapped Symbol (%p)" % [ self.result ]
end
when Pushdown::Transition
desc << "it returned a %s transition" % [ self.result.type_name ]
desc << " to %s" % [ self.result.state_class.type_name ] if
self.result.respond_to?( :state_class )
desc << " instead"
else
desc << "it did not (returned: %p)" % [ self.result ]
end
return desc
end
describe_negated_failure()
Build an appropriate failure message for the matcher.
def describe_negated_failure
desc = String.new( "expected not to transition" )
desc << " via %s" % [ self.expected_type ] if self.expected_type
desc << " to %s" % [ self.target_state ] if self.target_state
desc << ', but it did.'
return desc
end
matches_additional_expectations?()
Check that any additional matchers registered via the .and
mutator also match the parsed state body.
def matches_additional_expectations?
return self.additional_expectations.all? do |matcher|
matcher.matches?( self.parsed_state_body ) or
fail_with( matcher.failure_message )
end
end
update_ran_without_error?()
Call the state’s update callback and record the result, then return true
if no exception was raised.
def update_ran_without_error?
operation = self.callback || DEFAULT_CALLBACK
@result = begin
self.state.public_send( *operation )
rescue => err
err
end
return !@result.is_a?( Exception )
end
update_returned_transition?()
Returns true
if the result of calling update was a Transition
or a Symbol that corresponds with a valid transition.
def update_returned_transition?
case self.result
when Pushdown::Transition
return true
when Symbol
return self.state.class.transitions.include?( self.result )
else
return false
end
end