state-fu 0.11.1

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