motion-state-machine 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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