joelind-state_machine 0.8.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 (78) hide show
  1. data/CHANGELOG.rdoc +297 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +388 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +252 -0
  33. data/lib/state_machine/event_collection.rb +122 -0
  34. data/lib/state_machine/extensions.rb +149 -0
  35. data/lib/state_machine/guard.rb +230 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +492 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +351 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +322 -0
  43. data/lib/state_machine/machine.rb +1467 -0
  44. data/lib/state_machine/machine_collection.rb +155 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +394 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +120 -0
  60. data/test/unit/event_collection_test.rb +326 -0
  61. data/test/unit/event_test.rb +743 -0
  62. data/test/unit/guard_test.rb +908 -0
  63. data/test/unit/integrations/active_record_test.rb +1374 -0
  64. data/test/unit/integrations/data_mapper_test.rb +962 -0
  65. data/test/unit/integrations/sequel_test.rb +859 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +938 -0
  70. data/test/unit/machine_test.rb +2004 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1212 -0
  78. metadata +163 -0
@@ -0,0 +1,962 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ begin
4
+ # Load library
5
+ require 'rubygems'
6
+
7
+ gem 'dm-core', ENV['DM_VERSION'] ? "=#{ENV['DM_VERSION']}" : '>=0.9.4'
8
+ require 'dm-core'
9
+
10
+ # Establish database connection
11
+ DataMapper.setup(:default, 'sqlite3::memory:')
12
+ DataObjects::Sqlite3.logger = DataObjects::Logger.new("#{File.dirname(__FILE__)}/../../data_mapper.log", :info)
13
+
14
+ module DataMapperTest
15
+ class BaseTestCase < Test::Unit::TestCase
16
+ def default_test
17
+ end
18
+
19
+ protected
20
+ # Creates a new DataMapper resource (and the associated table)
21
+ def new_resource(auto_migrate = true, &block)
22
+ resource = Class.new do
23
+ include DataMapper::Resource
24
+
25
+ storage_names[:default] = 'foo'
26
+ def self.name; 'DataMapperTest::Foo'; end
27
+
28
+ property :id, DataMapper::Types::Serial
29
+ property :state, String
30
+
31
+ auto_migrate! if auto_migrate
32
+ end
33
+ resource.class_eval(&block) if block_given?
34
+ resource
35
+ end
36
+
37
+ # Creates a new DataMapper observer
38
+ def new_observer(resource, &block)
39
+ observer = Class.new do
40
+ include DataMapper::Observer
41
+ end
42
+ observer.observe(resource)
43
+ observer.class_eval(&block) if block_given?
44
+ observer
45
+ end
46
+ end
47
+
48
+ class IntegrationTest < BaseTestCase
49
+ def test_should_match_if_class_inherits_from_active_record
50
+ assert StateMachine::Integrations::DataMapper.matches?(new_resource)
51
+ end
52
+
53
+ def test_should_not_match_if_class_does_not_inherit_from_active_record
54
+ assert !StateMachine::Integrations::DataMapper.matches?(Class.new)
55
+ end
56
+ end
57
+
58
+ class MachineByDefaultTest < BaseTestCase
59
+ def setup
60
+ @resource = new_resource
61
+ @machine = StateMachine::Machine.new(@resource)
62
+ end
63
+
64
+ def test_should_use_save_as_action
65
+ assert_equal :save, @machine.action
66
+ end
67
+
68
+ def test_should_not_use_transactions
69
+ assert_equal false, @machine.use_transactions
70
+ end
71
+ end
72
+
73
+ class MachineTest < BaseTestCase
74
+ def setup
75
+ @resource = new_resource
76
+ @machine = StateMachine::Machine.new(@resource)
77
+ @machine.state :parked, :first_gear
78
+ @machine.state :idling, :value => lambda {'idling'}
79
+ end
80
+
81
+ def test_should_create_singular_with_scope
82
+ assert @resource.respond_to?(:with_state)
83
+ end
84
+
85
+ def test_should_only_include_records_with_state_in_singular_with_scope
86
+ parked = @resource.create :state => 'parked'
87
+ idling = @resource.create :state => 'idling'
88
+
89
+ assert_equal [parked], @resource.with_state(:parked)
90
+ end
91
+
92
+ def test_should_create_plural_with_scope
93
+ assert @resource.respond_to?(:with_states)
94
+ end
95
+
96
+ def test_should_only_include_records_with_states_in_plural_with_scope
97
+ parked = @resource.create :state => 'parked'
98
+ idling = @resource.create :state => 'idling'
99
+
100
+ assert_equal [parked, idling], @resource.with_states(:parked, :idling)
101
+ end
102
+
103
+ def test_should_create_singular_without_scope
104
+ assert @resource.respond_to?(:without_state)
105
+ end
106
+
107
+ def test_should_only_include_records_without_state_in_singular_without_scope
108
+ parked = @resource.create :state => 'parked'
109
+ idling = @resource.create :state => 'idling'
110
+
111
+ assert_equal [parked], @resource.without_state(:idling)
112
+ end
113
+
114
+ def test_should_create_plural_without_scope
115
+ assert @resource.respond_to?(:without_states)
116
+ end
117
+
118
+ def test_should_only_include_records_without_states_in_plural_without_scope
119
+ parked = @resource.create :state => 'parked'
120
+ idling = @resource.create :state => 'idling'
121
+ first_gear = @resource.create :state => 'first_gear'
122
+
123
+ assert_equal [parked, idling], @resource.without_states(:first_gear)
124
+ end
125
+
126
+ def test_should_allow_chaining_scopes
127
+ parked = @resource.create :state => 'parked'
128
+ idling = @resource.create :state => 'idling'
129
+
130
+ assert_equal [idling], @resource.without_state(:parked).with_state(:idling)
131
+ end
132
+
133
+ def test_should_not_rollback_transaction_if_false
134
+ @machine.within_transaction(@resource.new) do
135
+ @resource.create
136
+ false
137
+ end
138
+
139
+ assert_equal 1, @resource.all.size
140
+ end
141
+
142
+ def test_should_not_rollback_transaction_if_true
143
+ @machine.within_transaction(@resource.new) do
144
+ @resource.create
145
+ true
146
+ end
147
+
148
+ assert_equal 1, @resource.all.size
149
+ end
150
+
151
+ def test_should_not_override_the_column_reader
152
+ record = @resource.new
153
+ record.attribute_set(:state, 'parked')
154
+ assert_equal 'parked', record.state
155
+ end
156
+
157
+ def test_should_not_override_the_column_writer
158
+ record = @resource.new
159
+ record.state = 'parked'
160
+ assert_equal 'parked', record.attribute_get(:state)
161
+ end
162
+ end
163
+
164
+ class MachineUnmigratedTest < BaseTestCase
165
+ def setup
166
+ @resource = new_resource(false)
167
+ end
168
+
169
+ def test_should_allow_machine_creation
170
+ assert_nothing_raised { StateMachine::Machine.new(@resource) }
171
+ end
172
+ end
173
+
174
+ class MachineWithInitialStateTest < BaseTestCase
175
+ def setup
176
+ @resource = new_resource
177
+ @machine = StateMachine::Machine.new(@resource, :initial => 'parked')
178
+ @record = @resource.new
179
+ end
180
+
181
+ def test_should_set_initial_state_on_created_object
182
+ assert_equal 'parked', @record.state
183
+ end
184
+ end
185
+
186
+ class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
187
+ def setup
188
+ @resource = new_resource do
189
+ def initialize
190
+ # Skip attribute initialization
191
+ @initialized_state_machines = true
192
+ super
193
+ end
194
+ end
195
+
196
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => 'parked')
197
+ @record = @resource.new
198
+ end
199
+
200
+ def test_should_define_a_new_property_for_the_attribute
201
+ assert_not_nil @resource.properties[:status]
202
+ assert @record.respond_to?(:status)
203
+ assert @record.respond_to?(:status=)
204
+ end
205
+ end
206
+
207
+ class MachineWithColumnDefaultTest < BaseTestCase
208
+ def setup
209
+ @resource = new_resource do
210
+ property :status, String, :default => 'idling'
211
+ auto_migrate!
212
+ end
213
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
214
+ @record = @resource.new
215
+ end
216
+
217
+ def test_should_use_machine_default
218
+ assert_equal 'parked', @record.status
219
+ end
220
+ end
221
+
222
+ class MachineWithComplexPluralizationTest < BaseTestCase
223
+ def setup
224
+ @resource = new_resource
225
+ @machine = StateMachine::Machine.new(@resource, :status)
226
+ end
227
+
228
+ def test_should_create_singular_with_scope
229
+ assert @resource.respond_to?(:with_status)
230
+ end
231
+
232
+ def test_should_create_plural_with_scope
233
+ assert @resource.respond_to?(:with_statuses)
234
+ end
235
+ end
236
+
237
+ class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
238
+ def setup
239
+ @resource = new_resource do
240
+ attr_accessor :status
241
+ end
242
+
243
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => 'parked')
244
+ @record = @resource.new
245
+ end
246
+
247
+ def test_should_set_initial_state_on_created_object
248
+ assert_equal 'parked', @record.status
249
+ end
250
+ end
251
+
252
+ class MachineWithOwnerSubclassTest < BaseTestCase
253
+ def setup
254
+ @resource = new_resource
255
+ @machine = StateMachine::Machine.new(@resource, :state)
256
+
257
+ @subclass = Class.new(@resource)
258
+ @subclass_machine = @subclass.state_machine(:state) {}
259
+ @subclass_machine.state :parked, :idling, :first_gear
260
+ end
261
+
262
+ def test_should_only_include_records_with_subclass_states_in_with_scope
263
+ parked = @subclass.create :state => 'parked'
264
+ idling = @subclass.create :state => 'idling'
265
+
266
+ assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
267
+ end
268
+
269
+ def test_should_only_include_records_without_subclass_states_in_without_scope
270
+ parked = @subclass.create :state => 'parked'
271
+ idling = @subclass.create :state => 'idling'
272
+ first_gear = @subclass.create :state => 'first_gear'
273
+
274
+ assert_equal [parked, idling], @subclass.without_states(:first_gear)
275
+ end
276
+ end
277
+
278
+ class MachineWithTransactionsTest < BaseTestCase
279
+ def setup
280
+ @resource = new_resource
281
+ @machine = StateMachine::Machine.new(@resource, :use_transactions => true)
282
+ end
283
+
284
+ def test_should_rollback_transaction_if_false
285
+ @machine.within_transaction(@resource.new) do
286
+ @resource.create
287
+ false
288
+ end
289
+
290
+ assert_equal 0, @resource.all.size
291
+ end
292
+
293
+ def test_should_not_rollback_transaction_if_true
294
+ @machine.within_transaction(@resource.new) do
295
+ @resource.create
296
+ true
297
+ end
298
+
299
+ assert_equal 1, @resource.all.size
300
+ end
301
+ end
302
+
303
+ class MachineWithInitializedStateTest < BaseTestCase
304
+ def setup
305
+ @resource = new_resource
306
+ @machine = StateMachine::Machine.new(@resource, :initial => :parked)
307
+ @machine.state nil, :idling
308
+ end
309
+
310
+ def test_should_allow_nil_initial_state_when_static
311
+ record = @resource.new(:state => nil)
312
+ assert_nil record.state
313
+ end
314
+
315
+ def test_should_allow_nil_initial_state_when_dynamic
316
+ @machine.initial_state = lambda {:parked}
317
+ record = @resource.new(:state => nil)
318
+ assert_nil record.state
319
+ end
320
+
321
+ def test_should_allow_different_initial_state_when_static
322
+ record = @resource.new(:state => 'idling')
323
+ assert_equal 'idling', record.state
324
+ end
325
+
326
+ def test_should_allow_different_initial_state_when_dynamic
327
+ @machine.initial_state = lambda {:parked}
328
+ record = @resource.new(:state => 'idling')
329
+ assert_equal 'idling', record.state
330
+ end
331
+
332
+ def test_should_use_default_state_if_protected
333
+ @resource.class_eval do
334
+ protected :state=
335
+ end
336
+
337
+ assert_raise(ArgumentError) { @resource.new(:state => 'idling') }
338
+ end
339
+ end
340
+
341
+ class MachineWithCallbacksTest < BaseTestCase
342
+ def setup
343
+ @resource = new_resource
344
+ @machine = StateMachine::Machine.new(@resource)
345
+ @machine.state :parked, :idling
346
+ @machine.event :ignite
347
+ @record = @resource.new(:state => 'parked')
348
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
349
+ end
350
+
351
+ def test_should_run_before_callbacks
352
+ called = false
353
+ @machine.before_transition(lambda {called = true})
354
+
355
+ @transition.perform
356
+ assert called
357
+ end
358
+
359
+ def test_should_pass_transition_to_before_callbacks_with_one_argument
360
+ transition = nil
361
+ @machine.before_transition(lambda {|arg| transition = arg})
362
+
363
+ @transition.perform
364
+ assert_equal @transition, transition
365
+ end
366
+
367
+ def test_should_pass_transition_to_before_callbacks_with_multiple_arguments
368
+ callback_args = nil
369
+ @machine.before_transition(lambda {|*args| callback_args = args})
370
+
371
+ @transition.perform
372
+ assert_equal [@transition], callback_args
373
+ end
374
+
375
+ def test_should_run_before_callbacks_within_the_context_of_the_record
376
+ context = nil
377
+ @machine.before_transition(lambda {context = self})
378
+
379
+ @transition.perform
380
+ assert_equal @record, context
381
+ end
382
+
383
+ def test_should_run_after_callbacks
384
+ called = false
385
+ @machine.after_transition(lambda {called = true})
386
+
387
+ @transition.perform
388
+ assert called
389
+ end
390
+
391
+ def test_should_pass_transition_to_after_callbacks_with_multiple_arguments
392
+ callback_args = nil
393
+ @machine.after_transition(lambda {|*args| callback_args = args})
394
+
395
+ @transition.perform
396
+ assert_equal [@transition], callback_args
397
+ end
398
+
399
+ def test_should_run_after_callbacks_with_the_context_of_the_record
400
+ context = nil
401
+ @machine.after_transition(lambda {context = self})
402
+
403
+ @transition.perform
404
+ assert_equal @record, context
405
+ end
406
+
407
+ def test_should_allow_symbolic_callbacks
408
+ callback_args = nil
409
+
410
+ klass = class << @record; self; end
411
+ klass.send(:define_method, :after_ignite) do |*args|
412
+ callback_args = args
413
+ end
414
+
415
+ @machine.before_transition(:after_ignite)
416
+
417
+ @transition.perform
418
+ assert_equal [@transition], callback_args
419
+ end
420
+
421
+ def test_should_allow_string_callbacks
422
+ class << @record
423
+ attr_reader :callback_result
424
+ end
425
+
426
+ @machine.before_transition('@callback_result = [1, 2, 3]')
427
+ @transition.perform
428
+
429
+ assert_equal [1, 2, 3], @record.callback_result
430
+ end
431
+ end
432
+
433
+ class MachineWithLoopbackTest < BaseTestCase
434
+ def setup
435
+ dirty_attributes = nil
436
+
437
+ @resource = new_resource do
438
+ property :updated_at, DateTime
439
+ auto_migrate!
440
+
441
+ # Simulate dm-timestamps
442
+ before :update do
443
+ dirty_attributes = self.dirty_attributes.dup
444
+
445
+ return unless dirty?
446
+ self.updated_at = DateTime.now
447
+ end
448
+ end
449
+
450
+ @machine = StateMachine::Machine.new(@resource, :initial => :parked)
451
+ @machine.event :park
452
+
453
+ @record = @resource.create(:updated_at => Time.now - 1)
454
+ @timestamp = @record.updated_at
455
+
456
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
457
+ @transition.perform
458
+
459
+ @dirty_attributes = dirty_attributes
460
+ end
461
+
462
+ def test_should_include_state_in_dirty_attributes
463
+ expected = {@resource.properties[:state] => 'parked'}
464
+ assert_equal expected, @dirty_attributes
465
+ end
466
+
467
+ def test_should_update_record
468
+ assert_not_equal @timestamp, @record.updated_at
469
+ end
470
+ end
471
+
472
+ begin
473
+ gem 'dm-observer', ENV['DM_VERSION'] ? "=#{ENV['DM_VERSION']}" : '>=0.9.4'
474
+ require 'dm-observer'
475
+
476
+ class MachineWithObserversTest < BaseTestCase
477
+ def setup
478
+ @resource = new_resource
479
+ @machine = StateMachine::Machine.new(@resource)
480
+ @machine.state :parked, :idling
481
+ @machine.event :ignite
482
+ @record = @resource.new(:state => 'parked')
483
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
484
+ end
485
+
486
+ def test_should_provide_matcher_helpers
487
+ matchers = []
488
+
489
+ new_observer(@resource) do
490
+ matchers = [all, any, same]
491
+ end
492
+
493
+ assert_equal [StateMachine::AllMatcher.instance, StateMachine::AllMatcher.instance, StateMachine::LoopbackMatcher.instance], matchers
494
+ end
495
+
496
+ def test_should_call_before_transition_callback_if_requirements_match
497
+ called = false
498
+
499
+ observer = new_observer(@resource) do
500
+ before_transition :from => :parked do
501
+ called = true
502
+ end
503
+ end
504
+
505
+ @transition.perform
506
+ assert called
507
+ end
508
+
509
+ def test_should_not_call_before_transition_callback_if_requirements_do_not_match
510
+ called = false
511
+
512
+ observer = new_observer(@resource) do
513
+ before_transition :from => :idling do
514
+ called = true
515
+ end
516
+ end
517
+
518
+ @transition.perform
519
+ assert !called
520
+ end
521
+
522
+ def test_should_pass_transition_to_before_callbacks
523
+ callback_args = nil
524
+
525
+ observer = new_observer(@resource) do
526
+ before_transition do |*args|
527
+ callback_args = args
528
+ end
529
+ end
530
+
531
+ @transition.perform
532
+ assert_equal [@transition], callback_args
533
+ end
534
+
535
+ def test_should_call_after_transition_callback_if_requirements_match
536
+ called = false
537
+
538
+ observer = new_observer(@resource) do
539
+ after_transition :from => :parked do
540
+ called = true
541
+ end
542
+ end
543
+
544
+ @transition.perform
545
+ assert called
546
+ end
547
+
548
+ def test_should_not_call_after_transition_callback_if_requirements_do_not_match
549
+ called = false
550
+
551
+ observer = new_observer(@resource) do
552
+ after_transition :from => :idling do
553
+ called = true
554
+ end
555
+ end
556
+
557
+ @transition.perform
558
+ assert !called
559
+ end
560
+
561
+ def test_should_pass_transition_to_after_callbacks
562
+ callback_args = nil
563
+
564
+ observer = new_observer(@resource) do
565
+ after_transition do |*args|
566
+ callback_args = args
567
+ end
568
+ end
569
+
570
+ @transition.perform
571
+ assert_equal [@transition], callback_args
572
+ end
573
+
574
+ def test_should_raise_exception_if_targeting_invalid_machine
575
+ assert_raise(RUBY_VERSION < '1.9' ? IndexError : KeyError) do
576
+ new_observer(@resource) do
577
+ before_transition :invalid, :from => :parked do
578
+ end
579
+ end
580
+ end
581
+ end
582
+
583
+ def test_should_allow_targeting_specific_machine
584
+ @second_machine = StateMachine::Machine.new(@resource, :status)
585
+ @resource.auto_migrate!
586
+
587
+ called_state = false
588
+ called_status = false
589
+
590
+ observer = new_observer(@resource) do
591
+ before_transition :state, :from => :parked do
592
+ called_state = true
593
+ end
594
+
595
+ before_transition :status, :from => :parked do
596
+ called_status = true
597
+ end
598
+ end
599
+
600
+ @transition.perform
601
+
602
+ assert called_state
603
+ assert !called_status
604
+ end
605
+
606
+ def test_should_allow_targeting_multiple_specific_machines
607
+ @second_machine = StateMachine::Machine.new(@resource, :status)
608
+ @second_machine.state :parked, :idling
609
+ @second_machine.event :ignite
610
+ @resource.auto_migrate!
611
+
612
+ called_attribute = nil
613
+
614
+ attributes = []
615
+ observer = new_observer(@resource) do
616
+ before_transition :state, :status, :from => :parked do |transition|
617
+ called_attribute = transition.attribute
618
+ end
619
+ end
620
+
621
+ @transition.perform
622
+ assert_equal :state, called_attribute
623
+
624
+ StateMachine::Transition.new(@record, @second_machine, :ignite, :parked, :idling).perform
625
+ assert_equal :status, called_attribute
626
+ end
627
+ end
628
+
629
+ class MachineWithMixedCallbacksTest < BaseTestCase
630
+ def setup
631
+ @resource = new_resource
632
+ @machine = StateMachine::Machine.new(@resource)
633
+ @machine.state :parked, :idling
634
+ @machine.event :ignite
635
+ @record = @resource.new(:state => 'parked')
636
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
637
+
638
+ @notifications = notifications = []
639
+
640
+ # Create callbacks
641
+ @machine.before_transition(lambda {notifications << :callback_before_transition})
642
+ @machine.after_transition(lambda {notifications << :callback_after_transition})
643
+
644
+ observer = new_observer(@resource) do
645
+ before_transition do
646
+ notifications << :observer_before_transition
647
+ end
648
+
649
+ after_transition do
650
+ notifications << :observer_after_transition
651
+ end
652
+ end
653
+
654
+ @transition.perform
655
+ end
656
+
657
+ def test_should_invoke_callbacks_in_specific_order
658
+ expected = [
659
+ :callback_before_transition,
660
+ :observer_before_transition,
661
+ :callback_after_transition,
662
+ :observer_after_transition
663
+ ]
664
+
665
+ assert_equal expected, @notifications
666
+ end
667
+ end
668
+ rescue LoadError
669
+ $stderr.puts "Skipping DataMapper Observer tests. `gem install dm-observer#{" -v #{ENV['DM_VERSION']}" if ENV['DM_VERSION']}` and try again."
670
+ end
671
+
672
+ begin
673
+ gem 'dm-validations', ENV['DM_VERSION'] ? "=#{ENV['DM_VERSION']}" : '>=0.9.4'
674
+ require 'dm-validations'
675
+
676
+ class MachineWithValidationsTest < BaseTestCase
677
+ def setup
678
+ @resource = new_resource
679
+ @machine = StateMachine::Machine.new(@resource)
680
+ @machine.state :parked
681
+
682
+ @record = @resource.new
683
+ end
684
+
685
+ def test_should_invalidate_using_errors
686
+ @record.state = 'parked'
687
+
688
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, :park]])
689
+ assert_equal ['cannot transition via "park"'], @record.errors.on(:state)
690
+ end
691
+
692
+ def test_should_auto_prefix_custom_attributes_on_invalidation
693
+ @machine.invalidate(@record, :event, :invalid)
694
+
695
+ assert_equal ['is invalid'], @record.errors.on(:state_event)
696
+ end
697
+
698
+ def test_should_clear_errors_on_reset
699
+ @record.state = 'parked'
700
+ @record.errors.add(:state, 'is invalid')
701
+
702
+ @machine.reset(@record)
703
+ assert_nil @record.errors.on(:id)
704
+ end
705
+
706
+ def test_should_be_valid_if_state_is_known
707
+ @record.state = 'parked'
708
+
709
+ assert @record.valid?
710
+ end
711
+
712
+ def test_should_not_be_valid_if_state_is_unknown
713
+ @record.state = 'invalid'
714
+
715
+ assert !@record.valid?
716
+ assert_equal ['is invalid'], @record.errors.on(:state)
717
+ end
718
+ end
719
+
720
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
721
+ def setup
722
+ @resource = new_resource
723
+ @machine = StateMachine::Machine.new(@resource, :status, :attribute => :state)
724
+ @machine.state :parked
725
+
726
+ @record = @resource.new
727
+ end
728
+
729
+ def test_should_add_validation_errors_to_custom_attribute
730
+ @record.state = 'invalid'
731
+
732
+ assert !@record.valid?
733
+ assert_equal ['is invalid'], @record.errors.on(:state)
734
+
735
+ @record.state = 'parked'
736
+ assert @record.valid?
737
+ end
738
+ end
739
+
740
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
741
+ def setup
742
+ @resource = new_resource do
743
+ attr_accessor :seatbelt
744
+ end
745
+
746
+ @machine = StateMachine::Machine.new(@resource)
747
+ @machine.state :first_gear, :second_gear do
748
+ validates_present :seatbelt
749
+ end
750
+ @machine.other_states :parked
751
+ end
752
+
753
+ def test_should_be_valid_if_validation_fails_outside_state_scope
754
+ record = @resource.new(:state => 'parked', :seatbelt => nil)
755
+ assert record.valid?
756
+ end
757
+
758
+ def test_should_be_invalid_if_validation_fails_within_state_scope
759
+ record = @resource.new(:state => 'first_gear', :seatbelt => nil)
760
+ assert !record.valid?
761
+ end
762
+
763
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
764
+ record = @resource.new(:state => 'second_gear', :seatbelt => true)
765
+ assert record.valid?
766
+ end
767
+ end
768
+
769
+ class MachineWithEventAttributesOnValidationTest < BaseTestCase
770
+ def setup
771
+ @resource = new_resource
772
+ @machine = StateMachine::Machine.new(@resource)
773
+ @machine.event :ignite do
774
+ transition :parked => :idling
775
+ end
776
+
777
+ @record = @resource.new
778
+ @record.state = 'parked'
779
+ @record.state_event = 'ignite'
780
+ end
781
+
782
+ def test_should_fail_if_event_is_invalid
783
+ @record.state_event = 'invalid'
784
+ assert !@record.valid?
785
+ assert_equal ['is invalid'], @record.errors.full_messages
786
+ end
787
+
788
+ def test_should_fail_if_event_has_no_transition
789
+ @record.state = 'idling'
790
+ assert !@record.valid?
791
+ assert_equal ['cannot transition when idling'], @record.errors.full_messages
792
+ end
793
+
794
+ def test_should_be_successful_if_event_has_transition
795
+ assert @record.valid?
796
+ end
797
+
798
+ def test_should_run_before_callbacks
799
+ ran_callback = false
800
+ @machine.before_transition { ran_callback = true }
801
+
802
+ @record.valid?
803
+ assert ran_callback
804
+ end
805
+
806
+ def test_should_persist_new_state
807
+ @record.valid?
808
+ assert_equal 'idling', @record.state
809
+ end
810
+
811
+ def test_should_not_run_after_callbacks
812
+ ran_callback = false
813
+ @machine.after_transition { ran_callback = true }
814
+
815
+ @record.valid?
816
+ assert !ran_callback
817
+ end
818
+
819
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
820
+ @resource.class_eval do
821
+ attr_accessor :seatbelt
822
+ validates_present :seatbelt
823
+ end
824
+
825
+ ran_callback = false
826
+ @machine.after_transition { ran_callback = true }
827
+
828
+ @record.valid?
829
+ assert !ran_callback
830
+ end
831
+
832
+ def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
833
+ @resource.class_eval do
834
+ attr_accessor :seatbelt
835
+ validates_present :seatbelt
836
+ end
837
+
838
+ ran_callback = false
839
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
840
+
841
+ @record.valid?
842
+ assert ran_callback
843
+ end
844
+ end
845
+
846
+ class MachineWithEventAttributesOnSaveTest < BaseTestCase
847
+ def setup
848
+ @resource = new_resource
849
+ @machine = StateMachine::Machine.new(@resource)
850
+ @machine.event :ignite do
851
+ transition :parked => :idling
852
+ end
853
+
854
+ @record = @resource.new
855
+ @record.state = 'parked'
856
+ @record.state_event = 'ignite'
857
+ end
858
+
859
+ def test_should_fail_if_event_is_invalid
860
+ @record.state_event = 'invalid'
861
+ assert !@record.save
862
+ end
863
+
864
+ def test_should_fail_if_event_has_no_transition
865
+ @record.state = 'idling'
866
+ assert !@record.save
867
+ end
868
+
869
+ def test_should_be_successful_if_event_has_transition
870
+ assert_equal true, @record.save
871
+ end
872
+
873
+ def test_should_run_before_callbacks
874
+ ran_callback = false
875
+ @machine.before_transition { ran_callback = true }
876
+
877
+ @record.save
878
+ assert ran_callback
879
+ end
880
+
881
+ def test_should_run_before_callbacks_once
882
+ before_count = 0
883
+ @machine.before_transition { before_count += 1 }
884
+
885
+ @record.save
886
+ assert_equal 1, before_count
887
+ end
888
+
889
+ def test_should_persist_new_state
890
+ @record.save
891
+ assert_equal 'idling', @record.state
892
+ end
893
+
894
+ def test_should_run_after_callbacks
895
+ ran_callback = false
896
+ @machine.after_transition { ran_callback = true }
897
+
898
+ @record.save
899
+ assert ran_callback
900
+ end
901
+
902
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
903
+ @resource.before(:create) { throw :halt }
904
+
905
+ ran_callback = false
906
+ @machine.after_transition { ran_callback = true }
907
+
908
+ @record.save
909
+ assert !ran_callback
910
+ end
911
+
912
+ def test_should_run_after_callbacks_with_failures_enabled_if_fails
913
+ @resource.before(:create) { throw :halt }
914
+
915
+ ran_callback = false
916
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
917
+
918
+ @record.save
919
+ assert ran_callback
920
+ end
921
+ end
922
+
923
+ class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
924
+ def setup
925
+ @superclass = new_resource do
926
+ def persist
927
+ save
928
+ end
929
+ end
930
+ @resource = Class.new(@superclass)
931
+ @machine = StateMachine::Machine.new(@resource, :action => :persist)
932
+ @machine.event :ignite do
933
+ transition :parked => :idling
934
+ end
935
+
936
+ @record = @resource.new
937
+ @record.state = 'parked'
938
+ @record.state_event = 'ignite'
939
+ end
940
+
941
+ def test_should_not_transition_on_valid?
942
+ @record.valid?
943
+ assert_equal 'parked', @record.state
944
+ end
945
+
946
+ def test_should_not_transition_on_save
947
+ @record.save
948
+ assert_equal 'parked', @record.state
949
+ end
950
+
951
+ def test_should_transition_on_custom_action
952
+ @record.persist
953
+ assert_equal 'idling', @record.state
954
+ end
955
+ end
956
+ rescue LoadError
957
+ $stderr.puts "Skipping DataMapper Validation tests. `gem install dm-validations#{" -v #{ENV['DM_VERSION']}" if ENV['DM_VERSION']}` and try again."
958
+ end
959
+ end
960
+ rescue LoadError
961
+ $stderr.puts "Skipping DataMapper tests. `gem install dm-core#{" -v #{ENV['DM_VERSION']}" if ENV['DM_VERSION']}`, `gem install cucumber rspec hoe launchy do_sqlite3` and try again."
962
+ end