Skip to content

Commit

Permalink
Fix any_event and any_state methods in the define method and definiti…
Browse files Browse the repository at this point in the history
…on class

Closes #76
  • Loading branch information
piotrmurach committed Oct 7, 2023
1 parent e6bd962 commit 63b34d9
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 85 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Fix defining object finalizer in Ruby 3.1 to reference a UUID string
by Vadim Kononov(@vkononov)
* Fix message queue shutdown to raise valid error
* Fix any_event and any_state methods in the define method and definition class

## [v0.14.0] - 2020-09-12

Expand Down
69 changes: 53 additions & 16 deletions lib/finite_machine/definition.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
# frozen_string_literal: true

module FiniteMachine
# A class responsible for defining standalone state machine
# Responsible for defining a standalone state machine
#
# @api public
class Definition
# The machine deferreds
# The any event constant
#
# @return [Array[Proc]]
# @example
# on_before(any_event) { ... }
#
# @api private
def self.deferreds
@deferreds ||= []
# @return [FiniteMachine::Const]
#
# @api public
def self.any_event
ANY_EVENT
end

# Add deferred
# The any state constant
#
# @param [Proc] deferred
# the deferred execution
# @example
# event :go, any_state => :green
#
# @return [Array[Proc]]
# @example
# on_enter(any_state) { ... }
#
# @api private
def self.add_deferred(deferred)
deferreds << deferred
# @return [FiniteMachine::Const]
#
# @api public
def self.any_state
ANY_STATE
end

# Instantiate a new Definition
# Initialize a StateMachine
#
# @example
# class Engine < FiniteMachine::Definition
Expand All @@ -43,7 +51,12 @@ def self.new(*args)
end
end

# Set deferrerd methods on the subclass
# Add deferred methods to the subclass
#
# @param [Class] subclass
# the inheriting subclass
#
# @return [void]
#
# @api private
def self.inherited(subclass)
Expand All @@ -52,11 +65,35 @@ def self.inherited(subclass)
deferreds.each { |d| subclass.add_deferred(d) }
end

# The state machine deferreds
#
# @return [Array<Proc>]
#
# @api private
def self.deferreds
@deferreds ||= []
end

# Add deferred
#
# @param [Proc] deferred
# the deferred execution
#
# @return [Array<Proc>]
#
# @api private
def self.add_deferred(deferred)
deferreds << deferred
end

# Delay lookup of DSL method
#
# @param [Symbol] method_name
# the method name
# @param [Array] arguments
# the method arguments
#
# @return [nil]
# @return [void]
#
# @api private
def self.method_missing(method_name, *arguments, &block)
Expand Down
124 changes: 99 additions & 25 deletions spec/unit/define_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

RSpec.describe FiniteMachine, ".define" do
context "with block" do
it "creates system state machine" do
stub_const("TrafficLights", FiniteMachine.define do
it "creates a state machine" do
stub_const("TrafficLights", described_class.define do
initial :green

event :slow, :green => :yellow
Expand All @@ -12,44 +12,118 @@
event :go, :yellow => :green
end)

lights_fsm_a = TrafficLights.new
lights_fsm_b = TrafficLights.new
fsm_a = TrafficLights.new
fsm_b = TrafficLights.new

expect(lights_fsm_a.current).to eql(:green)
expect(lights_fsm_b.current).to eql(:green)
expect(fsm_a.current).to eq(:green)
expect(fsm_b.current).to eq(:green)

lights_fsm_a.slow
expect(lights_fsm_a.current).to eql(:yellow)
expect(lights_fsm_b.current).to eql(:green)
fsm_a.slow
expect(fsm_a.current).to eq(:yellow)
expect(fsm_b.current).to eq(:green)

lights_fsm_a.stop
expect(lights_fsm_a.current).to eql(:red)
expect(lights_fsm_b.current).to eql(:green)
fsm_a.stop
expect(fsm_a.current).to eq(:red)
expect(fsm_b.current).to eq(:green)
end

it "uses any_state method inside the define method block" do
stub_const("TrafficLights", described_class.define do
initial :green

event :slow, any_state => :yellow

on_enter(any_state) { |event| target << "enter_#{event.to}" }
on_transition(any_state) { |event| target << "transition_#{event.to}" }
on_exit(any_state) { |event| target << "exit_#{event.from}" }
end)

fsm = TrafficLights.new(called = [])
fsm.slow

expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[exit_green transition_yellow enter_yellow])
end

it "uses any_event method inside the define method block" do
stub_const("TrafficLights", described_class.define do
initial :green

event :slow, :green => :yellow

on_before(any_event) { |event| target << "before_#{event.name}" }
on_after(any_event) { |event| target << "after_#{event.name}" }
end)

fsm = TrafficLights.new(called = [])
fsm.slow

expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[before_slow after_slow])
end
end

context "without block" do
it "creates state machine" do
it "creates a state machine" do
called = []
stub_const("TrafficLights", FiniteMachine.define)
stub_const("TrafficLights", described_class.define)
TrafficLights.initial(:green)
TrafficLights.event(:slow, :green => :yellow)
TrafficLights.event(:stop, :yellow => :red)
TrafficLights.event(:ready,:red => :yellow)
TrafficLights.event(:go, :yellow => :green)
TrafficLights.on_enter(:yellow) { |event| called << "on_enter_yellow" }
TrafficLights.handle(FiniteMachine::InvalidStateError) { |exception|
TrafficLights.event(:slow, :green => :yellow)
TrafficLights.event(:stop, :yellow => :red)
TrafficLights.event(:ready, :red => :yellow)
TrafficLights.event(:go, :yellow => :green)
TrafficLights.on_enter(:yellow) { called << "on_enter_yellow" }
TrafficLights.handle(FiniteMachine::InvalidStateError) do
called << "error_handler"
}
end

fsm = TrafficLights.new

expect(fsm.current).to eql(:green)
expect(fsm.current).to eq(:green)
fsm.slow
expect(fsm.current).to eql(:yellow)
expect(fsm.current).to eq(:yellow)
fsm.ready
expect(fsm.current).to eql(:yellow)
expect(called).to match_array(["on_enter_yellow", "error_handler"])
expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[on_enter_yellow error_handler])
end

it "uses any_state method outside the define method block" do
stub_const("TrafficLights", described_class.define)
TrafficLights.initial(:green)
TrafficLights.event(:slow, TrafficLights.any_state => :yellow)
TrafficLights.on_enter(TrafficLights.any_state) do |event|
target << "enter_#{event.to}"
end
TrafficLights.on_transition(TrafficLights.any_state) do |event|
target << "transition_#{event.to}"
end
TrafficLights.on_exit(TrafficLights.any_state) do |event|
target << "exit_#{event.from}"
end

fsm = TrafficLights.new(called = [])
fsm.slow

expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[exit_green transition_yellow enter_yellow])
end

it "uses any_event method outside the define method block" do
stub_const("TrafficLights", described_class.define)
TrafficLights.initial(:green)
TrafficLights.event(:slow, :green => :yellow)
TrafficLights.on_before(TrafficLights.any_event) do |event|
target << "before_#{event.name}"
end
TrafficLights.on_after(TrafficLights.any_event) do |event|
target << "after_#{event.name}"
end

fsm = TrafficLights.new(called = [])
fsm.slow

expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[before_slow after_slow])
end
end
end
82 changes: 61 additions & 21 deletions spec/unit/definition_spec.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
# frozen_string_literal: true

RSpec.describe FiniteMachine::Definition, "definition" do

RSpec.describe FiniteMachine::Definition do
before do
class Engine < FiniteMachine::Definition
stub_const("Engine", Class.new(described_class) do
initial :neutral

event :forward, [:reverse, :neutral] => :one
event :forward, %i[reverse neutral] => :one
event :shift, :one => :two
event :shift, :two => :one
event :back, [:neutral, :one] => :reverse
event :back, %i[neutral one] => :reverse

on_enter :reverse do |event|
on_enter :reverse do
target.turn_reverse_lights_on
end

on_exit :reverse do |event|
on_exit :reverse do
target.turn_reverse_lights_off
end

handle FiniteMachine::InvalidStateError do |exception| end
end
handle FiniteMachine::InvalidStateError do
target.turn_reverse_lights_off
end
end)
end

it "creates unique instances" do
Expand All @@ -35,7 +36,7 @@ class Engine < FiniteMachine::Definition
expect(engine_b.current).to eq(:neutral)
end

it "allows to create standalone machine" do
it "creates a standalone machine" do
stub_const("Car", Class.new do
def turn_reverse_lights_off
@reverse_lights = false
Expand All @@ -61,38 +62,77 @@ def reverse_lights?
engine.back
expect(engine.current).to eq(:reverse)
expect(car.reverse_lights?).to eq(true)

engine.shift
expect(engine.current).to eq(:reverse)
expect(car.reverse_lights?).to eq(false)
end

it "supports inheritance of definitions" do
class GenericStateMachine < FiniteMachine::Definition
it "uses any_state method inside the definition class" do
stub_const("TrafficLights", Class.new(described_class) do
initial :green

event :slow, any_state => :yellow

on_enter(any_state) { |event| target << "enter_#{event.to}" }
on_transition(any_state) { |event| target << "transition_#{event.to}" }
on_exit(any_state) { |event| target << "exit_#{event.from}" }
end)

fsm = TrafficLights.new(called = [])
fsm.slow

expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[exit_green transition_yellow enter_yellow])
end

it "uses any_event method inside the definition class" do
stub_const("TrafficLights", Class.new(described_class) do
initial :green

event :slow, :green => :yellow

on_before(any_event) { |event| target << "before_#{event.name}" }
on_after(any_event) { |event| target << "after_#{event.name}" }
end)

fsm = TrafficLights.new(called = [])
fsm.slow

expect(fsm.current).to eq(:yellow)
expect(called).to eq(%w[before_slow after_slow])
end

it "supports definitions inheritance" do
stub_const("GenericStateMachine", Class.new(described_class) do
initial :red

event :start, :red => :green

on_enter { |event| target << "generic" }
end
on_enter { target << "generic" }
end)

class SpecificStateMachine < GenericStateMachine
stub_const("SpecificStateMachine", Class.new(GenericStateMachine) do
event :stop, :green => :yellow

on_enter(:yellow) { |event| target << "specific" }
end
on_enter(:yellow) { target << "specific" }
end)

called = []
generic_fsm = GenericStateMachine.new(called)
specific_fsm = SpecificStateMachine.new(called)

expect(generic_fsm.states).to match_array([:none, :red, :green])
expect(specific_fsm.states).to match_array([:none, :red, :green, :yellow])
expect(generic_fsm.states).to eq(%i[none red green])
expect(specific_fsm.states).to eq(%i[none red green yellow])

expect(specific_fsm.current).to eq(:red)

specific_fsm.start
expect(specific_fsm.current).to eq(:green)
expect(called).to match_array(["generic"])
expect(called).to eq(%w[generic])

specific_fsm.stop
expect(specific_fsm.current).to eq(:yellow)
expect(called).to match_array(["generic", "generic", "specific"])
expect(called).to eq(%w[generic generic specific])
end
end
Loading

0 comments on commit 63b34d9

Please sign in to comment.