davidlee-state-fu 0.3.1 → 0.10.0

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 (90) hide show
  1. data/README.textile +124 -34
  2. data/Rakefile +36 -30
  3. data/lib/no_stdout.rb +1 -1
  4. data/lib/state-fu.rb +9 -8
  5. data/lib/state_fu/active_support_lite/array/access.rb +12 -5
  6. data/lib/state_fu/active_support_lite/array/conversions.rb +10 -4
  7. data/lib/state_fu/active_support_lite/array/extract_options.rb +5 -4
  8. data/lib/state_fu/active_support_lite/array/grouping.rb +7 -4
  9. data/lib/state_fu/active_support_lite/array/random_access.rb +4 -3
  10. data/lib/state_fu/active_support_lite/array/wrapper.rb +4 -3
  11. data/lib/state_fu/active_support_lite/array.rb +3 -1
  12. data/lib/state_fu/active_support_lite/blank.rb +18 -9
  13. data/lib/state_fu/active_support_lite/cattr_reader.rb +4 -1
  14. data/lib/state_fu/active_support_lite/keys.rb +8 -3
  15. data/lib/state_fu/active_support_lite/misc.rb +6 -4
  16. data/lib/state_fu/active_support_lite/module/delegation.rb +130 -0
  17. data/lib/state_fu/active_support_lite/module.rb +1 -0
  18. data/lib/state_fu/active_support_lite/object.rb +5 -2
  19. data/lib/state_fu/active_support_lite/string.rb +6 -1
  20. data/lib/state_fu/active_support_lite/symbol.rb +2 -1
  21. data/lib/state_fu/applicable.rb +41 -0
  22. data/lib/state_fu/{helper.rb → arrays.rb} +45 -121
  23. data/lib/state_fu/binding.rb +136 -159
  24. data/lib/state_fu/core_ext.rb +78 -10
  25. data/lib/state_fu/event.rb +112 -48
  26. data/lib/state_fu/exceptions.rb +80 -34
  27. data/lib/state_fu/executioner.rb +149 -0
  28. data/lib/state_fu/has_options.rb +16 -0
  29. data/lib/state_fu/hooks.rb +21 -16
  30. data/lib/state_fu/interface.rb +80 -83
  31. data/lib/state_fu/lathe.rb +361 -148
  32. data/lib/state_fu/logger.rb +122 -45
  33. data/lib/state_fu/machine.rb +60 -32
  34. data/lib/state_fu/method_factory.rb +180 -72
  35. data/lib/state_fu/methodical.rb +17 -0
  36. data/lib/state_fu/persistence/active_record.rb +6 -1
  37. data/lib/state_fu/persistence/attribute.rb +1 -0
  38. data/lib/state_fu/persistence/base.rb +8 -6
  39. data/lib/state_fu/persistence.rb +94 -23
  40. data/lib/state_fu/sprocket.rb +26 -11
  41. data/lib/state_fu/state.rb +8 -27
  42. data/lib/state_fu/transition.rb +207 -98
  43. data/lib/state_fu/transition_query.rb +214 -0
  44. data/lib/state_fu.rb +1 -0
  45. data/lib/tasks/spec_last.rake +46 -0
  46. data/lib/tasks/state_fu.rake +57 -0
  47. data/lib/vizier.rb +61 -61
  48. data/spec/custom_formatter.rb +49 -0
  49. data/spec/features/binding_and_transition_helper_mixin_spec.rb +2 -2
  50. data/spec/features/method_missing_only_once_spec.rb +28 -0
  51. data/spec/features/not_requirements_spec.rb +83 -46
  52. data/spec/features/plotter_spec.rb +97 -0
  53. data/spec/features/shared_log_spec.rb +7 -0
  54. data/spec/features/singleton_machine_spec.rb +39 -0
  55. data/spec/features/state_and_array_options_accessor_spec.rb +1 -1
  56. data/spec/features/{transition_boolean_comparison.rb → transition_boolean_comparison_spec.rb} +29 -18
  57. data/spec/helper.rb +6 -117
  58. data/spec/integration/active_record_persistence_spec.rb +18 -4
  59. data/spec/integration/binding_extension_spec.rb +1 -1
  60. data/spec/integration/class_accessor_spec.rb +49 -59
  61. data/spec/integration/event_definition_spec.rb +20 -20
  62. data/spec/integration/example_01_document_spec.rb +13 -8
  63. data/spec/integration/example_02_string_spec.rb +3 -2
  64. data/spec/integration/instance_accessor_spec.rb +16 -19
  65. data/spec/integration/lathe_extension_spec.rb +2 -2
  66. data/spec/integration/machine_duplication_spec.rb +59 -37
  67. data/spec/integration/relaxdb_persistence_spec.rb +6 -3
  68. data/spec/integration/requirement_reflection_spec.rb +66 -57
  69. data/spec/integration/state_definition_spec.rb +72 -66
  70. data/spec/integration/transition_spec.rb +169 -173
  71. data/spec/spec.opts +5 -3
  72. data/spec/spec_helper.rb +132 -0
  73. data/spec/state_fu_spec.rb +870 -0
  74. data/spec/units/binding_spec.rb +33 -22
  75. data/spec/units/event_spec.rb +3 -22
  76. data/spec/units/exceptions_spec.rb +7 -0
  77. data/spec/units/lathe_spec.rb +7 -7
  78. data/spec/units/machine_spec.rb +67 -75
  79. data/spec/units/method_factory_spec.rb +55 -48
  80. data/spec/units/sprocket_spec.rb +5 -7
  81. data/spec/units/state_spec.rb +33 -24
  82. metadata +31 -19
  83. data/lib/state_fu/active_support_lite/inheritable_attributes.rb +0 -1
  84. data/lib/state_fu/fu_space.rb +0 -51
  85. data/lib/state_fu/mock_transition.rb +0 -38
  86. data/spec/BDD/plotter_spec.rb +0 -115
  87. data/spec/integration/dynamic_requirement_spec.rb +0 -160
  88. data/spec/integration/ex_machine_for_accounts_spec.rb +0 -79
  89. data/spec/integration/sanity_spec.rb +0 -31
  90. data/spec/units/fu_space_spec.rb +0 -95
@@ -0,0 +1,870 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ #
4
+ # Door
5
+ #
6
+
7
+ describe "A door which opens and shuts:" do
8
+ before :all do
9
+
10
+ # class Door
11
+ make_pristine_class('Door') do
12
+ include StateFu
13
+
14
+ attr_accessor :locked
15
+
16
+ def shut
17
+ "I don't know how to shut!"
18
+ end
19
+
20
+ def locked?
21
+ !!locked
22
+ end
23
+
24
+ def method_missing(method_name, *args, &block)
25
+ raise NoMethodError.new("I'm just a door!")
26
+ end
27
+
28
+ machine do
29
+ event :shut, :transitions_from => :open, :to => :closed
30
+ event :slam, :transitions_from => :open, :to => :closed
31
+ event :open, :transitions_from => :closed, :to => :open,
32
+ :requires => :not_locked?,
33
+ :message => "Sorry, it's locked."
34
+ end
35
+ end
36
+ end # before
37
+
38
+ describe "Door's state machine" do
39
+ it "have two states, :open and :closed" do
40
+ Door.machine.states.names.should == [:open, :closed]
41
+ end
42
+
43
+ it "have two events, :shut and :open" do
44
+ Door.machine.events.names.should == [:shut, :slam, :open]
45
+ end
46
+
47
+ it "have an initial state of :open, the first state defined" do
48
+ Door.machine.initial_state.name.should == :open
49
+ end
50
+
51
+ it "have an initial state of :open, the first state defined" do
52
+ Door.machine.initial_state.name.should == :open
53
+ end
54
+
55
+ it "have a requirement :not_locked? for the :open event" do
56
+ Door.machine.events[:open].requirements.should == [:not_locked?]
57
+ end
58
+
59
+ it "have a requirement_message 'Sorry, it's locked.' for :not_locked?" do
60
+ Door.machine.requirement_messages[:not_locked?].should == "Sorry, it's locked."
61
+ end
62
+
63
+ it "can reflect on event origin and target states" do
64
+ event = Door.machine.events[:open]
65
+ event.origin_names.should == [:closed]
66
+ event.target_names.should == [:open]
67
+ event.simple?.should == true # because there's only one possible target
68
+ event.origin.should == :closed # because there's only one origin
69
+ event.target.should == :open # because there's only one target
70
+ event.machine.should == Door.machine
71
+ end
72
+
73
+ end
74
+
75
+ describe "door" do
76
+ before do
77
+ @door = Door.new
78
+ end
79
+
80
+ describe "magic event methods" do
81
+
82
+ it "doesn't normally have a method #shut!" do
83
+ @door.respond_to?(:shut!).should == false
84
+ end
85
+
86
+ it "will define #shut! when method_missing is called for the first time" do
87
+ begin
88
+ @door.play_the_ukelele
89
+ rescue NoMethodError
90
+ end
91
+ @door.respond_to?(:shut!).should == true
92
+ end
93
+
94
+ it "will keep any existing methods when method_missing is triggered" do
95
+ @door.respond_to?(:shut).should == true
96
+ @door.respond_to?(:can_shut?).should == false
97
+ @door.shut.should == "I don't know how to shut!"
98
+ @door.can_shut?.should == true # triggers method_missing
99
+ @door.respond_to?(:shut).should == true
100
+ @door.respond_to?(:can_shut?).should == true # new methods defined
101
+ @door.shut.should == "I don't know how to shut!" # old method retained
102
+ end
103
+
104
+ it "gets a set of new methods when any magic method is called" do
105
+ @door.respond_to?(:shut).should == true # already defined
106
+ @door.respond_to?(:open).should == false
107
+ @door.respond_to?(:can_shut?).should == false
108
+ @door.respond_to?(:can_open?).should == false
109
+ @door.respond_to?(:shut!).should == false
110
+ @door.respond_to?(:open!).should == false
111
+ @door.can_shut?.should == true # call one of them (triggers method_missing)
112
+ @door.respond_to?(:open).should == false # a private method: Kernel#open
113
+ @door.respond_to?(:can_shut?).should == true # but these are all newly defined public methods
114
+ @door.respond_to?(:can_open?).should == true
115
+ @door.respond_to?(:shut!).should == true
116
+ @door.respond_to?(:open!).should == true
117
+ end
118
+
119
+ it "retains any previously defined method_missing" do
120
+ begin
121
+ @door.hug_me
122
+ rescue NoMethodError => e
123
+ e.message.should == "I'm just a door!"
124
+ end
125
+ end
126
+
127
+ describe "for :slam - " do
128
+ describe "#slam" do
129
+ it "should return an unfired StateFu::Transition" do
130
+ t = @door.slam
131
+ t.should be_kind_of(StateFu::Transition)
132
+ t.fired?.should == false
133
+ end
134
+ end
135
+
136
+ describe "#can_slam?" do
137
+ it "should be true if the transition is valid for the current state" do
138
+ @door.current_state.should == :open
139
+ @door.can_slam?.should == true
140
+ end
141
+
142
+ it "should be false when the transition has unmet requirements" do
143
+ Door.machine.events[:slam].lathe do
144
+ requires :some_impossible_condition do
145
+ false
146
+ end
147
+ end
148
+ @door.can_slam?.should == false
149
+ end
150
+
151
+ it "should be nil when the transition is invalid for the current state" do
152
+ @door.shut!
153
+ @door.current_state.should == :closed
154
+ @door.can_slam?.should == nil
155
+ end
156
+ end
157
+ end
158
+
159
+ end # magic event methods
160
+
161
+ describe "magic state methods" do
162
+ it "should be defined for each state by method_missing voodoo" do
163
+ @door.should_not respond_to(:closed?)
164
+ @door.should_not respond_to(:open?)
165
+ @door.open?.should == true
166
+ @door.should respond_to(:closed?)
167
+ @door.should respond_to(:open?)
168
+ end
169
+
170
+ describe "for :closed - " do
171
+ describe "#closed?" do
172
+ it "should be true when the current_state is :closed" do
173
+ @door.current_state.should == :open
174
+ @door.closed?.should == false
175
+ @door.shut!
176
+ @door.closed?.should == true
177
+ end
178
+ end
179
+ end
180
+ end # magic state methods
181
+
182
+ it "#can_shut? when the current state is open" do
183
+ @door.current_state.should == :open
184
+ @door.can_shut?.should == true
185
+ @door.can_open?.should == nil # not a valid transition from this state -> nil
186
+ end
187
+
188
+ it "transitions from :open to :closed on #shut!" do
189
+ @door.current_state.should == :open
190
+ shut_result = @door.shut!
191
+ shut_result.should be_true
192
+ shut_result.should be_kind_of(StateFu::Transition)
193
+ shut_result.should be_complete
194
+ @door.current_state.should == :closed
195
+ end
196
+
197
+ it "raises a StateFu::InvalidTransition if #shut! is called when already :closed" do
198
+ @door.current_state.should == :open
199
+ @door.shut!.should be_true
200
+ @door.current_state.should == :closed
201
+ lambda do
202
+ t = @door.shut!
203
+ t.origin.should == :open
204
+ end.should raise_error(StateFu::InvalidTransition)
205
+ end
206
+
207
+ it "raises StateFu::RequirementError if #open! is called when it is locked" do
208
+ @door.shut!
209
+ @door.locked = true
210
+ lambda { @door.open! }.should raise_error(StateFu::RequirementError)
211
+ end
212
+
213
+ it "tells you why it won't open if you ask nicely" do
214
+ @door.shut!
215
+ @door.locked = true
216
+ @door.locked?.should be_true
217
+
218
+ transition = @door.state_fu.transition :open
219
+ transition.requirement_errors.should == {:not_locked? => "Sorry, it's locked."}
220
+ end
221
+
222
+ it "gives you information about the requirement errors if you rescue the RequirementError" do
223
+ @door.shut!
224
+ @door.locked = true
225
+ @door.locked?.should be_true
226
+ begin
227
+ @door.open!
228
+ rescue StateFu::RequirementError => e
229
+ e.to_a.should == ["Sorry, it's locked."]
230
+ e.to_h.should == {:not_locked? => "Sorry, it's locked."}
231
+ e.to_enum.should be_kind_of(Enumerable::Enumerator)
232
+ e.should_not be_empty
233
+ e.length.should == 1
234
+ e.each do |requirement, message|
235
+ requirement.should == :not_locked?
236
+ message.should == "Sorry, it's locked."
237
+ end
238
+ end
239
+ end
240
+
241
+ describe "Transition objects" do
242
+
243
+ # TODO refactor me
244
+ def should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed(transition)
245
+ transition.should be_kind_of(StateFu::Transition)
246
+ transition.fired?.should == false
247
+ transition.current_state.should == :open
248
+ transition.event.should == :slam
249
+ transition.origin.should == :open
250
+ transition.target.should == :closed
251
+ end
252
+
253
+ it "returns a Transition on #slam" do
254
+ transition = @door.slam
255
+ should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
256
+ end
257
+
258
+ it "returns a Transition on #state_fu.slam" do
259
+ transition = @door.state_fu.slam
260
+ should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
261
+ end
262
+
263
+ it "returns a Transition on #state_fu.transition :slam" do
264
+ transition = @door.state_fu.transition :slam
265
+ should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
266
+ end
267
+
268
+ it "returns a Transition on #state_fu.transition [:slam, :closed]" do
269
+ transition = @door.state_fu.transition [:slam, :closed]
270
+ should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
271
+ end
272
+
273
+ it "changes the door's state when you #fire! the transition" do
274
+ transition = @door.slam
275
+ transition.fire!
276
+ transition.fired?.should == true
277
+ transition.complete?.should == true
278
+ @door.current_state.should == :closed
279
+ end
280
+
281
+ it "can tell you its #origin and #target states" do
282
+ transition = @door.state_fu.transition :shut
283
+ transition.origin.should be_kind_of(StateFu::State)
284
+ transition.target.should be_kind_of(StateFu::State)
285
+ transition.origin.should == :open
286
+ transition.target.should == :closed
287
+ end
288
+
289
+ it "can give you information about any requirement errors" do
290
+ @door.shut!
291
+ @door.locked = true
292
+ transition = @door.state_fu.transition :open
293
+ transition.valid?.should == false
294
+ transition.unmet_requirements.should == [:not_locked?]
295
+ transition.unmet_requirement_messages.should == ["Sorry, it's locked."]
296
+ transition.requirement_errors.should == {:not_locked? => "Sorry, it's locked."}
297
+ transition.first_unmet_requirement.should == :not_locked?
298
+ transition.first_unmet_requirement_message.should == "Sorry, it's locked."
299
+ end
300
+ end
301
+
302
+ # TODO save this for later ...............
303
+ describe "#state_fu_binding" do
304
+ it "be a StateFu::Binding" do
305
+ @door.state_fu_binding.should be_kind_of StateFu::Binding
306
+ end
307
+
308
+ it "have a current_state which is initially :open" do
309
+ @door.state_fu_binding.current_state.should == :open
310
+ end
311
+
312
+ it "have two events, :shut and :slam" do
313
+ @door.state_fu_binding.events.should == [:shut, :slam]
314
+ end
315
+
316
+ it "have a list of #valid_transitions" do
317
+ @door.state_fu_binding.valid_transitions.should be_kind_of(StateFu::TransitionQuery)
318
+ @door.state_fu_binding.valid_transitions.length.should == 2
319
+ t = @door.state_fu_binding.valid_transitions.first
320
+ t.event.name.should == :shut
321
+ t.origin.name.should == :open
322
+ t.target.name.should == :closed
323
+ t = @door.state_fu_binding.valid_transitions.last
324
+ t.event.name.should == :slam
325
+ t.origin.name.should == :open
326
+ t.target.name.should == :closed
327
+ end
328
+ end
329
+
330
+ describe "#state_fu" do
331
+ it "be the same as door#state_fu_binding" do
332
+ @door.state_fu.should == @door.state_fu_binding
333
+ end
334
+ end
335
+
336
+ describe "#stfu" do
337
+ it "be the same as door#state_fu_binding" do
338
+ @door.stfu.should == @door.state_fu_binding
339
+ end
340
+ end
341
+
342
+ describe "#fu" do
343
+ it "be the same as door#state_fu_binding" do
344
+ @door.fu.should == @door.state_fu_binding
345
+ end
346
+ end
347
+
348
+ end
349
+ end
350
+
351
+ #
352
+ # Heart
353
+ #
354
+
355
+ describe "a simple machine, a heart which beats:" do
356
+
357
+ before :all do
358
+ make_pristine_class('Heart') do
359
+ include StateFu
360
+
361
+ def heartbeats
362
+ @heartbeats ||= []
363
+ end
364
+
365
+ machine do
366
+ cycle :state => :beating, :on => :beat do
367
+ causes(:heartbeat) { heartbeats << :thumpthump }
368
+ end
369
+ event :stop, :from => { :beating => :stopped }
370
+ end
371
+ end
372
+ end # before
373
+
374
+ describe "the machine" do
375
+ it "have two states, :beating and :stopped" do
376
+ Heart.machine.states.names.should == [:beating,:stopped]
377
+ end
378
+
379
+ it "have two events, :beat and :stop" do
380
+ Heart.machine.events.names.should == [:beat, :stop]
381
+ end
382
+
383
+ it "have an initial state of :beating" do
384
+ Heart.machine.initial_state.name.should == :beating
385
+ end
386
+ end
387
+
388
+ describe "it" do
389
+ before do
390
+ @heart = Heart.new
391
+ end
392
+
393
+ it "cause a heartbeat on heart#beat!" do
394
+ @heart.heartbeats.should == []
395
+ @heart.beat!.should be_true
396
+ @heart.heartbeats.should == [:thumpthump]
397
+ end
398
+
399
+ it "raise an InvalidTransition if it tries to beat after it's stopped" do
400
+ @heart.stop!
401
+ @heart.current_state.should == :stopped
402
+ lambda { @heart.beat! }.should raise_error(StateFu::InvalidTransition)
403
+ end
404
+
405
+ it "transition to :stopped on #next!" do
406
+ @heart.current_state.should == :beating
407
+ @heart.state_fu.transitions.not_cyclic.length.should == 1
408
+ @heart.state_fu.next_transition.should_not == nil
409
+ @heart.state_fu.next_state.should_not == nil
410
+ @heart.next_state!
411
+ @heart.current_state.should == :stopped
412
+ end
413
+
414
+ it "transition to :stopped on #next_state!" do
415
+ @heart.current_state.should == :beating
416
+ @heart.next_state!
417
+ @heart.current_state.should == :stopped
418
+ end
419
+
420
+ it "transition to :stopped on #next_transition!" do
421
+ @heart.current_state.should == :beating
422
+ @heart.next_state!
423
+ @heart.current_state.should == :stopped
424
+ end
425
+
426
+ end
427
+ end
428
+
429
+ #
430
+ # Traffic Lights
431
+ #
432
+
433
+ describe "a simple machine, a set of traffic lights:" do
434
+ before :all do
435
+
436
+ make_pristine_class('TrafficLights') do
437
+ include StateFu
438
+ attr_reader :photos
439
+
440
+ def initialize
441
+ @photos = []
442
+ end
443
+
444
+ def red_light_camera
445
+ @photos << :click
446
+ end
447
+
448
+ machine do
449
+ state :go, :colour => :green
450
+ state :caution, :colour => :amber
451
+ state :stop, :colour => :red do
452
+ on_entry :red_light_camera
453
+ end
454
+
455
+ connect_states :go, :caution, :stop, :go
456
+ end
457
+ end
458
+ end # before
459
+
460
+ describe "the machine:" do
461
+ it "have three states, :go, :caution, and :stop" do
462
+ TrafficLights.machine.states.names.should == [:go, :caution, :stop]
463
+ end
464
+
465
+ it "have three events :go_to_caution, :caution_to_stop, and :stop_to_go" do
466
+ TrafficLights.machine.events.names.should == [:go_to_caution, :caution_to_stop, :stop_to_go]
467
+ end
468
+
469
+ it "have an initial_state of :go" do
470
+ TrafficLights.machine.initial_state.name.should == :go
471
+ end
472
+
473
+ describe "the states' options" do
474
+ it "have an appropriate colour" do
475
+ TrafficLights.machine.states[:go] [:colour].should == :green
476
+ TrafficLights.machine.states[:caution][:colour].should == :amber
477
+ TrafficLights.machine.states[:stop] [:colour].should == :red
478
+ end
479
+ end
480
+ end
481
+
482
+ describe "it" do
483
+ before do
484
+ @lights = TrafficLights.new
485
+ end
486
+
487
+ it "transition from :go to :caution on #go_to_caution!" do
488
+ @lights.current_state.should == :go
489
+ @lights.go_to_caution!
490
+ @lights.current_state.should == :caution
491
+ end
492
+
493
+ it "transition from :go to :caution on #next!" do
494
+ @lights.current_state.should == :go
495
+ @lights.next!
496
+ @lights.current_state.should == :caution
497
+ end
498
+
499
+ it "transition from :go to :caution on #next_state!" do
500
+ @lights.current_state.should == :go
501
+ @lights.next_state!
502
+ @lights.current_state.should == :caution
503
+ end
504
+
505
+ it "transition from :go to :caution on #fire_next_transition!" do
506
+ @lights.current_state.should == :go
507
+ @lights.fire_next_transition!
508
+ @lights.current_state.should == :caution
509
+ end
510
+
511
+ describe "when entering the :stop state" do
512
+ it "fire :red_light_camera" do
513
+ @lights.next!
514
+ @lights.photos.should be_empty
515
+ @lights.next!
516
+ @lights.current_state.should == :stop
517
+ @lights.photos.length.should == 1
518
+ end
519
+ end
520
+ end
521
+ end
522
+
523
+ #
524
+ # Recorder
525
+ #
526
+ describe "arguments given to different method signatures" do
527
+ before :all do
528
+ make_pristine_class('Recorder') do
529
+ include StateFu
530
+ attr_accessor :received
531
+
532
+ def initialize
533
+ @received = {}
534
+ end
535
+
536
+ # arguments passed to methods / procs:
537
+ # these method signatures get a transition
538
+ def a1(t) received[:a1] = [t] end
539
+ def b1(t=nil) received[:b1] = [t] end
540
+ def c1(*t) received[:c1] = [t] end
541
+
542
+ # these method signatures get a transition and a list of arguments
543
+ def a2(t,a) received[:a2] = [t,a] end
544
+ def b2(t,a=nil) received[:b2] = [t,a] end
545
+ def c2(t,*a) received[:c2] = [t,a] end
546
+
547
+ # these method signatures get a transition, a list of arguments,
548
+ # and the object which owns the machine
549
+ def a3(t,a,o) received[:a3] = [t,a,o] end
550
+ def b3(t,a,o=nil) received[:b3] = [t,a,o] end
551
+ def c3(t,a,*o) received[:c3] = [t,a,o] end
552
+
553
+ machine do
554
+ cycle :state => :observing, :on => :observe do
555
+ trigger :a1, :b1, :c1, :a2, :b2, :c2, :a3, :b3, :c3
556
+ end
557
+ end
558
+
559
+ end
560
+ end # before
561
+
562
+ describe "the machine" do
563
+ it "have an event :observe which is a #cycle?" do
564
+ Recorder.machine.events[:observe].cycle?.should be_true
565
+ end
566
+
567
+ it "have a list of execute hooks" do
568
+ Recorder.machine.events[:observe].hooks[:execute].should == [:a1, :b1, :c1, :a2, :b2, :c2, :a3, :b3, :c3]
569
+ end
570
+ end
571
+
572
+ describe "it" do
573
+ before do
574
+ @recorder = Recorder.new
575
+ end
576
+
577
+ it "fire a transition on #observe!" do
578
+ t = @recorder.observe!
579
+ results = @recorder.received
580
+ t.should be_kind_of(StateFu::Transition)
581
+ t.should be_complete
582
+ end
583
+
584
+ describe "observing method calls on #observe!" do
585
+ before do
586
+ @t = @recorder.observe!
587
+ @results = @recorder.received
588
+ end
589
+
590
+ it "call the event's :execute hooks on #observe!" do
591
+ @results.keys.should =~ [:a1, :b1, :c1, :a2, :b2, :c2, :a3, :b3, :c3]
592
+ end
593
+
594
+ describe "methods which expect one argument" do
595
+ it "receive a StateFu::Transition" do
596
+ @results[:a1].should == [@t]
597
+ @results[:b1].should == [@t]
598
+ @results[:c1].should == [[@t]]
599
+ end
600
+ end
601
+
602
+ describe "methods which expect two arguments" do
603
+ it "receive a StateFu::Transition and an argument list" do
604
+ @results[:a2].should == [@t, @t.args]
605
+ @results[:b2].should == [@t, @t.args]
606
+ @results[:c2].should == [@t, [@t.args]]
607
+ end
608
+ end
609
+
610
+ describe "methods which expect three arguments" do
611
+ it "receive a StateFu::Transition, an argument list and the recorder object" do
612
+ @results[:a3].should == [@t, @t.args, @recorder]
613
+ @results[:b3].should == [@t, @t.args, @recorder]
614
+ @results[:c3].should == [@t, @t.args, [@recorder]]
615
+ end
616
+ end
617
+ end
618
+ end
619
+ end
620
+
621
+ #
622
+ # Pokies
623
+ #
624
+
625
+ describe "sitting at a poker machine" do
626
+
627
+ before :all do
628
+ make_pristine_class('PokerMachine') do
629
+
630
+ attr_accessor :silly_noises_inflicted
631
+
632
+ def insert_coins n
633
+ @credits = n * PokerMachine::CREDITS_PER_COIN
634
+ end
635
+
636
+ # sets coins to 0 and returns what it was
637
+ def refund_coins
638
+ (self.credits, x = 0, self.credits / PokerMachine::CREDITS_PER_COIN)[1]
639
+ end
640
+
641
+ def play_a_silly_noise
642
+ @silly_noises_inflicted << [:silly_noise]
643
+ end
644
+
645
+ # an array with the accessors (StateFu::Bindings)
646
+ # for each of the wheels' state machines, for convenience
647
+ def wheels
648
+ [wheel_one, wheel_two, wheel_three]
649
+ end
650
+
651
+ def wheels_spinning?
652
+ wheels.any?(&:spinning?)
653
+ end
654
+
655
+ def display
656
+ wheels.map(&:current_state_name)
657
+ end
658
+
659
+ def wait
660
+ while wheels_spinning?
661
+ spin_wheels!
662
+ end
663
+ stop_spinning!
664
+ end
665
+
666
+ PokerMachine::CREDITS_TO_PLAY = 5
667
+ PokerMachine::CREDITS_PER_COIN = 5
668
+
669
+ attr_accessor :credits
670
+
671
+ def initialize
672
+ @credits = 0
673
+ @silly_noises_inflicted = []
674
+ end
675
+
676
+ machine do
677
+ # adds a hook to the machine's global after slot
678
+ after_everything :play_a_silly_noise
679
+
680
+ # Define helper methods with 'proc' or its alias 'define'. This is
681
+ # implicit when you supply a block and a symbol for an event or state
682
+ # hook, a requirement, or a requirement failure message.
683
+ #
684
+ # Named procs are "machine-local": they are available in any other
685
+ # block evaluated by StateFu for a given machine, but are not defined
686
+ # on the stateful class itself.
687
+ #
688
+ # Use them to extend the state machine DSL without cluttering up your
689
+ # classes themselves.
690
+ #
691
+ # If you want a method which spans multiple machines (eg 'wheels',
692
+ # above) or which is available to your object in any context, define
693
+ # it as a standard method. You will then be able to access it in any
694
+ # of your state machines.
695
+ named_proc(:wheel_states) { wheels.map(&:current_state) }
696
+ named_proc(:wheels_stopped?) do
697
+ !wheels.any?(&:spinning?)
698
+ end
699
+
700
+ state :ready do
701
+
702
+ event :pull_lever, :transitions_to => :spinning do
703
+ # The execution context always provides handy access to all the
704
+ # methods of the PokerMachine instance - however, constants must
705
+ # still be qualified.
706
+ requires(:enough_credits) { self.credits >= PokerMachine::CREDITS_TO_PLAY }
707
+ triggers(:deduct_credits) { self.credits -= PokerMachine::CREDITS_TO_PLAY }
708
+ triggers(:spin_wheels) { [wheel_one, wheel_two,wheel_three].each(&:start!) }
709
+ # if we enable this line, the machine will #wait automatically
710
+ # so that merely pulling the lever causes it to return to the ready state:
711
+ #
712
+ # after :wait
713
+ end # :pull_lever event
714
+ end # :ready state
715
+
716
+ state :spinning do
717
+ cycle :spin_wheels do
718
+ # executes after the transition has been accepted
719
+ after do
720
+ wheels.each do |wheel|
721
+ if wheel.spinning?
722
+ wheel.spin!
723
+ end
724
+ end
725
+ end # execute
726
+ end # :spinning state
727
+
728
+ event :stop_spinning, :to => :ready do
729
+ requires :wheels_stopped?
730
+ execute :payout do
731
+ if wheel_states == wheel_states.uniq
732
+ self.credits += wheel_states.first[:value]
733
+ end
734
+ end
735
+ end # :stop_spinning event
736
+ end # spinning state
737
+ end # default machine
738
+
739
+ [:one, :two, :three].each do |wheel|
740
+ machine "wheel_#{wheel}" do
741
+
742
+ state :bomb, :value => -5
743
+ state :cherry, :value => 5
744
+ state :smiley, :value => 10
745
+ state :gold, :value => 15
746
+
747
+ state :spinning do
748
+ cycle :spin do
749
+ execute do
750
+ silly_noises_inflicted << :spinning_noise
751
+ end
752
+ after do
753
+ if rand(3) == 0
754
+ # we use binding.stop! rather than self.stop! here
755
+ # to disambiguate which machine we're sending the event to.
756
+ #
757
+ # .binding yields a StateFu::Binding, which has all the same
758
+ # magic methods as @pokie, but is explicitly for one machine,
759
+ # and one @pokie.
760
+ #
761
+ # @pokie.stop! would always cause the same wheel to stop
762
+ # (the first one, becuase it was defined first, and automatically
763
+ # defined methods never clobber any pre-existing methods) -
764
+ # which isn't what we want here.
765
+ binding.stop!([:bomb, :cherry, :smiley, :gold].rand)
766
+ end
767
+ end
768
+ end
769
+ end
770
+
771
+ initial_state states.except(:spinning).rand
772
+
773
+ event :start, :from => states.except(:spinning), :to => :spinning
774
+ event :stop, :from => :spinning, :to => states.except(:spinning)
775
+
776
+ end # machine :cell_#{cell}
777
+ end # each cell
778
+ end # PokerMachine
779
+ end # before
780
+
781
+ describe "the state machine" do
782
+ end
783
+
784
+ before :each do
785
+ @pokie = PokerMachine.new
786
+ end
787
+
788
+ # just a sanity check for method_missing
789
+ it "doesn't talk to you" do
790
+ lambda { @pokie.talk_to_me }.should raise_error(NoMethodError)
791
+ end
792
+
793
+ it "you need credits to pull the lever" do
794
+ @pokie.state_fu!
795
+ @pokie.credits.should == 0
796
+ @pokie.state_fu!
797
+ @pokie.can_pull_lever?.should == false
798
+ lambda { @pokie.pull_lever! }.should raise_error(StateFu::RequirementError)
799
+ end
800
+
801
+ it "has three wheels" do
802
+ @pokie.wheels.length.should == 3
803
+ end
804
+
805
+ it "displays three icons" do
806
+ @pokie.display.should be_kind_of(Array)
807
+ @pokie.display.map(&:class).should == [Symbol, Symbol, Symbol]
808
+ (@pokie.display - [:bomb, :cherry, :smiley, :gold]).should be_empty
809
+ end
810
+
811
+ describe "putting in 20 coins" do
812
+ before do
813
+ @pokie.insert_coins(20)
814
+ end
815
+
816
+ it "gives you 100 credits" do
817
+ @pokie.credits.should == 100
818
+ end
819
+
820
+ describe "then pulling the lever" do
821
+
822
+ it "spins the icons" do
823
+ @pokie.pull_lever!
824
+ @pokie.display.should == [:spinning, :spinning, :spinning]
825
+ end
826
+
827
+ it "takes away credits" do
828
+ credits_before_pulling_lever = @pokie.credits
829
+ @pokie.pull_lever!
830
+ @pokie.credits.should == credits_before_pulling_lever - PokerMachine::CREDITS_TO_PLAY
831
+ end
832
+
833
+ it "makes a silly noise" do
834
+ lambda { @pokie.pull_lever! }.should change(@pokie.silly_noises_inflicted, :length)
835
+ end
836
+
837
+ it "wont let you pull it again while it's still spinning" do
838
+ @pokie.pull_lever!
839
+ @pokie.spinning?.should be_true
840
+ @pokie.can_pull_lever?.should == nil
841
+ lambda{ @pokie.pull_lever! }.should raise_error(StateFu::InvalidTransition)
842
+ end
843
+
844
+ it "makes a spinning sound while you wait" do
845
+ @pokie.pull_lever!
846
+ noises_before = @pokie.silly_noises_inflicted
847
+ @pokie.wait
848
+ (@pokie.silly_noises_inflicted).should include(:spinning_noise)
849
+ end
850
+
851
+ it "it stops spinning after a little #wait" do
852
+ @pokie.pull_lever!
853
+ @pokie.wait
854
+ @pokie.spinning?.should be_false
855
+ end
856
+
857
+ it "gives you more credits if all the icons are the same" do
858
+ @pokie.pull_lever!
859
+ @pokie.wheel_one.stop! :smiley
860
+ @pokie.wheel_two.stop! :smiley
861
+ @pokie.wheel_three.stop! :smiley
862
+ @pokie.wait
863
+ @pokie.credits.should == 105
864
+ end
865
+ end
866
+ end
867
+ end
868
+
869
+
870
+