state-fu 0.11.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.
Files changed (85) hide show
  1. data/LICENSE +40 -0
  2. data/README.textile +293 -0
  3. data/Rakefile +114 -0
  4. data/lib/binding.rb +292 -0
  5. data/lib/event.rb +192 -0
  6. data/lib/executioner.rb +120 -0
  7. data/lib/hooks.rb +39 -0
  8. data/lib/interface.rb +132 -0
  9. data/lib/lathe.rb +538 -0
  10. data/lib/machine.rb +184 -0
  11. data/lib/method_factory.rb +243 -0
  12. data/lib/persistence.rb +116 -0
  13. data/lib/persistence/active_record.rb +34 -0
  14. data/lib/persistence/attribute.rb +47 -0
  15. data/lib/persistence/base.rb +100 -0
  16. data/lib/persistence/relaxdb.rb +23 -0
  17. data/lib/persistence/session.rb +7 -0
  18. data/lib/sprocket.rb +58 -0
  19. data/lib/state-fu.rb +56 -0
  20. data/lib/state.rb +48 -0
  21. data/lib/support/active_support_lite/array.rb +9 -0
  22. data/lib/support/active_support_lite/array/access.rb +60 -0
  23. data/lib/support/active_support_lite/array/conversions.rb +202 -0
  24. data/lib/support/active_support_lite/array/extract_options.rb +21 -0
  25. data/lib/support/active_support_lite/array/grouping.rb +109 -0
  26. data/lib/support/active_support_lite/array/random_access.rb +13 -0
  27. data/lib/support/active_support_lite/array/wrapper.rb +25 -0
  28. data/lib/support/active_support_lite/blank.rb +67 -0
  29. data/lib/support/active_support_lite/cattr_reader.rb +57 -0
  30. data/lib/support/active_support_lite/keys.rb +57 -0
  31. data/lib/support/active_support_lite/misc.rb +59 -0
  32. data/lib/support/active_support_lite/module.rb +1 -0
  33. data/lib/support/active_support_lite/module/delegation.rb +130 -0
  34. data/lib/support/active_support_lite/object.rb +9 -0
  35. data/lib/support/active_support_lite/string.rb +38 -0
  36. data/lib/support/active_support_lite/symbol.rb +16 -0
  37. data/lib/support/applicable.rb +41 -0
  38. data/lib/support/arrays.rb +197 -0
  39. data/lib/support/core_ext.rb +90 -0
  40. data/lib/support/exceptions.rb +106 -0
  41. data/lib/support/has_options.rb +16 -0
  42. data/lib/support/logger.rb +165 -0
  43. data/lib/support/methodical.rb +17 -0
  44. data/lib/support/no_stdout.rb +55 -0
  45. data/lib/support/plotter.rb +62 -0
  46. data/lib/support/vizier.rb +300 -0
  47. data/lib/tasks/spec_last.rake +55 -0
  48. data/lib/tasks/state_fu.rake +57 -0
  49. data/lib/transition.rb +338 -0
  50. data/lib/transition_query.rb +224 -0
  51. data/spec/custom_formatter.rb +49 -0
  52. data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
  53. data/spec/features/method_missing_only_once_spec.rb +28 -0
  54. data/spec/features/not_requirements_spec.rb +118 -0
  55. data/spec/features/plotter_spec.rb +97 -0
  56. data/spec/features/shared_log_spec.rb +7 -0
  57. data/spec/features/singleton_machine_spec.rb +39 -0
  58. data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
  59. data/spec/features/transition_boolean_comparison_spec.rb +101 -0
  60. data/spec/helper.rb +13 -0
  61. data/spec/integration/active_record_persistence_spec.rb +202 -0
  62. data/spec/integration/binding_extension_spec.rb +41 -0
  63. data/spec/integration/class_accessor_spec.rb +117 -0
  64. data/spec/integration/event_definition_spec.rb +74 -0
  65. data/spec/integration/example_01_document_spec.rb +133 -0
  66. data/spec/integration/example_02_string_spec.rb +88 -0
  67. data/spec/integration/instance_accessor_spec.rb +97 -0
  68. data/spec/integration/lathe_extension_spec.rb +67 -0
  69. data/spec/integration/machine_duplication_spec.rb +101 -0
  70. data/spec/integration/relaxdb_persistence_spec.rb +97 -0
  71. data/spec/integration/requirement_reflection_spec.rb +270 -0
  72. data/spec/integration/state_definition_spec.rb +163 -0
  73. data/spec/integration/transition_spec.rb +1033 -0
  74. data/spec/spec.opts +9 -0
  75. data/spec/spec_helper.rb +132 -0
  76. data/spec/state_fu_spec.rb +948 -0
  77. data/spec/units/binding_spec.rb +192 -0
  78. data/spec/units/event_spec.rb +214 -0
  79. data/spec/units/exceptions_spec.rb +82 -0
  80. data/spec/units/lathe_spec.rb +570 -0
  81. data/spec/units/machine_spec.rb +229 -0
  82. data/spec/units/method_factory_spec.rb +366 -0
  83. data/spec/units/sprocket_spec.rb +69 -0
  84. data/spec/units/state_spec.rb +59 -0
  85. metadata +171 -0
@@ -0,0 +1,163 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../helper")
2
+
3
+ ##
4
+ ##
5
+ ##
6
+
7
+ describe "Adding states to a Machine" do
8
+
9
+ include MySpecHelper
10
+
11
+ before(:each) do
12
+ make_pristine_class 'Klass'
13
+ @k = Klass.new()
14
+ end
15
+
16
+ it "should allow me to call machine() { state(:egg) }" do
17
+ lambda {Klass.state_fu_machine(){ state :egg } }.should_not raise_error()
18
+ end
19
+
20
+ describe "having called machine() { state(:egg) }" do
21
+
22
+ before(:each) do
23
+ Klass.state_fu_machine(){ state :egg }
24
+ end
25
+
26
+ it "should return [:egg] given machine.state_names" do
27
+ Klass.state_fu_machine.should respond_to(:state_names)
28
+ Klass.state_fu_machine.state_names.should == [:egg]
29
+ end
30
+
31
+ it "should return [<StateFu::State @name=:egg>] given machine.states" do
32
+ Klass.state_fu_machine.should respond_to(:states)
33
+ Klass.state_fu_machine.states.length.should == 1
34
+ Klass.state_fu_machine.states.first.should be_kind_of( StateFu::State )
35
+ Klass.state_fu_machine.states.first.name.should == :egg
36
+ end
37
+
38
+ it "should return :egg given machine.states.first.name" do
39
+ Klass.state_fu_machine.should respond_to(:states)
40
+ Klass.state_fu_machine.states.length.should == 1
41
+ Klass.state_fu_machine.states.first.should respond_to(:name)
42
+ Klass.state_fu_machine.states.first.name.should == :egg
43
+ end
44
+
45
+ it "should return a <StateFu::State @name=:egg> given machine.states[:egg]" do
46
+ Klass.state_fu_machine.should respond_to(:states)
47
+ result = Klass.state_fu_machine.states[:egg]
48
+ result.should_not be_nil
49
+ result.should be_kind_of( StateFu::State )
50
+ result.name.should == :egg
51
+ end
52
+
53
+
54
+ it "should allow me to call machine(){ state(:chick) }" do
55
+ lambda {Klass.state_fu_machine(){ state :chick } }.should_not raise_error()
56
+ end
57
+
58
+ describe "having called machine() { state(:chick) }" do
59
+ before do
60
+ Klass.state_fu_machine() { state :chick }
61
+ end
62
+
63
+ it "should return [:egg] given machine.state_names" do
64
+ Klass.state_fu_machine.should respond_to(:state_names)
65
+ Klass.state_fu_machine.state_names.should == [:egg, :chick]
66
+ end
67
+
68
+ it "should return a <StateFu::State @name=:chick> given machine.states[:egg]" do
69
+ Klass.state_fu_machine.should respond_to(:states)
70
+ result = Klass.state_fu_machine.states[:chick]
71
+ result.should_not be_nil
72
+ result.should be_kind_of( StateFu::State )
73
+ result.name.should == :chick
74
+ end
75
+
76
+ end
77
+
78
+ describe "calling machine() { state(:bird) {|s| .. } }" do
79
+
80
+ it "should yield the state to the block as |s|" do
81
+ state = nil
82
+ Klass.state_fu_machine() do
83
+ state(:bird) do |s|
84
+ state = s
85
+ end
86
+ end
87
+ state.should be_kind_of( StateFu::State )
88
+ state.name.should == :bird
89
+ end
90
+
91
+ end
92
+
93
+ describe "calling machine() { state(:bird) { ... } }" do
94
+
95
+ it "should instance_eval the block as a StateFu::Lathe" do
96
+ lathe = nil
97
+ Klass.state_fu_machine() do
98
+ state(:bird) do
99
+ lathe = self
100
+ end
101
+ end
102
+ lathe.should be_kind_of(StateFu::Lathe)
103
+ lathe.state_or_event.should be_kind_of(StateFu::State)
104
+ lathe.state_or_event.name.should == :bird
105
+ end
106
+
107
+ end
108
+
109
+ describe "calling state(:bird) consecutive times" do
110
+
111
+ it "should yield the same state each time" do
112
+ Klass.state_fu_machine() { state :bird }
113
+ bird_1 = Klass.state_fu_machine.states[:bird]
114
+ Klass.state_fu_machine() { state :bird }
115
+ bird_2 = Klass.state_fu_machine.states[:bird]
116
+ bird_1.should == bird_2
117
+ end
118
+
119
+ end
120
+ end
121
+
122
+ describe "calling machine() { states(:egg, :chick, :bird, :poultry => true) }" do
123
+
124
+ it "should create 3 states" do
125
+ Klass.state_fu_machine().should be_empty
126
+ Klass.state_fu_machine() { states(:egg, :chick, :bird, :poultry => true) }
127
+ Klass.state_fu_machine().state_names().should == [:egg, :chick, :bird]
128
+ Klass.state_fu_machine().states.length.should == 3
129
+ Klass.state_fu_machine().states.map(&:name).should == [:egg, :chick, :bird]
130
+ Klass.state_fu_machine().states().each do |s|
131
+ s.options[:poultry].should be_true
132
+ s.should be_kind_of(StateFu::State)
133
+ end
134
+ end
135
+
136
+ describe "merging options" do
137
+ before do
138
+ make_pristine_class('Klass')
139
+ end
140
+ it "should merge options when states are mentioned more than once" do
141
+ # reset!
142
+ machine = Klass.state_fu_machine
143
+ machine.states.length.should == 0
144
+ Klass.state_fu_machine() { states(:egg, :chick, :bird, :poultry => true) }
145
+ machine = Klass.state_fu_machine
146
+ machine.states.length.should == 3
147
+
148
+ # make sure they're the same states
149
+ states_1 = machine.states
150
+ Klass.state_fu_machine(){ states( :egg, :chick, :bird, :covering => 'feathers')}
151
+ states_1.should == machine.states
152
+
153
+ # ensure options were merged
154
+ machine.states().each do |s|
155
+ s.options[:poultry].should be_true
156
+ s.options[:covering].should == 'feathers'
157
+ s.should be_kind_of(StateFu::State)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
@@ -0,0 +1,1033 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../helper")
2
+
3
+ # TODO - refactor me into manageable chunks
4
+
5
+ describe StateFu::Transition do
6
+ include MySpecHelper
7
+ before do
8
+ reset!
9
+ make_pristine_class("Klass")
10
+ end
11
+
12
+ describe "transition args / options" do
13
+ before do
14
+ make_pristine_class('Alphabet') do
15
+ machine do
16
+ connect_states :a, :b
17
+ end
18
+ end
19
+ @abc = Alphabet.new
20
+ evt = Alphabet.machine.events[:a_to_b]
21
+ tgt = Alphabet.machine.states[:b]
22
+ @t = StateFu::Transition.new(@abc.stfu, evt, tgt,
23
+ :a, :b, 'c' => 'cat')
24
+ end
25
+
26
+ it "should behave like this" do
27
+ @t.args.should == [:a, :b, {'c' => 'cat'}]
28
+ @t.options.should == {:c => 'cat'}
29
+
30
+ @t.apply!({'d' => :e})
31
+ @t.options.should == {:c => 'cat', :d => :e}
32
+
33
+ @t.args.should == [:a, :b, {'c' => 'cat'}]
34
+
35
+ @t.args = [:A, :B]
36
+ @t.args.should == [:A, :B]
37
+ @t.options.should == {:c => 'cat', :d => :e}
38
+
39
+ @t.args = [:X, :Y, {:scale => :metric }]
40
+
41
+ @t.options.should == { :c => 'cat', :d => :e , :scale => :metric }
42
+ @t.args.options.should == @t.options
43
+ end
44
+ end
45
+
46
+ #
47
+ #
48
+ #
49
+
50
+ describe "A simple machine with 2 states and a single event" do
51
+ before do
52
+ @machine = Klass.state_fu_machine do
53
+ state :src do
54
+ event :transfer, :to => :dest
55
+ end
56
+ end
57
+ @origin = @machine.states[:src]
58
+ @target = @machine.states[:dest]
59
+ @event = @machine.events.first
60
+ @obj = Klass.new
61
+ end
62
+
63
+ it "should have two states named :src and :dest" do
64
+ @machine.states.length.should == 2
65
+ @machine.states.should == [@origin, @target]
66
+ @origin.name.should == :src
67
+ @target.name.should == :dest
68
+ @machine.state_names.should == [:src, :dest]
69
+ end
70
+
71
+ it "should have one event :transfer, from :src to :dest" do
72
+ @machine.events.length.should == 1
73
+ @event.origin.should == @origin
74
+ @event.target.should == @target
75
+ end
76
+
77
+ describe "instance methods on a transition" do
78
+ before do
79
+ @t = @obj.state_fu.transition( :transfer )
80
+ end
81
+
82
+ describe "the transition before firing" do
83
+ it "should not be fired" do
84
+ @t.should_not be_fired
85
+ end
86
+
87
+ it "should not be halted" do
88
+ @t.should_not be_halted
89
+ end
90
+
91
+ it "should not be accepted" do
92
+ @t.should_not be_accepted
93
+ end
94
+
95
+ it "should have a current_state of the origin state" do
96
+ @t.current_state.should == @origin
97
+ end
98
+
99
+ it "should have a current_hook of nil" do
100
+ @t.current_hook.should == nil
101
+ end
102
+ end # transition before fire!
103
+
104
+ describe "calling fire! on a transition with no conditions or hooks" do
105
+ it "should change the state of the binding" do
106
+ @obj.state_fu.state.should == @origin
107
+ @t.fire!
108
+ @obj.state_fu.state.should == @target
109
+ end
110
+
111
+ it "should have an empty set of hooks" do
112
+ @t.hooks.map(&:last).flatten.should == []
113
+ end
114
+
115
+ it "should change the field when persistence is via an attribute" do
116
+ @obj.state_fu.persister.should be_kind_of( StateFu::Persistence::Attribute )
117
+ @obj.state_fu.persister.field_name.to_s.should == StateFu::DEFAULT_FIELD.to_s
118
+ @obj.send( :state_fu_field ).should == "src"
119
+ @t.fire!
120
+ @obj.send( :state_fu_field ).should == "dest"
121
+ end
122
+ end # transition.fire!
123
+
124
+ describe "the transition after firing is complete" do
125
+ before do
126
+ @t.fire!()
127
+ end
128
+
129
+ it "should be fired" do
130
+ @t.should be_fired
131
+ end
132
+
133
+ it "should not be halted" do
134
+ @t.should_not be_halted
135
+ end
136
+
137
+ it "should be accepted" do
138
+ @t.should be_accepted
139
+ end
140
+
141
+ it "should have a current_state of the target state" do
142
+ @t.current_state.should == @target
143
+ end
144
+
145
+ it "should have a current_hook && current_hook_slot of nil" do
146
+ @t.current_hook.should == nil
147
+ @t.current_hook_slot.should == nil
148
+ end
149
+ end # transition after fire
150
+ end # transition instance methods
151
+
152
+ # binding instance methods
153
+ # TODO move these to binding spec
154
+ describe "instance methods on the binding" do
155
+ describe "constructing a new transition with state_fu.transition" do
156
+
157
+ it "should raise an ArgumentError if a bad event name is given" do
158
+ lambda do
159
+ trans = @obj.state_fu.transition( :transfibrillate )
160
+ end.should raise_error( ArgumentError )
161
+ end
162
+
163
+ it "should create a new transition given an event_name" do
164
+ trans = @obj.state_fu.transition( :transfer )
165
+ trans.should be_kind_of( StateFu::Transition )
166
+ trans.binding.should == @obj.state_fu
167
+ trans.object.should == @obj
168
+ trans.origin.should == @origin
169
+ trans.target.should == @target
170
+ trans.options.should == {}
171
+ trans.errors.should == []
172
+ trans.args.should == []
173
+ end
174
+
175
+ it "should create a new transition given a StateFu::Event" do
176
+ e = @obj.state_fu.machine.events.first
177
+ e.name.should == :transfer
178
+ trans = @obj.state_fu.transition( e )
179
+ trans.should be_kind_of( StateFu::Transition )
180
+ trans.binding.should == @obj.state_fu
181
+ trans.object.should == @obj
182
+ trans.origin.should == @origin
183
+ trans.target.should == @target
184
+ trans.options.should == {}
185
+ trans.errors.should == []
186
+ trans.args.should == []
187
+ end
188
+
189
+ it "should define any methods declared in a block given to .transition" do
190
+ trans = @obj.state_fu.transition( :transfer ) do
191
+ def snoo
192
+ return [self]
193
+ end
194
+ end
195
+ trans.should be_kind_of( StateFu::Transition )
196
+ trans.should respond_to(:snoo)
197
+ trans.snoo.should == [trans]
198
+ t2 = @obj.state_fu.transition( :transfer )
199
+ t2.should_not respond_to( :snoo)
200
+ end
201
+ end # state_fu.transition
202
+
203
+ describe "state_fu.events" do
204
+ it "should be an array with the only event as its single element" do
205
+ @obj.state_fu.events.should == [@event]
206
+ end
207
+ end
208
+
209
+ describe "state_fu.fire!( :transfer )" do
210
+ it "should change the state when called" do
211
+ @obj.state_fu.should respond_to( :fire_transition! )
212
+ @obj.state_fu.state.should == @origin
213
+ @obj.state_fu.fire_transition!( :transfer )
214
+ @obj.state_fu.state.should == @target
215
+ end
216
+
217
+ it "should return a transition object" do
218
+ @obj.state_fu.fire_transition!( :transfer ).should be_kind_of( StateFu::Transition )
219
+ end
220
+
221
+ end # state_fu.fire!
222
+
223
+ describe "calling cycle!()" do
224
+ it "should raise a TransitionNotFound error" do
225
+ lambda { @obj.state_fu.cycle!() }.should raise_error( StateFu::TransitionNotFound )
226
+ end
227
+ end # cycle!
228
+
229
+ describe "calling next!()" do
230
+ it "should change the state" do
231
+ @obj.state_fu.state.should == @origin
232
+ t = @obj.state_fu.transfer
233
+ t.should be_valid
234
+ @obj.state_fu.valid_transitions.length.should == 1
235
+ @obj.state_fu.next!
236
+ @obj.state_fu.state.should == @target
237
+ end
238
+
239
+ it "should return a transition" do
240
+ trans = @obj.state_fu.next!()
241
+ trans.should be_kind_of( StateFu::Transition )
242
+ end
243
+
244
+ it "should define any methods declared in a block given to .transition" do
245
+ trans = @obj.state_fu.next_transition do
246
+ def snoo
247
+ return [self]
248
+ end
249
+ end
250
+ trans.should be_kind_of( StateFu::Transition )
251
+ # trans.should respond_to(:snoo)
252
+ trans.snoo.should == [trans]
253
+ end
254
+
255
+ it "should raise an error when there is no next state" do
256
+ Klass.state_fu_machine(:noop) {}
257
+ lambda { @obj.noop.next! }.should raise_error( StateFu::TransitionNotFound )
258
+ end
259
+ it "should raise an error when there is more than one next state" do
260
+ Klass.state_fu_machine(:toomany) { event( :go, :from => :one, :to => [:a,:b,:c] ) }
261
+ lambda { @obj.toomany.next! }.should raise_error( StateFu::TransitionNotFound )
262
+ end
263
+ end # next!
264
+
265
+ describe "passing args / options to the transition" do
266
+ before do
267
+ @args = [:a, :b, {:c => :d }]
268
+ end
269
+
270
+ describe "calling transition( :transfer, :a, :b, :c => :d )" do
271
+ it "should set args and options on the transition" do
272
+ t = @obj.state_fu.transition( :transfer, *@args )
273
+ t.args.should == [ :a, :b, {:c => :d} ]
274
+ t.options.should == { :c => :d }
275
+ end
276
+ end
277
+
278
+ describe "calling next!( :a, :b, :c => :d )" do
279
+ it "should set args and options on the transition" do
280
+ t = @obj.state_fu.next!( *@args )
281
+ t.args.should == [ :a, :b, {:c => :d}]
282
+ t.options.should == { :c => :d }
283
+ end
284
+ end
285
+ end # passing args / options
286
+ end # binding instance methods
287
+ end # simple machine w/ 2 states, 1 transition
288
+
289
+ #
290
+ #
291
+ #
292
+
293
+ describe "A simple machine with 1 state and an event cycling at the same state" do
294
+
295
+ before do
296
+ @machine = Klass.state_fu_machine do
297
+ state :state_fuega do
298
+ event :transfer, :to => :state_fuega
299
+ end
300
+ end
301
+ @state = @machine.states[:state_fuega]
302
+ @event = @machine.events.first
303
+ @obj = Klass.new
304
+ end
305
+
306
+ describe "state_fu instance methods" do
307
+ describe "calling state_fu.cycle!()" do
308
+ it "should not change the state" do
309
+ @obj.state_fu.state.should == @state
310
+ @obj.state_fu.cycle!
311
+ @obj.state_fu.state.should == @state
312
+ end
313
+
314
+ it "should pass args / options to the transition" do
315
+ t = @obj.state_fu.cycle!( nil, :a, :b , { :c => :d } )
316
+ t.args.should == [ :a, :b, { :c => :d } ]
317
+ t.options.should == { :c => :d }
318
+ end
319
+
320
+ it "should not raise an error" do
321
+ @obj.state_fu.cycle!
322
+ end
323
+
324
+ it "should return an accepted transition" do
325
+ @obj.state_fu.state.should == @state
326
+ t = @obj.state_fu.cycle!
327
+ t.should be_kind_of( StateFu::Transition )
328
+ t.should be_accepted
329
+ end
330
+
331
+ end # state_fu.cycle!
332
+ end # state_fu instance methods
333
+ end # 1 state w/ cyclic event
334
+
335
+ #
336
+ #
337
+ #
338
+
339
+ describe "A simple machine with 3 states and an event to & from multiple states" do
340
+
341
+ before do
342
+ @machine = Klass.state_fu_machine do
343
+ states :a, :b
344
+ states :x, :y
345
+
346
+ event( :go ) do
347
+ from :a, :b
348
+ to :x, :y
349
+ end
350
+
351
+ initial_state :a
352
+ end
353
+ @a = @machine.states[:a]
354
+ @b = @machine.states[:b]
355
+ @x = @machine.states[:x]
356
+ @y = @machine.states[:y]
357
+ @event = @machine.events.first
358
+ @obj = Klass.new
359
+ end
360
+
361
+ it "should have an event from [:a, :b] to [:x, :y]" do
362
+ @event.origins.should == [@a, @b]
363
+ @event.targets.should == [@x, @y]
364
+ @obj.state_fu.state.should == @a
365
+ end
366
+
367
+ describe "transition instance methods" do
368
+ end
369
+
370
+ describe "state_fu instance methods" do
371
+ describe "state_fu.transition" do
372
+ it "should raise StateFu::UnknownTarget unless a valid targets state is supplied or can be inferred" do
373
+ lambda do
374
+ @obj.state_fu.transition( :go )
375
+ end.should raise_error( StateFu::UnknownTarget )
376
+
377
+ lambda do
378
+ @obj.state_fu.transition( [:go, nil] )
379
+ end.should raise_error( StateFu::UnknownTarget )
380
+
381
+ lambda do
382
+ @obj.state_fu.transition( [:go, :awol] )
383
+ end.should raise_error( StateFu::UnknownTarget )
384
+
385
+ lambda do
386
+ @obj.state_fu.transition( [:go, :x] )
387
+ @obj.state_fu.transition( [:go, :y] )
388
+ end.should_not raise_error( StateFu::UnknownTarget )
389
+ end
390
+
391
+ it "should return a transition with the specified destination" do
392
+ t = @obj.state_fu.transition( [:go, :x] )
393
+ t.should be_kind_of( StateFu::Transition )
394
+ t.event.name.should == :go
395
+ t.target.name.should == :x
396
+
397
+ lambda do
398
+ @obj.state_fu.transition( [:go, :y] )
399
+ end.should_not raise_error( )
400
+ end
401
+ end # state_fu.transition
402
+
403
+ describe "state_fu.fire_transition!" do
404
+ it "should raise an StateFu::UnknownTarget unless a valid targets state is supplied" do
405
+ lambda do
406
+ @obj.state_fu.fire_transition!( :go )
407
+ end.should raise_error( StateFu::UnknownTarget )
408
+
409
+ lambda do
410
+ @obj.state_fu.fire_transition!( [ :go, :awol ] )
411
+ end.should raise_error( StateFu::UnknownTarget )
412
+ end
413
+ end # state_fu.fire!
414
+
415
+ describe "state_fu.next!" do
416
+ it "should raise an StateFu::TransitionNotFound" do
417
+ lambda do
418
+ @obj.state_fu.next!
419
+ end.should raise_error( StateFu::TransitionNotFound )
420
+ end
421
+ end # next!
422
+
423
+ describe "state_fu.cycle!" do
424
+ it "should raise StateFu::TransitionNotFound" do
425
+ lambda do
426
+ @obj.state_fu.cycle!
427
+ end.should raise_error( StateFu::TransitionNotFound )
428
+ end
429
+ end # cycle!
430
+
431
+ end # state_fu instance methods
432
+ end # 1 state w/ cyclic event
433
+
434
+ describe "A simple machine w/ 2 states, 1 event and named hooks " do
435
+ before do
436
+ Klass.class_eval do
437
+ attr_reader :calls
438
+
439
+ def called name
440
+ (@calls ||= [])<< name
441
+ end
442
+
443
+ def before_go ; called :before_go end
444
+ def after_go ; called :after_go end
445
+ def execute_go ; called :execute_go end
446
+ def entering_a ; called :entering_a end
447
+ def accepted_a ; called :accepted_a end
448
+ def exiting_a ; called :exiting_a end
449
+ def entering_b ; called :entering_b end
450
+ def accepted_b ; called :accepted_b end
451
+ def exiting_b ; called :exiting_b end
452
+
453
+ end
454
+
455
+ @machine = Klass.state_fu_machine do
456
+
457
+ state :a do
458
+ on_exit( :exiting_a )
459
+ end
460
+
461
+ state :b do
462
+ on_entry( :entering_b )
463
+ accepted( :accepted_b )
464
+ end
465
+
466
+ event( :go ) do
467
+ from :a, :to => :b
468
+
469
+ before :before_go
470
+ execute :execute_go
471
+ after :after_go
472
+ end
473
+
474
+ initial_state :a
475
+ end
476
+
477
+ @a = @machine.states[:a]
478
+ @b = @machine.states[:b]
479
+ @event = @machine.events[:go]
480
+ @obj = Klass.new
481
+ end # before
482
+
483
+ describe "state :a" do
484
+ it "should have a hook for on_exit" do
485
+ @a.hooks[:exit].should == [ :exiting_a ]
486
+ end
487
+ end
488
+
489
+ describe "state :b" do
490
+ it "should have a hook for on_entry" do
491
+ @b.hooks[:entry].should == [ :entering_b ]
492
+ end
493
+ end
494
+
495
+ describe "event :go" do
496
+ it "should have a hook for before" do
497
+ @event.hooks[:before].should == [ :before_go ]
498
+ end
499
+
500
+ it "should have a hook for execute" do
501
+ @event.hooks[:execute].should == [ :execute_go ]
502
+ end
503
+
504
+ it "should have a hook for after" do
505
+ @event.hooks[:execute].should == [ :execute_go ]
506
+ end
507
+ end
508
+
509
+
510
+ describe "a transition for the event" do
511
+
512
+ it "should have all defined hooks in correct order of execution" do
513
+ t = @obj.state_fu.transition( :go )
514
+ hooks = t.hooks.map(&:last).flatten
515
+ hooks.should be_kind_of( Array )
516
+ hooks.should_not be_empty
517
+ hooks.should == [ :before_go,
518
+ :exiting_a,
519
+ :execute_go,
520
+ :entering_b,
521
+ :after_go,
522
+ :accepted_b ]
523
+ end
524
+ end # a transition ..
525
+
526
+ describe "fire! calling hooks" do
527
+ before do
528
+ @t = @obj.state_fu.transition( :go )
529
+ end
530
+
531
+ it "should update the object's state after state:entering and before event:after" do
532
+ @binding = @obj.state_fu
533
+ pending
534
+ @t.fire!
535
+ end
536
+
537
+ it "should be accepted after state:entering and before event:after" do
538
+ pending
539
+ mock( @obj ).entering_b( @t ) { @t.should_not be_accepted }
540
+ mock( @obj ).after_go(@t) { @t.should be_accepted }
541
+ mock( @obj ).accepted_b(@t) { @t.should be_accepted }
542
+ @t.fire!
543
+ end
544
+
545
+ it "should call the method for each hook on @obj in order, with the transition" do
546
+ pending
547
+ mock( @obj ).before_go(@t) { @called << :before_go }
548
+ mock( @obj ).exiting_a(@t) { @called << :exiting_a }
549
+ mock( @obj ).execute_go(@t) { @called << :execute_go }
550
+ mock( @obj ).entering_b(@t) { @called << :entering_b }
551
+ mock( @obj ).after_go(@t) { @called << :after_go }
552
+ mock( @obj ).accepted_b(@t) { @called << :accepted_b }
553
+
554
+ @t.fire!()
555
+ end
556
+
557
+ describe "adding an anonymous hook for event.hooks[:execute]" do
558
+ before do
559
+ called = @called # get us a ref for the closure
560
+ Klass.state_fu_machine do
561
+ event( :go ) do
562
+ execute do |ctx|
563
+ called( :execute_proc )
564
+ end
565
+ end
566
+ end
567
+ end
568
+
569
+ it "should be called at the correct point" do
570
+ @event.hooks[:execute].length.should == 2
571
+ @event.hooks[:execute].first.class.should == Symbol
572
+ @event.hooks[:execute].last.class.should == Proc
573
+ @t.fire!()
574
+ @obj.calls.should == [ :before_go,
575
+ :exiting_a,
576
+ :execute_go,
577
+ :execute_proc,
578
+ :entering_b,
579
+ :after_go,
580
+ :accepted_b ]
581
+ end
582
+
583
+ it "should be replace the previous proc for a slot if redefined" do
584
+ pending
585
+ called = @called # get us a ref for the closure
586
+ Klass.state_fu_machine do
587
+ event( :go ) do
588
+ execute do |ctx|
589
+ called << :execute_proc_2
590
+ end
591
+ end
592
+ end
593
+
594
+ @event.hooks[:execute].length.should == 2
595
+ @event.hooks[:execute].first.class.should == Symbol
596
+ @event.hooks[:execute].last.class.should == Proc
597
+
598
+ @t.fire!()
599
+ @called.should == [ :before_go,
600
+ :exiting_a,
601
+ :execute_go,
602
+ :execute_proc_2,
603
+ :entering_b,
604
+ :after_go,
605
+ :accepted_b ]
606
+ end
607
+ end # anonymous hook
608
+
609
+ describe "adding a named hook with a block" do
610
+ describe "with arity of -1/0" do
611
+ it "should call the block in the context of the transition" do
612
+ pending
613
+ called = @called # get us a ref for the closure
614
+ Klass.state_fu_machine do
615
+ event( :go ) do
616
+ execute(:named_execute) do
617
+ raise self.class.inspect unless self.is_a?( StateFu::Transition )
618
+ called << :execute_named_proc
619
+ end
620
+ end
621
+ end
622
+ @t.fire!()
623
+ @called.should == [ :before_go,
624
+ :exiting_a,
625
+ :execute_go,
626
+ :execute_named_proc,
627
+ :entering_b,
628
+ :after_go,
629
+ :accepted_b ]
630
+ end
631
+ end # arity 0
632
+
633
+ describe "with arity of 1" do
634
+ it "should call the proc in the context of the object, passing the transition as the argument" do
635
+ pending
636
+ called = @called # get us a ref for the closure
637
+ Klass.state_fu_machine do
638
+ event( :go ) do
639
+ execute(:named_execute) do |ctx|
640
+ raise ctx.class.inspect unless ctx.is_a?( StateFu::Transition )
641
+ raise self.class.inspect unless self.is_a?( Klass )
642
+ called << :execute_named_proc
643
+ end
644
+ end
645
+ end
646
+ @t.fire!()
647
+ @called.should == [ :before_go,
648
+ :exiting_a,
649
+ :execute_go,
650
+ :execute_named_proc,
651
+ :entering_b,
652
+ :after_go,
653
+ :accepted_b ]
654
+ end
655
+ end # arity 1
656
+ end # named proc
657
+
658
+ describe "halting the transition during the execute hook" do
659
+
660
+ before do
661
+ Klass.state_fu_machine do
662
+ event( :go ) do
663
+ execute do
664
+ halt!("stop")
665
+ end
666
+ end
667
+ end
668
+ end # before
669
+
670
+ it "should prevent the transition from being accepted" do
671
+ @obj.state_fu.state.name.should == :a
672
+ @t.fire!()
673
+ @obj.state_fu.state.name.should == :a
674
+ @t.should be_kind_of( StateFu::Transition )
675
+ @t.should be_halted
676
+ @t.should_not be_accepted
677
+ @obj.calls.flatten.should == [ :before_go,
678
+ :exiting_a,
679
+ :execute_go ]
680
+ end
681
+
682
+ it "should have current_hook_slot set to where it halted" do
683
+ @obj.state_fu.state.name.should == :a
684
+ @t.fire!()
685
+ @t.current_hook_slot.should == [:event, :execute]
686
+ end
687
+
688
+ it "should have current_hook set to where it halted" do
689
+ @obj.state_fu.state.name.should == :a
690
+ @t.fire!()
691
+ @t.current_hook.should be_kind_of( Proc )
692
+ end
693
+
694
+ end # halting from execute
695
+ end # fire! calling hooks
696
+
697
+ end # machine w/ hooks
698
+
699
+ describe "A binding for a machine with an event transition requirement" do
700
+ before do
701
+ @machine = Klass.state_fu_machine do
702
+ event( :go, :from => :a, :to => :b ) do
703
+ requires( :ok? )
704
+ end
705
+
706
+ initial_state :a
707
+ end
708
+ Klass.class_eval do
709
+ attr_accessor :ok
710
+ def ok?; ok; end
711
+ end
712
+ @obj = Klass.new
713
+ @binding = @obj.state_fu
714
+ @event = @machine.events[:go]
715
+ @a = @machine.states[:a]
716
+ @b = @machine.states[:b]
717
+ # stub(@obj).ok? { true }
718
+ end
719
+
720
+ describe "when no block is supplied for the requirement" do
721
+
722
+ it "should have an event named :go" do
723
+ @machine.events[:go].requirements.should == [:ok?]
724
+ @machine.events[:go].targets.should_not be_blank
725
+ @machine.events[:go].origins.should_not be_blank
726
+ @machine.states.map(&:name).sort_by(&:to_s).should == [:a, :b]
727
+ @a.should be_kind_of( StateFu::State )
728
+ @event.should be_kind_of( StateFu::Event )
729
+ @event.origins.map(&:name).should == [:a]
730
+ @binding.current_state.should == @machine.states[:a]
731
+ @event.from?( @machine.states[:a] ).should be_true
732
+ @machine.events[:go].from?( @binding.current_state ).should be_true
733
+ @binding.events.should_not be_empty
734
+ end
735
+
736
+
737
+ it "should contain the event in @binding.valid_events if @obj.ok? is true" do
738
+ # stub( @binding ).ok?() { true }
739
+ # set_method_arity(@binding,:ok, 0)
740
+ @obj.ok = true
741
+ @binding.current_state.should == @machine.initial_state
742
+ @binding.events.should == @machine.events
743
+ @binding.valid_events.should == [@event]
744
+ end
745
+
746
+ it "should not contain :go in @binding.valid_events if !@obj.ok?" do
747
+ # stub( @binding ).ok?() { false }
748
+ @obj.ok = false
749
+ @binding.events.should == @machine.events
750
+ @binding.valid_events.should == []
751
+ end
752
+
753
+ it "should raise a RequirementError if requirements are not satisfied" do
754
+ #stub( @binding ).ok? { false }
755
+ @obj.ok = false
756
+ lambda do
757
+ @obj.state_fu.fire_transition!( :go )
758
+ end.should raise_error( StateFu::RequirementError )
759
+ end
760
+
761
+ end # no block
762
+
763
+ describe "when a block is supplied for the requirement" do
764
+
765
+ it "should be a valid event if the block is true " do
766
+ @machine.named_procs[:ok?] = Proc.new() { true }
767
+ @binding.valid_events.should == [@event]
768
+
769
+ @machine.named_procs[:ok?] = Proc.new() { |binding| true }
770
+ @binding.valid_events.should == [@event]
771
+
772
+ end
773
+
774
+ it "should not be a valid event if the block is false" do
775
+ @machine.named_procs[:ok?] = Proc.new() { false }
776
+ @binding.valid_events.should == []
777
+
778
+ @machine.named_procs[:ok?] = Proc.new() { |binding| false }
779
+ @binding.valid_events.should == []
780
+ end
781
+
782
+ end # block supplied
783
+
784
+ end # machine w/guard conditions
785
+
786
+ describe "A binding for a machine with a state transition requirement" do
787
+ before do
788
+ @machine = Klass.state_fu_machine do
789
+ event( :go, :from => :a, :to => :b )
790
+ state( :b ) do
791
+ requires :entry_ok?
792
+ end
793
+ end
794
+ Klass.class_eval do
795
+ attr_accessor :entry_ok
796
+ def entry_ok?
797
+ entry_ok
798
+ end
799
+ end
800
+
801
+ @obj = Klass.new
802
+ @binding = @obj.state_fu
803
+ @obj.entry_ok = true
804
+ @event = @machine.events[:go]
805
+ @a = @machine.states[:a]
806
+ @b = @machine.states[:b]
807
+ end
808
+
809
+ describe "when no block is supplied for the requirement" do
810
+
811
+ it "should be valid if @binding.valid_transitions' values includes the state" do
812
+ t = @binding.transition([@event, @b])
813
+ @binding.valid_next_states.should == [@b]
814
+ end
815
+
816
+ it "should be invalid if @obj.entry_ok? is false" do
817
+ #mock( @obj ).entry_ok? { false }
818
+ @obj.entry_ok = false
819
+ @b.entry_requirements.should == [:entry_ok?]
820
+ @binding.valid_next_states.should == []
821
+ end
822
+
823
+ it "should be valid if @obj.entry_ok? is true" do
824
+ # mock( @obj ).entry_ok? { true }
825
+ @obj.entry_ok = true
826
+ @binding.valid_next_states.should == [@b]
827
+ end
828
+
829
+ end # no block
830
+
831
+ describe "when a block is supplied for the requirement" do
832
+
833
+ it "should be a valid event if the block is true " do
834
+ @machine.named_procs[:entry_ok?] = Proc.new() { true }
835
+ @binding.valid_next_states.should == [@b]
836
+
837
+ @machine.named_procs[:entry_ok?] = Proc.new() { |binding| true }
838
+ @binding.valid_next_states.should == [@b]
839
+ end
840
+
841
+ it "should not be a valid event if the block is false" do
842
+ @machine.named_procs[:entry_ok?] = Proc.new() { false }
843
+ @binding.valid_next_states.should == []
844
+
845
+ @machine.named_procs[:entry_ok?] = Proc.new() { |binding| false }
846
+ @binding.valid_next_states.should == []
847
+ end
848
+
849
+ end # block supplied
850
+ end # machine with state transition requirement
851
+
852
+ describe "a hook method accessing the transition, object, binding and arguments to fire!" do
853
+ before do
854
+ reset!
855
+ make_pristine_class("Klass")
856
+ @machine = Klass.state_fu_machine do
857
+ event(:run, :from => :start, :to => :finish ) do
858
+ execute( :run_exec )
859
+ end
860
+ end # machine
861
+ @obj = Klass.new()
862
+ end # before
863
+
864
+ describe "a method defined on the stateful object" do
865
+
866
+ it "should be able to conditionally execute code based on whether the transition is a test" do
867
+ pending
868
+ testing = nil
869
+ @obj.__define_singleton_method(:run_exec) do
870
+ testing = t.testing?
871
+ end
872
+ @obj.state_fu.fire! :run do |t|
873
+ t.test_only = true
874
+ end
875
+ testing.should == true
876
+ end
877
+
878
+ it "should be able to call methods on the transition mixed in via machine.helper" do
879
+ t1 = @obj.state_fu.transition( :run)
880
+ t1.should_not respond_to(:my_rad_method)
881
+
882
+ @machine.helper :my_rad_helper
883
+ module ::MyRadHelper
884
+ def my_rad_method( x )
885
+ x
886
+ end
887
+ end
888
+ t2 = @obj.state_fu.transition( :run )
889
+ t2.should respond_to( :my_rad_method )
890
+ t2.my_rad_method( 6 ).should == 6
891
+
892
+ @machine.instance_eval do
893
+ helpers.pop
894
+ end
895
+ t3 = @obj.state_fu.transition( :run )
896
+
897
+ # triple check for contamination
898
+ t1.should_not respond_to(:my_rad_method)
899
+ t2.should respond_to(:my_rad_method)
900
+ t3.should_not respond_to(:my_rad_method)
901
+ end
902
+
903
+ it "should be able to access the args / options passed to fire! via transition.args" do
904
+ pending
905
+ # NOTE a trailing hash gets munged into options - not args
906
+ args = [:a, :b, { 'c' => :d }]
907
+ @obj.__define_singleton_method(:run_exec) do
908
+ t.args.should == [:a, :b,{'c' => :d}]
909
+ t.options.should == {}
910
+ end
911
+ trans = @obj.state_fu.fire!( :run, *args )
912
+ trans.should be_accepted
913
+ end
914
+ end # method defined on object
915
+
916
+ describe "a block passed to binding.transition" do
917
+ it "should execute in the context of the transition initializer after it's set up" do
918
+ pending
919
+ @obj.__define_singleton_method(:run_exec) do
920
+ t.args.should == ['who','yo','daddy?']
921
+ t.options.should == {:hi => :mum}
922
+ end
923
+ trans = @obj.state_fu.transition( :run ) do
924
+ @args = %w/ who yo daddy? /
925
+ @options = {:hi => :mum}
926
+
927
+ end
928
+ trans.fire!()
929
+ end
930
+ end
931
+
932
+ end # args with fire!
933
+
934
+ describe "next_transition" do
935
+ describe "when there are multiple events but only one is fireable?" do
936
+ before do
937
+ pending
938
+ reset!
939
+ make_pristine_class("Klass")
940
+ @machine = Klass.state_fu_machine do
941
+ initial_state :alive do
942
+ event :impossibility do
943
+ to :afterlife
944
+ requires :truth_of_patent_falsehoods? do
945
+ false
946
+ end
947
+ end
948
+
949
+ event :inevitability do
950
+ to :plain_old_dead
951
+ end
952
+ end
953
+ end
954
+ @obj = Klass.new()
955
+ @binding = @obj.state_fu
956
+ @binding.events.length.should == 2
957
+ #@machine.events[:impossibility].fireable_by?( @binding ).should == false
958
+ #@machine.events[:inevitability].fireable_by?( @binding ).should == true
959
+ end
960
+
961
+ describe "when the fireable? event has only one target" do
962
+ it "should return a transition for the fireable event & its target" do
963
+ @machine.events[:inevitability].targets.length.should == 1
964
+ t = @binding.next_transition
965
+ t.should be_kind_of( StateFu::Transition )
966
+ t.from.should == @binding.current_state
967
+ t.to.should == @machine.states[:plain_old_dead]
968
+ t.event.should == @machine.events[:inevitability]
969
+ end
970
+ end
971
+
972
+ describe "when the fireable? event has multiple targets but only one can be entered" do
973
+ before do
974
+ reset!
975
+ make_pristine_class("Klass")
976
+ @machine = Klass.state_fu_machine do
977
+ initial_state :alive
978
+
979
+ state :cremated
980
+
981
+ state :buried do
982
+ requires :plot_at_cemetary? do
983
+ false
984
+ end
985
+ end
986
+
987
+ event :inevitability do
988
+ from :alive
989
+ to :cremated, :buried
990
+ end
991
+ end
992
+ @obj = Klass.new()
993
+ @binding = @obj.state_fu
994
+ @machine.events[:inevitability].should be_kind_of(StateFu::Event)
995
+ @binding.valid_events.map(&:name).should == [@machine.events[:inevitability]].map(&:name)
996
+ @binding.valid_events.should == [@machine.events[:inevitability]]
997
+ @binding.valid_transitions.map(&:target).map(&:name).should == [:cremated]
998
+ end # before
999
+
1000
+ it "should return a transition for the fireable event & the enterable target" do
1001
+ t = @binding.next_transition
1002
+ t.should be_kind_of( StateFu::Transition )
1003
+ t.from.should == @binding.current_state
1004
+ t.to.should == @machine.states[:cremated]
1005
+ t.event.should == @machine.events[:inevitability]
1006
+ end
1007
+ end
1008
+
1009
+ describe "when the fireable? event has multiple targets and more than one can be entered" do
1010
+ before do
1011
+ @machine.lathe do
1012
+ event :inevitability do
1013
+ to :cremated, :buried
1014
+ end
1015
+ end
1016
+ @obj = Klass.new()
1017
+ @binding = @obj.state_fu
1018
+ end
1019
+
1020
+ it "should not return a transition" do
1021
+ t = @binding.next_transition
1022
+ t.should be_nil
1023
+ end
1024
+
1025
+ it "should raise an IllegalTransition if next! is called" do
1026
+ lambda { @binding.next! }.should raise_error( StateFu::IllegalTransition )
1027
+ end
1028
+ end
1029
+
1030
+ end
1031
+ end
1032
+ end
1033
+