Skip to content

Commit

Permalink
Refactor StateMachine-related classes & improve related documentation
Browse files Browse the repository at this point in the history
Closes #427.

Closes #425.

I'm not 100% convinced about making StatePredicate a superclass of
State, but I think the overall effect is an improvement.

Co-authored-by: Nitish Rathi <nitishrathi@gmail.com>
  • Loading branch information
floehopper and nitishr committed Jan 20, 2020
2 parents fed0eee + 4d70b7e commit 8751dcb
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 48 deletions.
17 changes: 7 additions & 10 deletions lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,9 @@ def throws(tag, object = nil)

# @overload def then
# Used as syntactic sugar to improve readability. It has no effect on state of the expectation.
# @overload def then(state_machine.is(state_name))
# Used to change the +state_machine+ to the state specified by +state_name+ when the expected invocation occurs.
# @param [StateMachine::State] state_machine.is(state_name) provides a mechanism to change the +state_machine+ into the state specified by +state_name+ when the expected method is invoked.
# @overload def then(state)
# Used to change the +state_machine+ to the specified state when the expected invocation occurs.
# @param [StateMachine::State] state state_machine.is(state_name) provides a mechanism to change the +state_machine+ into the state specified by +state_name+ when the expected method is invoked.
#
# @see API#states
# @see StateMachine
Expand All @@ -481,17 +481,14 @@ def throws(tag, object = nil)
# radio.expects(:select_channel).with('BBC World Service').when(power.is('on'))
# radio.expects(:adjust_volume).with(-5).when(power.is('on'))
# radio.expects(:switch_off).then(power.is('off'))
def then(*parameters)
if parameters.length == 1
state = parameters.first
add_side_effect(ChangeStateSideEffect.new(state))
end
def then(state = nil)
add_side_effect(ChangeStateSideEffect.new(state)) if state
self
end

# Constrains the expectation to occur only when the +state_machine+ is in the state specified by +state_name+.
# Constrains the expectation to occur only when the +state_machine+ is in the state specified by +state_predicate+.
#
# @param [StateMachine::StatePredicate] state_machine.is(state_name) provides a mechanism to determine whether the +state_machine+ is in the state specified by +state_name+ when the expected method is invoked.
# @param [StateMachine::StatePredicate] state_predicate state_machine.is(state_name) provides a mechanism to determine whether the +state_machine+ is in the state specified by +state_predicate+ when the expected method is invoked.
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
# @see API#states
Expand Down
68 changes: 30 additions & 38 deletions lib/mocha/state_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,32 @@ module Mocha
# A state machine that is used to constrain the order of invocations.
# An invocation can be constrained to occur when a state {#is}, or {#is_not}, active.
class StateMachine
# Provides a mechanism to change the state of a {StateMachine} at some point in the future.
class State
# Provides the ability to determine whether a {StateMachine} is in a specified state at some point in the future.
class StatePredicate
# @private
def initialize(state_machine, state)
def initialize(state_machine, state, description, &active_check)
@state_machine = state_machine
@state = state
end

# @private
def activate
@state_machine.current_state = @state
@description = description
@active_check = active_check
end

# @private
def active?
@state_machine.current_state == @state
@active_check.call(@state_machine.current_state, @state)
end

# @private
def mocha_inspect
"#{@state_machine.name} is #{@state.mocha_inspect}"
"#{@state_machine.name} #{@description} #{@state.mocha_inspect}"
end
end

# Provides the ability to determine whether a {StateMachine} is in a specified state at some point in the future.
class StatePredicate
# @private
def initialize(state_machine, state)
@state_machine = state_machine
@state = state
end

# @private
def active?
@state_machine.current_state != @state
end

# Provides a mechanism to change the state of a {StateMachine} at some point in the future.
class State < StatePredicate
# @private
def mocha_inspect
"#{@state_machine.name} is not #{@state.mocha_inspect}"
def activate
@state_machine.current_state = @state
end
end

Expand Down Expand Up @@ -73,29 +59,35 @@ def become(next_state_name)
@current_state = next_state_name
end

# Provides a mechanism to change the {StateMachine} into the state specified by +state_name+ at some point in the future.
# Provides mechanisms to (a) determine whether the {StateMachine} is in a given state; or (b) to change the {StateMachine} into the given state.
#
# @param [String] state_name name of expected/desired state.
# @return [StatePredicate,State] (a) state predicate which, when queried, will indicate whether the {StateMachine} is in the given state; or (b) state which, when activated, will change the {StateMachine} into the given state.
#
# Or provides a mechanism to determine whether the {StateMachine} is in the state specified by +state_name+ at some point in the future.
# @overload def is(expected_state_name)
# Provides a mechanism to determine whether the {StateMachine} is in the state specified by +expected_state_name+ at some point in the future
# @param [String] expected_state_name name of expected state.
# @return [StatePredicate] state predicate which, when queried, will indicate whether the {StateMachine} is in the state specified by +expected_state_name+
#
# @param [String] state_name name of new state
# @return [State] state which, when activated, will change the {StateMachine} into the state with the specified +state_name+.
# @overload def is(desired_state_name)
# Provides a mechanism to change the {StateMachine} into the state specified by +desired_state_name+ at some point in the future.
# @param [String] desired_state_name name of desired new state.
# @return [State] state which, when activated, will change the {StateMachine} into the state with the specified +desired_state_name+.
def is(state_name)
State.new(self, state_name)
State.new(self, state_name, 'is') { |current, given| current == given }
end

# Provides a mechanism to determine whether the {StateMachine} is not in the state specified by +state_name+ at some point in the future.
# Provides a mechanism to determine whether the {StateMachine} is *not* in the state specified by +unexpected_state_name+ at some point in the future.
#
def is_not(state_name) # rubocop:disable Naming/PredicateName
StatePredicate.new(self, state_name)
# @param [String] unexpected_state_name name of unexpected state.
# @return [StatePredicate] state predicate which, when queried, will indicate whether the {StateMachine} is *not* in the state specified by +unexpected_state_name+.
def is_not(unexpected_state_name) # rubocop:disable Naming/PredicateName
StatePredicate.new(self, unexpected_state_name, 'is not') { |current, given| current != given }
end

# @private
def mocha_inspect
if @current_state
"#{@name} is #{@current_state.mocha_inspect}"
else
"#{@name} has no current state"
end
%(#{@name} #{@current_state ? "is #{@current_state.mocha_inspect}" : 'has no current state'})
end
end
end

0 comments on commit 8751dcb

Please sign in to comment.