motion-state-machine 0.8.1

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.
@@ -0,0 +1,3 @@
1
+ module StateMachine
2
+ VERSION = "0.8.1"
3
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/motion-state-machine/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Sebastian Burkhart"]
6
+ gem.email = ["sebastianburkhart@me.com"]
7
+ gem.description = %q{A finite state machine for RubyMotion with a flavor of Grand Central Dispatch.}
8
+ gem.summary = %q{Comes with a nice syntax for state and transition definition. Supports triggering via events, timeouts and NSNotifications.}
9
+ gem.homepage = "https://github.com/opyh/motion-state-machine"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "motion-state-machine"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = StateMachine::VERSION
17
+
18
+ gem.add_development_dependency 'rake'
19
+ end
@@ -0,0 +1,118 @@
1
+ describe StateMachine::Base do
2
+
3
+ describe "#initialize" do
4
+
5
+ it "should raise if not given a start state" do
6
+ lambda {state_machine = StateMachine::Base.new}.
7
+ should.raise(ArgumentError)
8
+ end
9
+
10
+ describe "when given a start state" do
11
+ before do
12
+ @fsm = StateMachine::Base.new start_state: :start
13
+ end
14
+
15
+ it "should initialize and use the internal state dictionary" do
16
+ dictionary = @fsm.instance_variable_get(:@state_symbols_to_states)
17
+ dictionary.class.should == Hash
18
+ end
19
+
20
+ it "should create an internal waiting_for_start state" do
21
+ dictionary = @fsm.instance_variable_get(:@state_symbols_to_states)
22
+ dictionary.count.should == 2
23
+ state = dictionary[:waiting_for_start]
24
+ state.class.should == StateMachine::State
25
+ state.transition_map.count.should == 1
26
+ transition = state.transition_map[:on][:start].first
27
+ transition.options[:from].should == :waiting_for_start
28
+ transition.options[:to].should ==:start
29
+ end
30
+
31
+ it "should set its current state to :waiting_for_start" do
32
+ state = @fsm.current_state
33
+ state.class.should == StateMachine::State
34
+ state.symbol.should == :waiting_for_start
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ describe "after correct initialization" do
41
+
42
+ before do
43
+ @fsm = StateMachine::Base.new start_state: :awake
44
+ @fsm.when(:awake) do |state|
45
+ state.die :on => :terminate
46
+ end
47
+ end
48
+
49
+ describe "#state(symbol, name = nil)" do
50
+ it "should create & return a new state for the given symbol if not existing" do
51
+ states_hash = @fsm.instance_variable_get(:@state_symbols_to_states)
52
+ state_count_before = states_hash.count
53
+ states_hash.has_key?(:some_other_state).should == false
54
+ state = @fsm.state(:some_other_state, "Fake State")
55
+ state.class.should == StateMachine::State
56
+ states_hash.count.should == state_count_before + 1
57
+ states_hash.has_key?(:some_other_state).should == true
58
+ state.name.should == "Fake State"
59
+ end
60
+
61
+ it "should return the state with the given symbol if existing" do
62
+ states_hash = @fsm.instance_variable_get(:@state_symbols_to_states)
63
+ state_count_before = states_hash.count
64
+ states_hash[:foo] = "bar"
65
+ states_hash.count.should == state_count_before + 1
66
+ @fsm.state(:foo).should == "bar"
67
+ states_hash.count.should == state_count_before + 1
68
+ end
69
+ end
70
+
71
+ describe "#start!" do
72
+ it "should remember the initial queue if called from main queue" do
73
+ Dispatch::Queue.current.to_s.should == Dispatch::Queue.main.to_s
74
+ @fsm.start!
75
+ @fsm.initial_queue.to_s.should == Dispatch::Queue.main.to_s
76
+ end
77
+
78
+ it "should remember the initial queue if called from another queue" do
79
+ other_queue = Dispatch::Queue.concurrent(:default)
80
+ other_queue.to_s.should != Dispatch::Queue.main.to_s
81
+ other_queue.sync do
82
+ @fsm.start!
83
+ end
84
+ @fsm.initial_queue.to_s.should == other_queue.to_s
85
+ end
86
+
87
+ it "should change the current state to the start state" do
88
+ @fsm.current_state.symbol.should == :waiting_for_start
89
+ @fsm.start!
90
+ @fsm.current_state.symbol.should == :awake
91
+ end
92
+ end
93
+
94
+ describe "#terminated?" do
95
+ it "should return false until the machine is terminated" do
96
+ @fsm.current_state.symbol.should == :waiting_for_start
97
+ @fsm.terminated?.should == false
98
+ @fsm.start!
99
+ @fsm.current_state.symbol.should == :awake
100
+ @fsm.terminated?.should == false
101
+ @fsm.event(:terminate)
102
+ @fsm.terminated?.should == true
103
+ end
104
+ end
105
+
106
+ # describe "#stop_and_cleanup" do
107
+ # it "should unregister all notifications"
108
+ # it "should remove references to self from all registered states"
109
+ # end
110
+
111
+ # it "should not be too slow"
112
+ #
113
+ # it "should be thread-safe"
114
+
115
+ end
116
+
117
+
118
+ end
@@ -0,0 +1,74 @@
1
+ # Benchmark that checks if the machine is fast enough.
2
+ # If it should ever happen that some implementation change
3
+ # slows it down, this spec will be red.
4
+
5
+ # Test state machine that loops in 3 states.
6
+
7
+ class LoopingThreeStateMachine < StateMachine::Base
8
+ attr_accessor :steps, :loops
9
+ attr_accessor :is_dead
10
+
11
+ def initialize
12
+ super(start_state: :first_state)
13
+
14
+ @steps = 0
15
+ @loops = 0
16
+
17
+ self.when :first_state do |state|
18
+ state.transition_to :second_state, on: :next,
19
+ action: proc { @steps += 1 }
20
+ end
21
+
22
+ self.when :second_state do |state|
23
+ state.transition_to :third_state, on: :next,
24
+ action: proc { @steps += 1 }
25
+ end
26
+
27
+ self.when :third_state do |state|
28
+ state.transition_to :first_state, on: :next,
29
+ action: proc { @steps += 1; @loops += 1 }
30
+ end
31
+
32
+ self
33
+ end
34
+ end
35
+
36
+ describe LoopingThreeStateMachine do
37
+ before do
38
+ @fsm = LoopingThreeStateMachine.new
39
+ end
40
+
41
+ it "should loop correctly" do
42
+ @fsm.start!
43
+ 100.times { @fsm.event :next }
44
+ @fsm.steps.should == 100
45
+ @fsm.loops.should == @fsm.steps / 3
46
+ end
47
+
48
+ it "should prove that the state machine can handle more than 10k events per second" do
49
+ other_queue = Dispatch::Queue.new('org.screenfashion.motion-state-machine')
50
+ other_queue.sync { @fsm.start! }
51
+ started_on = NSDate.date
52
+ dispatch_group = Dispatch::Group.new
53
+ event_count = 100000
54
+
55
+ event_count.times do |i|
56
+ other_queue.async(dispatch_group) { @fsm.event :next }
57
+ end
58
+
59
+ send_time = NSDate.date.timeIntervalSinceDate started_on
60
+
61
+ dispatch_group.wait # wait for the events to be handled
62
+
63
+ handle_time = NSDate.date.timeIntervalSinceDate started_on
64
+
65
+ send_time.should < 0.2
66
+
67
+ frequency = event_count / handle_time
68
+ frequency.should > 10000
69
+
70
+ puts "\nNeeded #{send_time}s to send #{event_count} events, #{handle_time}s to handle them."
71
+ puts "That's a frequency of #{frequency} state changes per second.\n"
72
+ end
73
+
74
+ end
@@ -0,0 +1,57 @@
1
+ describe StateMachine::NotificationTransition do
2
+
3
+ before do
4
+ @state_machine = StateMachine::Base.new start_state: :awaiting_notification
5
+ action = proc {@fired = true}
6
+ @state_machine.when :awaiting_notification do |state|
7
+ @transition = state.transition_to(:notified, on_notification: "SomeNotification", action: action).first
8
+ state.transition_to :canceled, on: :cancel
9
+ end
10
+ end
11
+
12
+ it "should be created correctly" do
13
+ @transition.should.is_a(StateMachine::NotificationTransition)
14
+ end
15
+
16
+ describe "when running in main queue" do
17
+ it "should be executed when receiving the notification and unarm correctly" do
18
+ @fired = false
19
+ @state_machine.start! # will arm the transition
20
+ @state_machine.current_state.symbol.should == :awaiting_notification
21
+ @fired.should == false
22
+
23
+ NSNotificationCenter.defaultCenter.postNotificationName "SomeNotification", object: nil
24
+ @state_machine.current_state.symbol.should == :notified
25
+ @fired.should == true
26
+ @fired = false
27
+
28
+ NSNotificationCenter.defaultCenter.postNotificationName "SomeNotification", object: nil
29
+ @state_machine.current_state.symbol.should == :notified
30
+ @fired.should == false
31
+ end
32
+ end
33
+
34
+ describe "when running in other queue" do
35
+ it "should be executed when receiving the notification and unarm correctly" do
36
+ @fired = false
37
+
38
+ other_queue = Dispatch::Queue.concurrent
39
+ other_queue.sync do
40
+ @state_machine.start! # will arm the transition
41
+ end
42
+ @state_machine.current_state.symbol.should == :awaiting_notification
43
+ @fired.should == false
44
+
45
+ NSNotificationCenter.defaultCenter.postNotificationName "SomeNotification", object: nil
46
+ sleep 0.1
47
+ @state_machine.current_state.symbol.should == :notified
48
+ @fired.should == true
49
+ @fired = false
50
+
51
+ NSNotificationCenter.defaultCenter.postNotificationName "SomeNotification", object: nil
52
+ @state_machine.current_state.symbol.should == :notified
53
+ @fired.should == false
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,53 @@
1
+ describe StateMachine::SendEventTransition do
2
+ before do
3
+ @state_machine = StateMachine::Base.new start_state: :awake
4
+ @source_state = @state_machine.state :awake
5
+ @destination_state = @state_machine.state :tired
6
+ @options = {
7
+ state_machine: @state_machine,
8
+ from: :awake,
9
+ to: :tired,
10
+ type: :on,
11
+ on: :work_done
12
+ }
13
+ @transition = StateMachine::Transition.make @options
14
+ end
15
+
16
+ it "should correctly register in the factory" do
17
+ @transition.should.is_a(StateMachine::SendEventTransition)
18
+ end
19
+
20
+ describe "#initialize(options)" do
21
+ it "should not arm the transition" do
22
+ @state_machine.event(:work_done)
23
+ @state_machine.current_state.symbol.should == :waiting_for_start
24
+ end
25
+ end
26
+
27
+ describe "#arm" do
28
+ it "should execute the transition when the event is sent to the state machine" do
29
+ @state_machine.start!
30
+ @state_machine.current_state.symbol.should == :awake
31
+ @state_machine.event(:work_done)
32
+ @state_machine.current_state.symbol.should == :awake
33
+ @transition.arm
34
+ # necessary for the event to work
35
+ @source_state.register(@transition)
36
+ @state_machine.event(:work_done)
37
+ @state_machine.current_state.symbol.should == :tired
38
+ end
39
+ end
40
+
41
+ describe "#unarm" do
42
+ it "should make sure the transition is not executed when the event is sent to the state machine" do
43
+ transition = StateMachine::Transition.make @options
44
+ @state_machine.start!
45
+ @state_machine.current_state.symbol.should == :awake
46
+ @transition.unarm
47
+ @source_state.register(@transition)
48
+ @state_machine.event(:work_done)
49
+ @state_machine.current_state.symbol.should == :awake
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,219 @@
1
+ describe StateMachine::State do
2
+ before do
3
+ @living = StateMachine::State.new "stub", symbol: :awake, name: "Living"
4
+ end
5
+
6
+ describe "Entering/exiting" do
7
+ before do
8
+ # These seem not to be correctly deinitialized after single specs,
9
+ # so we have to reinitialize them ourselves.
10
+
11
+ @entry_action_called = false
12
+ @other_entry_action_called = false
13
+ @exit_action_called = false
14
+ @other_exit_action_called = false
15
+
16
+ @state_machine = StateMachine::Base.new start_state: :awake
17
+ @state_machine.when(:awake) do |state|
18
+ state.transition_to :tired, on: :work_done
19
+ state.on_exit do
20
+ @exit_action_called = true
21
+ end
22
+ state.on_exit do
23
+ @other_exit_action_called = true
24
+ end
25
+ end
26
+ @state_machine.when(:tired) do |state|
27
+ state.transition_to :very_excited, on: :something_happened
28
+ state.on_entry do
29
+ @entry_action_called = true
30
+ end
31
+ state.on_entry do
32
+ @other_entry_action_called = true
33
+ end
34
+ end
35
+
36
+ @state_machine.start!
37
+ @state_machine.current_state.symbol.should == :awake
38
+ end
39
+
40
+ describe "#enter!" do
41
+ it "should set the state machine's current state" do
42
+ @state_machine.state(:tired).send :enter!
43
+ @state_machine.current_state.symbol.should == :tired
44
+ end
45
+
46
+ it "should execute the entry actions" do
47
+ @entry_action_called.should == false
48
+ @other_entry_action_called.should == false
49
+ @state_machine.event(:work_done)
50
+ @entry_action_called.should == true
51
+ @other_entry_action_called.should == true
52
+ end
53
+
54
+ it "should arm the transitions" do
55
+ @state_machine.event(:something_happened) # should be ignored
56
+ @state_machine.current_state.symbol.should == :awake
57
+ @state_machine.event(:work_done)
58
+ @state_machine.current_state.symbol.should == :tired
59
+ @state_machine.event(:something_happened)
60
+ @state_machine.current_state.symbol.should == :very_excited
61
+ end
62
+
63
+ end
64
+
65
+ describe "#exit!" do
66
+ it "should set the state machine's current state to nil" do
67
+ @state_machine.state(:awake).send :exit!
68
+ @state_machine.current_state.should == nil
69
+ end
70
+
71
+ it "should execute the exit actions" do
72
+ @exit_action_called.should == false
73
+ @other_exit_action_called.should == false
74
+ @state_machine.event(:work_done)
75
+ @exit_action_called.should == true
76
+ @other_exit_action_called.should == true
77
+ end
78
+
79
+ it "should unarm the transitions" do
80
+ @state_machine.state(:awake).send :exit!
81
+ @state_machine.event(:work_done) # should be ignored
82
+ @state_machine.current_state.should == nil
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ describe "#guarded_execute(options)" do
89
+ before do
90
+ @state_machine = StateMachine::Base.new start_state: :awake
91
+ end
92
+ it "should not do anything when on a terminating state" do
93
+ @state_machine.when :awake do |state|
94
+ state.die on: :kill
95
+ end
96
+ @state_machine.start!
97
+ @state_machine.current_state.symbol.should == :awake
98
+ @state_machine.current_state.send :guarded_execute, :on, :kill
99
+ @state_machine.current_state.should.be.terminating
100
+ end
101
+
102
+ describe ":if and :unless guards" do
103
+
104
+ # if/unless logic table is tested in transition specs
105
+
106
+ it "should raise if multiple non-guarded transitions would be possible for the same event" do
107
+ @state_machine.when :awake do |state|
108
+ state.transition_to :state2, on: :work_done
109
+ state.transition_to :state3, on: :work_done
110
+ end
111
+ @state_machine.start!
112
+ lambda {@state_machine.current_state.send :guarded_execute, :on, :work_done}.should.raise RuntimeError
113
+ end
114
+
115
+ it "should raise if multiple guarded transitions would be possible for the same event" do
116
+ @state_machine.when :awake do |state|
117
+ state.transition_to :state2, on: :work_done, :if => proc { true }
118
+ state.transition_to :state3, on: :work_done, :if => proc { true }
119
+ end
120
+ @state_machine.start!
121
+ lambda {@state_machine.current_state.send :guarded_execute, :on, :work_done}.should.raise RuntimeError
122
+ end
123
+
124
+ it "should execute right transition of multiple are allowed" do
125
+ @state_machine.when :awake do |state|
126
+ state.transition_to :state2, on: :work_done, :if => proc { false }
127
+ state.transition_to :state3, on: :work_done, :if => proc { true }
128
+ end
129
+ @state_machine.start!
130
+ @state_machine.current_state.send :guarded_execute, :on, :work_done
131
+ @state_machine.current_state.symbol.should == :state3
132
+ end
133
+
134
+ it "should not execute any transition if all are disallowed" do
135
+ @state_machine.when :awake do |state|
136
+ state.transition_to :state2, on: :work_done, :if => proc { false }
137
+ state.transition_to :state3, on: :work_done, :if => proc { false }
138
+ end
139
+ @state_machine.start!
140
+ @state_machine.current_state.send :guarded_execute, :on, :work_done
141
+ @state_machine.current_state.symbol.should == :awake
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ describe "Definition DSL" do
149
+ before do
150
+ @state_machine = StateMachine::Base.new start_state: :awake
151
+ end
152
+
153
+ describe "#transition_to" do
154
+ it "should return an array of created transitions" do
155
+ @state_machine.when(:awake) do |state|
156
+ transitions = state.transition_to(:tired, after: 42)
157
+ transitions.class.should == Array
158
+ transitions.first.class.should == StateMachine::TimedTransition
159
+ end
160
+ end
161
+
162
+ describe "Argument error handling" do
163
+ it "should raise if not given a destination state symbol" do
164
+ proc do
165
+ @state_machine.when(:awake) do |state|
166
+ state.transition_to Hash.new, on: :eat
167
+ end
168
+ end.should.raise ArgumentError, /No destination state given/
169
+ end
170
+
171
+ it "should raise if not given a trigger event" do
172
+ proc do
173
+ @state_machine.when(:awake) do |state|
174
+ state.transition_to :sleepy
175
+ end
176
+ end.should.raise ArgumentError, /No trigger event/
177
+ end
178
+
179
+ it "should create the destination state if not existent" do
180
+ created = proc {@state_machine.states.collect(&:symbol).include?(:sleepy)}
181
+ created.call.should == false
182
+ @state_machine.when(:awake) do |state|
183
+ state.transition_to :sleepy, on: :hard_work_done
184
+ end
185
+ created.call.should == true
186
+ end
187
+ end
188
+ end
189
+
190
+ describe "#die" do
191
+ it "should return an array of created transitions" do
192
+ @state_machine.when(:awake) do |state|
193
+ transitions = state.die(on: :eat)
194
+ transitions.class.should == Array
195
+ transitions.first.class.should == StateMachine::SendEventTransition
196
+ end
197
+ end
198
+
199
+ it "should create termination states with different symbols" do
200
+ @state_machine.when(:awake) do |state|
201
+ # so many options!
202
+ transitions1 = state.die on: :suffocation
203
+ transitions2 = state.die on: :starving
204
+ transitions1.first.destination_state.symbol.should != transitions2.first.destination_state.symbol
205
+ end
206
+ end
207
+
208
+ it "should set the termination states' terminating flags" do
209
+ @state_machine.when(:awake) do |state|
210
+ non_terminating_transitions = state.transition_to :tired, on: :work_done
211
+ terminating_transitions = state.die on: :suffocation
212
+ non_terminating_transitions.first.destination_state.should.not.be.terminating
213
+ terminating_transitions.first.destination_state.should.be.terminating
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ end