finite_machine 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/README.md +467 -11
- data/Rakefile +40 -0
- data/lib/finite_machine.rb +44 -2
- data/lib/finite_machine/callable.rb +51 -0
- data/lib/finite_machine/dsl.rb +134 -0
- data/lib/finite_machine/event.rb +66 -0
- data/lib/finite_machine/observer.rb +136 -0
- data/lib/finite_machine/state_machine.rb +188 -0
- data/lib/finite_machine/subscribers.rb +41 -0
- data/lib/finite_machine/threadable.rb +44 -0
- data/lib/finite_machine/transition.rb +69 -0
- data/lib/finite_machine/version.rb +3 -1
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/callbacks_spec.rb +391 -0
- data/spec/unit/can_spec.rb +50 -0
- data/spec/unit/define_spec.rb +32 -0
- data/spec/unit/events_spec.rb +256 -0
- data/spec/unit/finished_spec.rb +51 -0
- data/spec/unit/if_unless_spec.rb +196 -0
- data/spec/unit/initialize_spec.rb +59 -0
- data/spec/unit/is_spec.rb +33 -0
- data/spec/unit/states_spec.rb +21 -0
- data/spec/unit/transition/parse_states_spec.rb +34 -0
- metadata +35 -3
@@ -0,0 +1,188 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
|
5
|
+
# Base class for state machine
|
6
|
+
class StateMachine
|
7
|
+
include Threadable
|
8
|
+
|
9
|
+
# Initial state, defaults to :none
|
10
|
+
attr_threadsafe :initial_state
|
11
|
+
|
12
|
+
# Final state, defaults to :none
|
13
|
+
attr_threadsafe :final_state
|
14
|
+
|
15
|
+
# Current state
|
16
|
+
attr_threadsafe :state
|
17
|
+
|
18
|
+
# Events DSL
|
19
|
+
attr_threadsafe :events
|
20
|
+
|
21
|
+
# The prefix used to name events.
|
22
|
+
attr_threadsafe :namespace
|
23
|
+
|
24
|
+
# The events and their transitions.
|
25
|
+
attr_threadsafe :transitions
|
26
|
+
|
27
|
+
# The state machine observer
|
28
|
+
attr_threadsafe :observer
|
29
|
+
|
30
|
+
# The state machine subscribers
|
31
|
+
attr_threadsafe :subscribers
|
32
|
+
|
33
|
+
# The state machine environment
|
34
|
+
attr_threadsafe :env
|
35
|
+
|
36
|
+
# Initialize state machine
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
def initialize(*args, &block)
|
40
|
+
@subscribers = Subscribers.new(self)
|
41
|
+
@events = EventsDSL.new(self)
|
42
|
+
@observer = Observer.new(self)
|
43
|
+
@transitions = Hash.new { |hash, name| hash[name] = Hash.new }
|
44
|
+
@env = Environment.new(target: self)
|
45
|
+
|
46
|
+
@dsl = DSL.new self
|
47
|
+
@dsl.call(&block) if block_given?
|
48
|
+
send(:"#{@dsl.initial_event}") unless @dsl.defer
|
49
|
+
end
|
50
|
+
|
51
|
+
def subscribe(*observers)
|
52
|
+
@subscribers.subscribe(*observers)
|
53
|
+
end
|
54
|
+
|
55
|
+
# TODO: use trigger to actually fire state machine events!
|
56
|
+
# Notify about event
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def notify(event_type, _transition, *data)
|
60
|
+
event_class = Event.const_get(event_type.capitalize.to_s)
|
61
|
+
state_or_action = event_class < Event::Anystate ? state : _transition.name
|
62
|
+
_event = event_class.new(state_or_action, _transition, *data)
|
63
|
+
subscribers.visit(_event)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get current state
|
67
|
+
#
|
68
|
+
# @return [String]
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def current
|
72
|
+
state
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if current state machtes provided state
|
76
|
+
#
|
77
|
+
# @param [String, Array[String]] state
|
78
|
+
#
|
79
|
+
# @return [Boolean]
|
80
|
+
#
|
81
|
+
# @api public
|
82
|
+
def is?(state)
|
83
|
+
if state.is_a?(Array)
|
84
|
+
state.include? current
|
85
|
+
else
|
86
|
+
state == current
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieve all states
|
91
|
+
#
|
92
|
+
# @return [Array[Symbol]]
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def states
|
96
|
+
event_names.map { |event| transitions[event].to_a }.flatten.uniq
|
97
|
+
end
|
98
|
+
|
99
|
+
# Retireve all event names
|
100
|
+
#
|
101
|
+
# @return [Array[Symbol]]
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
def event_names
|
105
|
+
transitions.keys
|
106
|
+
end
|
107
|
+
|
108
|
+
# Checks if event can be triggered
|
109
|
+
#
|
110
|
+
# @param [String] event
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
113
|
+
#
|
114
|
+
# @api public
|
115
|
+
def can?(event)
|
116
|
+
transitions[event].key?(current) || transitions[event].key?(ANY_STATE)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Checks if event cannot be triggered
|
120
|
+
#
|
121
|
+
# @param [String] event
|
122
|
+
#
|
123
|
+
# @return [Boolean]
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def cannot?(event)
|
127
|
+
!can?(event)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Checks if terminal state has been reached
|
131
|
+
#
|
132
|
+
# @return [Boolean]
|
133
|
+
#
|
134
|
+
# @api public
|
135
|
+
def finished?
|
136
|
+
is?(final_state)
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
#
|
141
|
+
# @api public
|
142
|
+
def errors
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
# Check if state is reachable
|
148
|
+
#
|
149
|
+
# @api private
|
150
|
+
def validate_state(_transition)
|
151
|
+
current_states = transitions[_transition.name].keys
|
152
|
+
if !current_states.include?(state) && !current_states.include?(ANY_STATE)
|
153
|
+
raise TransitionError, "inappropriate current state '#{state}'"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Performs transition
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
def transition(_transition, *args, &block)
|
161
|
+
validate_state(_transition)
|
162
|
+
|
163
|
+
return CANCELLED unless _transition.conditions.all? { |c| c.call(env) }
|
164
|
+
return NOTRANSITION if state == _transition.to
|
165
|
+
|
166
|
+
sync_exclusive do
|
167
|
+
notify :exitstate, _transition, *args
|
168
|
+
notify :enteraction, _transition, *args
|
169
|
+
|
170
|
+
begin
|
171
|
+
_transition.call
|
172
|
+
|
173
|
+
notify :transitionstate, _transition, *args
|
174
|
+
notify :transitionaction, _transition, *args
|
175
|
+
rescue StandardError => e
|
176
|
+
raise TransitionError, "#(#{e.class}): #{e.message}\n" +
|
177
|
+
"occured at #{e.backtrace.join("\n")}"
|
178
|
+
end
|
179
|
+
|
180
|
+
notify :enterstate, _transition, *args
|
181
|
+
notify :exitaction, _transition, *args
|
182
|
+
end
|
183
|
+
|
184
|
+
SUCCEEDED
|
185
|
+
end
|
186
|
+
|
187
|
+
end # StateMachine
|
188
|
+
end # FiniteMachine
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
|
5
|
+
# A class responsibile for storage of event subscribers
|
6
|
+
class Subscribers
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize(machine)
|
10
|
+
@machine = machine
|
11
|
+
@subscribers = []
|
12
|
+
@mutex = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&block)
|
16
|
+
@subscribers.each(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def index(subscriber)
|
20
|
+
@subscribers.index(subscriber)
|
21
|
+
end
|
22
|
+
|
23
|
+
def empty?
|
24
|
+
@subscribers.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def subscribe(*observers)
|
28
|
+
observers.each { |observer| @subscribers << observer }
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit(event)
|
32
|
+
each { |subscriber| @mutex.synchronize { event.notify subscriber } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset
|
36
|
+
@subscribers.clear
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
end # Subscribers
|
41
|
+
end # FiniteMachine
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
module Threadable
|
5
|
+
module InstanceMethods
|
6
|
+
@@sync = Sync.new
|
7
|
+
|
8
|
+
def sync_exclusive(&block)
|
9
|
+
@@sync.synchronize(:EX, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sync_shared(&block)
|
13
|
+
@@sync.synchronize(:SH, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(base)
|
18
|
+
base.extend ClassMethods
|
19
|
+
base.module_eval do
|
20
|
+
include InstanceMethods
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
include InstanceMethods
|
26
|
+
|
27
|
+
def attr_threadsafe(*attrs)
|
28
|
+
attrs.flatten.each do |attr|
|
29
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
30
|
+
def #{attr}
|
31
|
+
sync_shared { @#{attr} }
|
32
|
+
end
|
33
|
+
alias_method '#{attr}?', '#{attr}'
|
34
|
+
|
35
|
+
def #{attr}=(value)
|
36
|
+
sync_exclusive { @#{attr} = value }
|
37
|
+
end
|
38
|
+
RUBY_EVAL
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end # Threadable
|
44
|
+
end # FiniteMachine
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
|
5
|
+
# Class describing a transition associated with a given event
|
6
|
+
class Transition
|
7
|
+
include Threadable
|
8
|
+
|
9
|
+
attr_threadsafe :name
|
10
|
+
|
11
|
+
# State transitioning from
|
12
|
+
attr_threadsafe :from
|
13
|
+
|
14
|
+
# State transitioning to
|
15
|
+
attr_threadsafe :to
|
16
|
+
|
17
|
+
# Predicates before transitioning
|
18
|
+
attr_threadsafe :conditions
|
19
|
+
|
20
|
+
# The current state machine
|
21
|
+
attr_threadsafe :machine
|
22
|
+
|
23
|
+
# Initialize a Transition
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def initialize(machine, attrs = {})
|
27
|
+
@machine = machine
|
28
|
+
@name = attrs.fetch(:name, DEFAULT_STATE)
|
29
|
+
@from, @to = *parse_states(attrs)
|
30
|
+
@if = Array(attrs.fetch(:if, []))
|
31
|
+
@unless = Array(attrs.fetch(:unless, []))
|
32
|
+
@conditions = make_conditions
|
33
|
+
end
|
34
|
+
|
35
|
+
def make_conditions
|
36
|
+
@if.map { |c| Callable.new(c) } +
|
37
|
+
@unless.map { |c| Callable.new(c).invert }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Extract states from attributes
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def parse_states(attrs)
|
44
|
+
_attrs = attrs.dup
|
45
|
+
[:name, :if, :unless].each { |key| _attrs.delete(key) }
|
46
|
+
|
47
|
+
if [:from, :to].any? { |key| attrs.keys.include?(key) }
|
48
|
+
[Array(_attrs[:from] || ANY_STATE), _attrs[:to]]
|
49
|
+
else
|
50
|
+
[(keys = _attrs.keys).flatten, _attrs[keys.first]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute current transition
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
def call
|
58
|
+
sync_exclusive do
|
59
|
+
transitions = machine.transitions[name]
|
60
|
+
machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
[@name, @from, @to, @conditions].inspect
|
66
|
+
end
|
67
|
+
|
68
|
+
end # Transition
|
69
|
+
end # FiniteMachine
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'finite_machine'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
config.run_all_when_everything_filtered = true
|
8
|
+
config.filter_run :focus
|
9
|
+
|
10
|
+
# Run specs in random order to surface order dependencies. If you find an
|
11
|
+
# order dependency and want to debug it, you can fix the order by providing
|
12
|
+
# the seed, which is printed after each run.
|
13
|
+
# --seed 1234
|
14
|
+
config.order = 'random'
|
15
|
+
end
|
@@ -0,0 +1,391 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FiniteMachine, 'callbacks' do
|
6
|
+
|
7
|
+
it "triggers default init event" do
|
8
|
+
called = []
|
9
|
+
fsm = FiniteMachine.define do
|
10
|
+
initial :green
|
11
|
+
|
12
|
+
events {
|
13
|
+
event :slow, :green => :yellow
|
14
|
+
event :stop, :yellow => :red
|
15
|
+
event :ready, :red => :yellow
|
16
|
+
event :go, :yellow => :green
|
17
|
+
}
|
18
|
+
|
19
|
+
callbacks {
|
20
|
+
# generic callbacks
|
21
|
+
on_enter do |event| called << 'on_enter' end
|
22
|
+
on_transition do |event| called << 'on_transition' end
|
23
|
+
on_exit do |event| called << 'on_exit' end
|
24
|
+
|
25
|
+
# state callbacks
|
26
|
+
on_enter :none do |event| called << 'on_enter_none' end
|
27
|
+
on_enter :green do |event| called << 'on_enter_green' end
|
28
|
+
|
29
|
+
on_transition :none do |event| called << 'on_transition_none' end
|
30
|
+
on_transition :green do |event| called << 'on_transition_green' end
|
31
|
+
|
32
|
+
on_exit :none do |event| called << 'on_exit_none' end
|
33
|
+
on_exit :green do |event| called << 'on_exit_green' end
|
34
|
+
|
35
|
+
# event callbacks
|
36
|
+
on_enter :init do |event| called << 'on_enter_init' end
|
37
|
+
on_transition :init do |event| called << 'on_transition_init' end
|
38
|
+
on_exit :init do |event| called << 'on_exit_init' end
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
expect(called).to eql([
|
43
|
+
'on_exit_none',
|
44
|
+
'on_exit',
|
45
|
+
'on_enter_init',
|
46
|
+
'on_enter',
|
47
|
+
'on_transition_green',
|
48
|
+
'on_transition',
|
49
|
+
'on_transition_init',
|
50
|
+
'on_transition',
|
51
|
+
'on_enter_green',
|
52
|
+
'on_enter',
|
53
|
+
'on_exit_init',
|
54
|
+
'on_exit'
|
55
|
+
])
|
56
|
+
end
|
57
|
+
|
58
|
+
it "executes callbacks in order" do
|
59
|
+
called = []
|
60
|
+
fsm = FiniteMachine.define do
|
61
|
+
initial :green
|
62
|
+
|
63
|
+
events {
|
64
|
+
event :slow, :green => :yellow
|
65
|
+
event :stop, :yellow => :red
|
66
|
+
event :ready, :red => :yellow
|
67
|
+
event :go, :yellow => :green
|
68
|
+
}
|
69
|
+
|
70
|
+
callbacks {
|
71
|
+
# generic callbacks
|
72
|
+
on_enter do |event| called << 'on_enter' end
|
73
|
+
on_transition do |event| called << 'on_transition' end
|
74
|
+
on_exit do |event| called << 'on_exit' end
|
75
|
+
|
76
|
+
# state callbacks
|
77
|
+
on_enter :green do |event| called << 'on_enter_green' end
|
78
|
+
on_enter :yellow do |event| called << "on_enter_yellow" end
|
79
|
+
on_enter :red do |event| called << "on_enter_red" end
|
80
|
+
|
81
|
+
on_transition :green do |event| called << 'on_transition_green' end
|
82
|
+
on_transition :yellow do |event| called << "on_transition_yellow" end
|
83
|
+
on_transition :red do |event| called << "on_transition_red" end
|
84
|
+
|
85
|
+
on_exit :green do |event| called << 'on_exit_green' end
|
86
|
+
on_exit :yellow do |event| called << "on_exit_yellow" end
|
87
|
+
on_exit :red do |event| called << "on_exit_red" end
|
88
|
+
|
89
|
+
# event callbacks
|
90
|
+
on_enter :slow do |event| called << 'on_enter_slow' end
|
91
|
+
on_enter :stop do |event| called << "on_enter_stop" end
|
92
|
+
on_enter :ready do |event| called << "on_enter_ready" end
|
93
|
+
on_enter :go do |event| called << "on_enter_go" end
|
94
|
+
|
95
|
+
on_transition :slow do |event| called << 'on_transition_slow' end
|
96
|
+
on_transition :stop do |event| called << "on_transition_stop" end
|
97
|
+
on_transition :ready do |event| called << "on_transition_ready" end
|
98
|
+
on_transition :go do |event| called << "on_transition_go" end
|
99
|
+
|
100
|
+
on_exit :slow do |event| called << 'on_exit_slow' end
|
101
|
+
on_exit :stop do |event| called << "on_exit_stop" end
|
102
|
+
on_exit :ready do |event| called << "on_exit_ready" end
|
103
|
+
on_exit :go do |event| called << "on_exit_go" end
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
called = []
|
108
|
+
fsm.slow
|
109
|
+
expect(called).to eql([
|
110
|
+
'on_exit_green',
|
111
|
+
'on_exit',
|
112
|
+
'on_enter_slow',
|
113
|
+
'on_enter',
|
114
|
+
'on_transition_yellow',
|
115
|
+
'on_transition',
|
116
|
+
'on_transition_slow',
|
117
|
+
'on_transition',
|
118
|
+
'on_enter_yellow',
|
119
|
+
'on_enter',
|
120
|
+
'on_exit_slow',
|
121
|
+
'on_exit'
|
122
|
+
])
|
123
|
+
|
124
|
+
called = []
|
125
|
+
fsm.stop
|
126
|
+
expect(called).to eql([
|
127
|
+
'on_exit_yellow',
|
128
|
+
'on_exit',
|
129
|
+
'on_enter_stop',
|
130
|
+
'on_enter',
|
131
|
+
'on_transition_red',
|
132
|
+
'on_transition',
|
133
|
+
'on_transition_stop',
|
134
|
+
'on_transition',
|
135
|
+
'on_enter_red',
|
136
|
+
'on_enter',
|
137
|
+
'on_exit_stop',
|
138
|
+
'on_exit'
|
139
|
+
])
|
140
|
+
|
141
|
+
called = []
|
142
|
+
fsm.ready
|
143
|
+
expect(called).to eql([
|
144
|
+
'on_exit_red',
|
145
|
+
'on_exit',
|
146
|
+
'on_enter_ready',
|
147
|
+
'on_enter',
|
148
|
+
'on_transition_yellow',
|
149
|
+
'on_transition',
|
150
|
+
'on_transition_ready',
|
151
|
+
'on_transition',
|
152
|
+
'on_enter_yellow',
|
153
|
+
'on_enter',
|
154
|
+
'on_exit_ready',
|
155
|
+
'on_exit'
|
156
|
+
])
|
157
|
+
|
158
|
+
called = []
|
159
|
+
fsm.go
|
160
|
+
expect(called).to eql([
|
161
|
+
'on_exit_yellow',
|
162
|
+
'on_exit',
|
163
|
+
'on_enter_go',
|
164
|
+
'on_enter',
|
165
|
+
'on_transition_green',
|
166
|
+
'on_transition',
|
167
|
+
'on_transition_go',
|
168
|
+
'on_transition',
|
169
|
+
'on_enter_green',
|
170
|
+
'on_enter',
|
171
|
+
'on_exit_go',
|
172
|
+
'on_exit'
|
173
|
+
])
|
174
|
+
end
|
175
|
+
|
176
|
+
it "allows multiple callbacks for the same state" do
|
177
|
+
called = []
|
178
|
+
fsm = FiniteMachine.define do
|
179
|
+
initial :green
|
180
|
+
|
181
|
+
events {
|
182
|
+
event :slow, :green => :yellow
|
183
|
+
event :stop, :yellow => :red
|
184
|
+
event :ready, :red => :yellow
|
185
|
+
event :go, :yellow => :green
|
186
|
+
}
|
187
|
+
|
188
|
+
callbacks {
|
189
|
+
# generic callbacks
|
190
|
+
on_enter do |event| called << 'on_enter' end
|
191
|
+
on_transition do |event| called << 'on_transition' end
|
192
|
+
on_exit do |event| called << 'on_exit' end
|
193
|
+
|
194
|
+
# state callbacks
|
195
|
+
on_exit :green do |event| called << 'on_exit_green_1' end
|
196
|
+
on_exit :green do |event| called << 'on_exit_green_2' end
|
197
|
+
on_enter :yellow do |event| called << 'on_enter_yellow_1' end
|
198
|
+
on_enter :yellow do |event| called << 'on_enter_yellow_2' end
|
199
|
+
on_transition :yellow do |event| called << 'on_transition_yellow_1' end
|
200
|
+
on_transition :yellow do |event| called << 'on_transition_yellow_2' end
|
201
|
+
|
202
|
+
# event callbacks
|
203
|
+
on_enter :slow do |event| called << 'on_enter_slow_1' end
|
204
|
+
on_enter :slow do |event| called << 'on_enter_slow_2' end
|
205
|
+
on_transition :slow do |event| called << 'on_transition_slow_1' end
|
206
|
+
on_transition :slow do |event| called << 'on_transition_slow_2' end
|
207
|
+
on_exit :slow do |event| called << 'on_exit_slow_1' end
|
208
|
+
on_exit :slow do |event| called << 'on_exit_slow_2' end
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
called = []
|
213
|
+
fsm.slow
|
214
|
+
expect(fsm.current).to eql(:yellow)
|
215
|
+
expect(called).to eql([
|
216
|
+
'on_exit_green_1',
|
217
|
+
'on_exit_green_2',
|
218
|
+
'on_exit',
|
219
|
+
'on_enter_slow_1',
|
220
|
+
'on_enter_slow_2',
|
221
|
+
'on_enter',
|
222
|
+
'on_transition_yellow_1',
|
223
|
+
'on_transition_yellow_2',
|
224
|
+
'on_transition',
|
225
|
+
'on_transition_slow_1',
|
226
|
+
'on_transition_slow_2',
|
227
|
+
'on_transition',
|
228
|
+
'on_enter_yellow_1',
|
229
|
+
'on_enter_yellow_2',
|
230
|
+
'on_enter',
|
231
|
+
'on_exit_slow_1',
|
232
|
+
'on_exit_slow_2',
|
233
|
+
'on_exit'
|
234
|
+
])
|
235
|
+
end
|
236
|
+
|
237
|
+
it "allows for fluid callback definition" do
|
238
|
+
called = []
|
239
|
+
fsm = FiniteMachine.define do
|
240
|
+
initial :green
|
241
|
+
|
242
|
+
events {
|
243
|
+
event :slow, :green => :yellow
|
244
|
+
event :stop, :yellow => :red
|
245
|
+
event :ready, :red => :yellow
|
246
|
+
event :go, :yellow => :green
|
247
|
+
}
|
248
|
+
|
249
|
+
callbacks {
|
250
|
+
# state callbacks
|
251
|
+
on_exit_green do |event| called << 'on_exit_green' end
|
252
|
+
on_enter_yellow do |event| called << 'on_enter_yellow' end
|
253
|
+
on_transition_yellow do |event| called << 'on_transition_yellow' end
|
254
|
+
|
255
|
+
# event callbacks
|
256
|
+
on_enter_slow do |event| called << 'on_enter_slow' end
|
257
|
+
on_transition_slow do |event| called << 'on_transition_slow' end
|
258
|
+
on_exit_slow do |event| called << 'on_exit_slow' end
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
called = []
|
263
|
+
fsm.slow
|
264
|
+
expect(fsm.current).to eql(:yellow)
|
265
|
+
expect(called).to eql([
|
266
|
+
'on_exit_green',
|
267
|
+
'on_enter_slow',
|
268
|
+
'on_transition_yellow',
|
269
|
+
'on_transition_slow',
|
270
|
+
'on_enter_yellow',
|
271
|
+
'on_exit_slow'
|
272
|
+
])
|
273
|
+
end
|
274
|
+
|
275
|
+
it "passes event object to callback" do
|
276
|
+
evt = nil
|
277
|
+
|
278
|
+
fsm = FiniteMachine.define do
|
279
|
+
initial :green
|
280
|
+
|
281
|
+
events {
|
282
|
+
event :slow, :green => :yellow
|
283
|
+
}
|
284
|
+
|
285
|
+
callbacks {
|
286
|
+
on_enter(:yellow) { |e| evt = e }
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
expect(fsm.current).to eql(:green)
|
291
|
+
fsm.slow
|
292
|
+
expect(fsm.current).to eql(:yellow)
|
293
|
+
|
294
|
+
expect(evt.from).to eql(:green)
|
295
|
+
expect(evt.to).to eql(:yellow)
|
296
|
+
expect(evt.name).to eql(:slow)
|
297
|
+
end
|
298
|
+
|
299
|
+
it "passes extra parameters to callbacks" do
|
300
|
+
expected = {name: :init, from: :none, to: :green, a: nil, b: nil, c: nil }
|
301
|
+
|
302
|
+
callback = Proc.new { |event, a, b, c|
|
303
|
+
expect(event.from).to eql(expected[:from])
|
304
|
+
expect(event.to).to eql(expected[:to])
|
305
|
+
expect(event.name).to eql(expected[:name])
|
306
|
+
expect(a).to eql(expected[:a])
|
307
|
+
expect(b).to eql(expected[:b])
|
308
|
+
expect(c).to eql(expected[:c])
|
309
|
+
}
|
310
|
+
|
311
|
+
fsm = FiniteMachine.define do
|
312
|
+
initial :green
|
313
|
+
|
314
|
+
events {
|
315
|
+
event :slow, :green => :yellow
|
316
|
+
event :stop, :yellow => :red
|
317
|
+
event :ready, :red => :yellow
|
318
|
+
event :go, :yellow => :green
|
319
|
+
}
|
320
|
+
|
321
|
+
callbacks {
|
322
|
+
# generic callbacks
|
323
|
+
on_enter &callback
|
324
|
+
on_transition &callback
|
325
|
+
on_exit &callback
|
326
|
+
|
327
|
+
# state callbacks
|
328
|
+
on_enter :green, &callback
|
329
|
+
on_enter :yellow, &callback
|
330
|
+
on_enter :red, &callback
|
331
|
+
|
332
|
+
on_transition :green , &callback
|
333
|
+
on_transition :yellow, &callback
|
334
|
+
on_transition :red , &callback
|
335
|
+
|
336
|
+
on_exit :green , &callback
|
337
|
+
on_exit :yellow, &callback
|
338
|
+
on_exit :red , &callback
|
339
|
+
|
340
|
+
# event callbacks
|
341
|
+
on_enter :slow , &callback
|
342
|
+
on_enter :stop , &callback
|
343
|
+
on_enter :ready, &callback
|
344
|
+
on_enter :go , &callback
|
345
|
+
|
346
|
+
on_transition :slow , &callback
|
347
|
+
on_transition :stop , &callback
|
348
|
+
on_transition :ready, &callback
|
349
|
+
on_transition :go , &callback
|
350
|
+
|
351
|
+
on_exit :slow , &callback
|
352
|
+
on_exit :stop , &callback
|
353
|
+
on_exit :ready, &callback
|
354
|
+
on_exit :go , &callback
|
355
|
+
}
|
356
|
+
end
|
357
|
+
|
358
|
+
|
359
|
+
expected = {name: :slow, from: :green, to: :yellow, a: 1, b: 2, c: 3}
|
360
|
+
fsm.slow(1, 2, 3)
|
361
|
+
|
362
|
+
expected = {name: :stop, from: :yellow, to: :red, a: 'foo', b: 'bar'}
|
363
|
+
fsm.stop('foo', 'bar')
|
364
|
+
|
365
|
+
expected = {name: :ready, from: :red, to: :yellow, a: :foo, b: :bar}
|
366
|
+
fsm.ready(:foo, :bar)
|
367
|
+
|
368
|
+
expected = {name: :go, from: :yellow, to: :green, a: nil, b: nil}
|
369
|
+
fsm.go(nil, nil)
|
370
|
+
end
|
371
|
+
|
372
|
+
it "raises an error with invalid callback name" do
|
373
|
+
expect {
|
374
|
+
FiniteMachine.define do
|
375
|
+
initial :green
|
376
|
+
|
377
|
+
events {
|
378
|
+
event :slow, :green => :yellow
|
379
|
+
}
|
380
|
+
|
381
|
+
callbacks {
|
382
|
+
on_enter(:magic) { |event| called << 'on_enter'}
|
383
|
+
}
|
384
|
+
end
|
385
|
+
}.to raise_error(FiniteMachine::InvalidCallbackNameError, /magic is not a valid callback name/)
|
386
|
+
end
|
387
|
+
|
388
|
+
xit "propagates exceptions raised inside callback"
|
389
|
+
|
390
|
+
xit "executes callbacks with multiple 'from' transitions"
|
391
|
+
end
|