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