state_machine 0.8.1 → 0.9.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 (42) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +162 -23
  4. data/Rakefile +3 -18
  5. data/lib/state_machine.rb +3 -4
  6. data/lib/state_machine/callback.rb +65 -13
  7. data/lib/state_machine/eval_helpers.rb +20 -4
  8. data/lib/state_machine/initializers.rb +4 -0
  9. data/lib/state_machine/initializers/merb.rb +1 -0
  10. data/lib/state_machine/initializers/rails.rb +7 -0
  11. data/lib/state_machine/integrations.rb +21 -6
  12. data/lib/state_machine/integrations/active_model.rb +414 -0
  13. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  14. data/lib/state_machine/integrations/{active_record → active_model}/observer.rb +7 -7
  15. data/lib/state_machine/integrations/active_record.rb +65 -129
  16. data/lib/state_machine/integrations/active_record/locale.rb +4 -11
  17. data/lib/state_machine/integrations/data_mapper.rb +24 -6
  18. data/lib/state_machine/integrations/data_mapper/observer.rb +36 -0
  19. data/lib/state_machine/integrations/mongo_mapper.rb +295 -0
  20. data/lib/state_machine/integrations/sequel.rb +33 -7
  21. data/lib/state_machine/machine.rb +121 -23
  22. data/lib/state_machine/machine_collection.rb +12 -103
  23. data/lib/state_machine/transition.rb +125 -164
  24. data/lib/state_machine/transition_collection.rb +244 -0
  25. data/lib/tasks/state_machine.rb +12 -15
  26. data/test/functional/state_machine_test.rb +11 -1
  27. data/test/unit/callback_test.rb +305 -32
  28. data/test/unit/eval_helpers_test.rb +103 -1
  29. data/test/unit/event_test.rb +2 -1
  30. data/test/unit/guard_test.rb +2 -1
  31. data/test/unit/integrations/active_model_test.rb +909 -0
  32. data/test/unit/integrations/active_record_test.rb +1542 -1292
  33. data/test/unit/integrations/data_mapper_test.rb +1369 -1041
  34. data/test/unit/integrations/mongo_mapper_test.rb +1349 -0
  35. data/test/unit/integrations/sequel_test.rb +1214 -985
  36. data/test/unit/integrations_test.rb +8 -0
  37. data/test/unit/machine_collection_test.rb +140 -513
  38. data/test/unit/machine_test.rb +212 -10
  39. data/test/unit/state_test.rb +2 -1
  40. data/test/unit/transition_collection_test.rb +2098 -0
  41. data/test/unit/transition_test.rb +704 -552
  42. metadata +16 -3
@@ -0,0 +1,1349 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ # Load library
4
+ require 'rubygems'
5
+
6
+ if ENV['VERSION'] && Gem::Version.new(ENV['VERSION']) <= Gem::Version.new('0.7.0') || !Gem.available?('>=0.7.0')
7
+ gem 'activesupport', '~>2.3'
8
+ require 'active_support'
9
+ end
10
+
11
+ gem 'mongo_mapper', ENV['VERSION'] ? "=#{ENV['VERSION']}" : '>=0.5.5'
12
+ require 'mongo_mapper'
13
+
14
+ # Establish database connection
15
+ MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017, {:logger => Logger.new("#{File.dirname(__FILE__)}/../../mongo_mapper.log")})
16
+ MongoMapper.database = 'test'
17
+
18
+ module MongoMapperTest
19
+ class BaseTestCase < Test::Unit::TestCase
20
+ def default_test
21
+ end
22
+
23
+ protected
24
+ # Creates a new MongoMapper model (and the associated table)
25
+ def new_model(table_name = :foo, &block)
26
+
27
+ model = Class.new do
28
+ include MongoMapper::Document
29
+ set_collection_name(table_name)
30
+
31
+ def self.name; "MongoMapperTest::#{collection_name}"; end
32
+ def self.to_s; "MongoMapperTest::#{collection_name}"; end
33
+
34
+ key :state, String
35
+ end
36
+ model.class_eval(&block) if block_given?
37
+ model.collection.remove
38
+ model
39
+ end
40
+ end
41
+
42
+ class IntegrationTest < BaseTestCase
43
+ def test_should_match_if_class_includes_mongo_mapper
44
+ assert StateMachine::Integrations::MongoMapper.matches?(new_model)
45
+ end
46
+
47
+ def test_should_not_match_if_class_does_not_include_mongo_mapper
48
+ assert !StateMachine::Integrations::MongoMapper.matches?(Class.new)
49
+ end
50
+
51
+ def test_should_have_defaults
52
+ assert_equal e = {:action => :save}, StateMachine::Integrations::MongoMapper.defaults
53
+ end
54
+ end
55
+
56
+ class MachineWithoutDatabaseTest < BaseTestCase
57
+ def setup
58
+ @model = new_model do
59
+ # Simulate the database not being available entirely
60
+ def self.connection
61
+ raise Mongo::ConnectionFailure
62
+ end
63
+ end
64
+ end
65
+
66
+ def test_should_allow_machine_creation
67
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
68
+ end
69
+ end
70
+
71
+ class MachineByDefaultTest < BaseTestCase
72
+ def setup
73
+ @model = new_model
74
+ @machine = StateMachine::Machine.new(@model)
75
+ end
76
+
77
+ def test_should_use_save_as_action
78
+ assert_equal :save, @machine.action
79
+ end
80
+
81
+ def test_should_not_have_any_before_callbacks
82
+ assert_equal 0, @machine.callbacks[:before].size
83
+ end
84
+
85
+ def test_should_not_have_any_after_callbacks
86
+ assert_equal 0, @machine.callbacks[:after].size
87
+ end
88
+ end
89
+
90
+ class MachineWithStaticInitialStateTest < BaseTestCase
91
+ def setup
92
+ @model = new_model do
93
+ attr_accessor :value
94
+ end
95
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
96
+ end
97
+
98
+ def test_should_set_initial_state_on_created_object
99
+ record = @model.new
100
+ assert_equal 'parked', record.state
101
+ end
102
+
103
+ def test_should_set_initial_state_with_nil_attributes
104
+ record = @model.new(nil)
105
+ assert_equal 'parked', record.state
106
+ end
107
+
108
+ def test_should_still_set_attributes
109
+ record = @model.new(:value => 1)
110
+ assert_equal 1, record.value
111
+ end
112
+
113
+ def test_should_not_allow_initialize_blocks
114
+ block_args = nil
115
+ record = @model.new do |*args|
116
+ block_args = args
117
+ end
118
+
119
+ assert_nil block_args
120
+ end
121
+
122
+ def test_should_set_initial_state_before_setting_attributes
123
+ @model.class_eval do
124
+ attr_accessor :state_during_setter
125
+
126
+ define_method(:value=) do |value|
127
+ self.state_during_setter = state
128
+ end
129
+ end
130
+
131
+ record = @model.new(:value => 1)
132
+ assert_equal 'parked', record.state_during_setter
133
+ end
134
+
135
+ def test_should_not_set_initial_state_after_already_initialized
136
+ record = @model.new(:value => 1)
137
+ assert_equal 'parked', record.state
138
+
139
+ record.state = 'idling'
140
+ record.attributes = {}
141
+ assert_equal 'idling', record.state
142
+ end
143
+
144
+ def test_should_use_stored_values_when_loading_from_database
145
+ @machine.state :idling
146
+
147
+ record = @model.find(@model.create(:state => 'idling').id)
148
+ assert_equal 'idling', record.state
149
+ end
150
+
151
+ def test_should_use_stored_values_when_loading_from_database_with_nil_state
152
+ @machine.state nil
153
+
154
+ record = @model.find(@model.create(:state => nil).id)
155
+ assert_nil record.state
156
+ end
157
+ end
158
+
159
+ class MachineWithDynamicInitialStateTest < BaseTestCase
160
+ def setup
161
+ @model = new_model do
162
+ attr_accessor :value
163
+ end
164
+ @machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked})
165
+ @machine.state :parked
166
+ end
167
+
168
+ def test_should_set_initial_state_on_created_object
169
+ record = @model.new
170
+ assert_equal 'parked', record.state
171
+ end
172
+
173
+ def test_should_still_set_attributes
174
+ record = @model.new(:value => 1)
175
+ assert_equal 1, record.value
176
+ end
177
+
178
+ def test_should_not_allow_initialize_blocks
179
+ block_args = nil
180
+ record = @model.new do |*args|
181
+ block_args = args
182
+ end
183
+
184
+ assert_nil block_args
185
+ end
186
+
187
+ def test_should_set_initial_state_after_setting_attributes
188
+ @model.class_eval do
189
+ attr_accessor :state_during_setter
190
+
191
+ define_method(:value=) do |value|
192
+ self.state_during_setter = state || 'nil'
193
+ end
194
+ end
195
+
196
+ record = @model.new(:value => 1)
197
+ assert_equal 'nil', record.state_during_setter
198
+ end
199
+
200
+ def test_should_not_set_initial_state_after_already_initialized
201
+ record = @model.new(:value => 1)
202
+ assert_equal 'parked', record.state
203
+
204
+ record.state = 'idling'
205
+ record.attributes = {}
206
+ assert_equal 'idling', record.state
207
+ end
208
+
209
+ def test_should_use_stored_values_when_loading_from_database
210
+ @machine.state :idling
211
+
212
+ record = @model.find(@model.create(:state => 'idling').id)
213
+ assert_equal 'idling', record.state
214
+ end
215
+
216
+ def test_should_use_stored_values_when_loading_from_database_with_nil_state
217
+ @machine.state nil
218
+
219
+ record = @model.find(@model.create(:state => nil).id)
220
+ assert_nil record.state
221
+ end
222
+ end
223
+
224
+ class MachineWithColumnDefaultTest < BaseTestCase
225
+ def setup
226
+ @model = new_model do
227
+ key :status, String, :default => 'idling'
228
+ end
229
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
230
+ @record = @model.new
231
+ end
232
+
233
+ def test_should_use_machine_default
234
+ assert_equal 'parked', @record.status
235
+ end
236
+ end
237
+
238
+ class MachineWithConflictingPredicateTest < BaseTestCase
239
+ def setup
240
+ @model = new_model do
241
+ def state?(*args)
242
+ true
243
+ end
244
+ end
245
+
246
+ @machine = StateMachine::Machine.new(@model)
247
+ @record = @model.new
248
+ end
249
+
250
+ def test_should_not_define_attribute_predicate
251
+ assert @record.state?
252
+ end
253
+ end
254
+
255
+ class MachineWithColumnStateAttributeTest < BaseTestCase
256
+ def setup
257
+ @model = new_model
258
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
259
+ @machine.other_states(:idling)
260
+
261
+ @record = @model.new
262
+ end
263
+
264
+ def test_should_not_override_the_column_reader
265
+ @record[:state] = 'parked'
266
+ assert_equal 'parked', @record.state
267
+ end
268
+
269
+ def test_should_not_override_the_column_writer
270
+ @record.state = 'parked'
271
+ assert_equal 'parked', @record[:state]
272
+ end
273
+
274
+ def test_should_have_an_attribute_predicate
275
+ assert @record.respond_to?(:state?)
276
+ end
277
+
278
+ def test_should_test_for_existence_on_predicate_without_parameters
279
+ assert @record.state?
280
+
281
+ @record.state = nil
282
+ assert !@record.state?
283
+ end
284
+
285
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
286
+ assert !@record.state?(:idling)
287
+ end
288
+
289
+ def test_should_return_true_for_predicate_if_matches_current_value
290
+ assert @record.state?(:parked)
291
+ end
292
+
293
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
294
+ assert_raise(IndexError) { @record.state?(:invalid) }
295
+ end
296
+ end
297
+
298
+ class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
299
+ def setup
300
+ @model = new_model do
301
+ def initialize
302
+ # Skip attribute initialization
303
+ @initialized_state_machines = true
304
+ super
305
+ end
306
+ end
307
+
308
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
309
+ @machine.other_states(:idling)
310
+ @record = @model.new
311
+ end
312
+
313
+ def test_should_define_a_new_key_for_the_attribute
314
+ assert_not_nil @model.keys[:status]
315
+ end
316
+
317
+ def test_should_define_a_reader_attribute_for_the_attribute
318
+ assert @record.respond_to?(:status)
319
+ end
320
+
321
+ def test_should_define_a_writer_attribute_for_the_attribute
322
+ assert @record.respond_to?(:status=)
323
+ end
324
+
325
+ def test_should_define_an_attribute_predicate
326
+ assert @record.respond_to?(:status?)
327
+ end
328
+ end
329
+
330
+ class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
331
+ def setup
332
+ @model = new_model do
333
+ attr_accessor :status
334
+ end
335
+
336
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
337
+ @machine.other_states(:idling)
338
+ @record = @model.new
339
+ end
340
+
341
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
342
+ assert !@record.status?(:idling)
343
+ end
344
+
345
+ def test_should_return_true_for_predicate_if_matches_current_value
346
+ assert @record.status?(:parked)
347
+ end
348
+
349
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
350
+ assert_raise(IndexError) { @record.status?(:invalid) }
351
+ end
352
+
353
+ def test_should_set_initial_state_on_created_object
354
+ assert_equal 'parked', @record.status
355
+ end
356
+ end
357
+
358
+ class MachineWithAliasedAttributeTest < BaseTestCase
359
+ def setup
360
+ @model = new_model do
361
+ alias_attribute :vehicle_status, :state
362
+ end
363
+
364
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :vehicle_status)
365
+ @machine.state :parked
366
+
367
+ @record = @model.new
368
+ end
369
+
370
+ def test_should_check_custom_attribute_for_predicate
371
+ @record.vehicle_status = nil
372
+ assert !@record.status?(:parked)
373
+
374
+ @record.vehicle_status = 'parked'
375
+ assert @record.status?(:parked)
376
+ end
377
+ end
378
+
379
+ class MachineWithInitializedStateTest < BaseTestCase
380
+ def setup
381
+ @model = new_model
382
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
383
+ @machine.state nil, :idling
384
+ end
385
+
386
+ def test_should_allow_nil_initial_state_when_static
387
+ record = @model.new(:state => nil)
388
+ assert_nil record.state
389
+ end
390
+
391
+ def test_should_allow_nil_initial_state_when_dynamic
392
+ @machine.initial_state = lambda {:parked}
393
+ record = @model.new(:state => nil)
394
+ assert_nil record.state
395
+ end
396
+
397
+ def test_should_allow_different_initial_state_when_static
398
+ record = @model.new(:state => 'idling')
399
+ assert_equal 'idling', record.state
400
+ end
401
+
402
+ def test_should_allow_different_initial_state_when_dynamic
403
+ @machine.initial_state = lambda {:parked}
404
+ record = @model.new(:state => 'idling')
405
+ assert_equal 'idling', record.state
406
+ end
407
+
408
+ if defined?(MongoMapper::Plugins::Protected)
409
+ def test_should_use_default_state_if_protected
410
+ @model.class_eval do
411
+ attr_protected :state
412
+ end
413
+
414
+ record = @model.new(:state => 'idling')
415
+ assert_equal 'parked', record.state
416
+ end
417
+ end
418
+ end
419
+
420
+ class MachineWithLoopbackTest < BaseTestCase
421
+ def setup
422
+ @model = new_model do
423
+ key :updated_at, Time
424
+
425
+ before_update do |record|
426
+ record.updated_at = Time.now
427
+ end
428
+ end
429
+
430
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
431
+ @machine.event :park
432
+
433
+ @record = @model.create(:updated_at => Time.now - 1)
434
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
435
+
436
+ @timestamp = @record.updated_at
437
+ @transition.perform
438
+ end
439
+
440
+ def test_should_update_record
441
+ assert_not_equal @timestamp, @record.updated_at
442
+ end
443
+ end
444
+
445
+ class MachineWithDirtyAttributesTest < BaseTestCase
446
+ def setup
447
+ @model = new_model
448
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
449
+ @machine.event :ignite
450
+ @machine.state :idling
451
+
452
+ @record = @model.create
453
+
454
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
455
+ @transition.perform(false)
456
+ end
457
+
458
+ def test_should_include_state_in_changed_attributes
459
+ assert_equal %w(state), @record.changed
460
+ end
461
+
462
+ def test_should_track_attribute_change
463
+ assert_equal %w(parked idling), @record.changes['state']
464
+ end
465
+
466
+ def test_should_not_reset_changes_on_multiple_transitions
467
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
468
+ transition.perform(false)
469
+
470
+ assert_equal %w(parked idling), @record.changes['state']
471
+ end
472
+
473
+ def test_should_not_have_changes_when_loaded_from_database
474
+ record = @model.find(@record.id)
475
+ assert !record.changed?
476
+ end
477
+ end
478
+
479
+ class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
480
+ def setup
481
+ @model = new_model
482
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
483
+ @machine.event :park
484
+
485
+ @record = @model.create
486
+
487
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
488
+ @transition.perform(false)
489
+ end
490
+
491
+ def test_should_include_state_in_changed_attributes
492
+ assert_equal %w(state), @record.changed
493
+ end
494
+
495
+ def test_should_track_attribute_changes
496
+ assert_equal %w(parked parked), @record.changes['state']
497
+ end
498
+ end
499
+
500
+ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
501
+ def setup
502
+ @model = new_model do
503
+ key :status, String, :default => 'idling'
504
+ end
505
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
506
+ @machine.event :ignite
507
+ @machine.state :idling
508
+
509
+ @record = @model.create
510
+
511
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
512
+ @transition.perform(false)
513
+ end
514
+
515
+ def test_should_include_state_in_changed_attributes
516
+ assert_equal %w(status), @record.changed
517
+ end
518
+
519
+ def test_should_track_attribute_change
520
+ assert_equal %w(parked idling), @record.changes['status']
521
+ end
522
+
523
+ def test_should_not_reset_changes_on_multiple_transitions
524
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
525
+ transition.perform(false)
526
+
527
+ assert_equal %w(parked idling), @record.changes['status']
528
+ end
529
+ end
530
+
531
+ class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
532
+ def setup
533
+ @model = new_model do
534
+ key :status, String, :default => 'idling'
535
+ end
536
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
537
+ @machine.event :park
538
+
539
+ @record = @model.create
540
+
541
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
542
+ @transition.perform(false)
543
+ end
544
+
545
+ def test_should_include_state_in_changed_attributes
546
+ assert_equal %w(status), @record.changed
547
+ end
548
+
549
+ def test_should_track_attribute_changes
550
+ assert_equal %w(parked parked), @record.changes['status']
551
+ end
552
+ end
553
+
554
+ class MachineWithCallbacksTest < BaseTestCase
555
+ def setup
556
+ @model = new_model
557
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
558
+ @machine.other_states :idling
559
+ @machine.event :ignite
560
+
561
+ @record = @model.new(:state => 'parked')
562
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
563
+ end
564
+
565
+ def test_should_run_before_callbacks
566
+ called = false
567
+ @machine.before_transition {called = true}
568
+
569
+ @transition.perform
570
+ assert called
571
+ end
572
+
573
+ def test_should_pass_record_to_before_callbacks_with_one_argument
574
+ record = nil
575
+ @machine.before_transition {|arg| record = arg}
576
+
577
+ @transition.perform
578
+ assert_equal @record, record
579
+ end
580
+
581
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
582
+ callback_args = nil
583
+ @machine.before_transition {|*args| callback_args = args}
584
+
585
+ @transition.perform
586
+ assert_equal [@record, @transition], callback_args
587
+ end
588
+
589
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
590
+ context = nil
591
+ @machine.before_transition {context = self}
592
+
593
+ @transition.perform
594
+ assert_equal self, context
595
+ end
596
+
597
+ def test_should_run_after_callbacks
598
+ called = false
599
+ @machine.after_transition {called = true}
600
+
601
+ @transition.perform
602
+ assert called
603
+ end
604
+
605
+ def test_should_pass_record_to_after_callbacks_with_one_argument
606
+ record = nil
607
+ @machine.after_transition {|arg| record = arg}
608
+
609
+ @transition.perform
610
+ assert_equal @record, record
611
+ end
612
+
613
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
614
+ callback_args = nil
615
+ @machine.after_transition {|*args| callback_args = args}
616
+
617
+ @transition.perform
618
+ assert_equal [@record, @transition], callback_args
619
+ end
620
+
621
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
622
+ context = nil
623
+ @machine.after_transition {context = self}
624
+
625
+ @transition.perform
626
+ assert_equal self, context
627
+ end
628
+
629
+ def test_should_run_around_callbacks
630
+ before_called = false
631
+ after_called = false
632
+ @machine.around_transition {|block| before_called = true; block.call; after_called = true}
633
+
634
+ @transition.perform
635
+ assert before_called
636
+ assert after_called
637
+ end
638
+
639
+ def test_should_include_transition_states_in_known_states
640
+ @machine.before_transition :to => :first_gear, :do => lambda {}
641
+
642
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
643
+ end
644
+
645
+ def test_should_allow_symbolic_callbacks
646
+ callback_args = nil
647
+
648
+ klass = class << @record; self; end
649
+ klass.send(:define_method, :after_ignite) do |*args|
650
+ callback_args = args
651
+ end
652
+
653
+ @machine.before_transition(:after_ignite)
654
+
655
+ @transition.perform
656
+ assert_equal [@transition], callback_args
657
+ end
658
+
659
+ def test_should_allow_string_callbacks
660
+ class << @record
661
+ attr_reader :callback_result
662
+ end
663
+
664
+ @machine.before_transition('@callback_result = [1, 2, 3]')
665
+ @transition.perform
666
+
667
+ assert_equal [1, 2, 3], @record.callback_result
668
+ end
669
+ end
670
+
671
+ class MachineWithFailedBeforeCallbacksTest < BaseTestCase
672
+ def setup
673
+ @callbacks = []
674
+
675
+ @model = new_model
676
+ @machine = StateMachine::Machine.new(@model)
677
+ @machine.state :parked, :idling
678
+ @machine.event :ignite
679
+ @machine.before_transition {@callbacks << :before_1; false}
680
+ @machine.before_transition {@callbacks << :before_2}
681
+ @machine.after_transition {@callbacks << :after}
682
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
683
+
684
+ @record = @model.new(:state => 'parked')
685
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
686
+ @result = @transition.perform
687
+ end
688
+
689
+ def test_should_be_successful
690
+ assert @result
691
+ end
692
+
693
+ def test_should_change_current_state
694
+ assert_equal 'idling', @record.state
695
+ end
696
+
697
+ def test_should_run_action
698
+ assert !@record.new_record?
699
+ end
700
+
701
+ def test_should_run_further_callbacks
702
+ assert_equal [:before_1, :before_2, :around_before, :around_after, :after], @callbacks
703
+ end
704
+ end
705
+
706
+ class MachineWithFailedActionTest < BaseTestCase
707
+ def setup
708
+ @model = new_model do
709
+ validates_inclusion_of :state, :within => %w(first_gear)
710
+ end
711
+
712
+ @machine = StateMachine::Machine.new(@model)
713
+ @machine.state :parked, :idling
714
+ @machine.event :ignite
715
+
716
+ @callbacks = []
717
+ @machine.before_transition {@callbacks << :before}
718
+ @machine.after_transition {@callbacks << :after}
719
+ @machine.after_transition(:include_failures => true) {@callbacks << :after_failure}
720
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
721
+ @machine.around_transition(:include_failures => true) do |block|
722
+ @callbacks << :around_before_failure
723
+ block.call
724
+ @callbacks << :around_after_failure
725
+ end
726
+
727
+ @record = @model.new(:state => 'parked')
728
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
729
+ @result = @transition.perform
730
+ end
731
+
732
+ def test_should_not_be_successful
733
+ assert !@result
734
+ end
735
+
736
+ def test_should_not_change_current_state
737
+ assert_equal 'parked', @record.state
738
+ end
739
+
740
+ def test_should_not_save_record
741
+ assert @record.new_record?
742
+ end
743
+
744
+ def test_should_run_before_callbacks_and_after_callbacks_with_failures
745
+ assert_equal [:before, :around_before, :around_before_failure, :around_after_failure, :after_failure], @callbacks
746
+ end
747
+ end
748
+
749
+ class MachineWithFailedAfterCallbacksTest < BaseTestCase
750
+ def setup
751
+ @callbacks = []
752
+
753
+ @model = new_model
754
+ @machine = StateMachine::Machine.new(@model)
755
+ @machine.state :parked, :idling
756
+ @machine.event :ignite
757
+ @machine.after_transition {@callbacks << :after_1; false}
758
+ @machine.after_transition {@callbacks << :after_2}
759
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
760
+
761
+ @record = @model.new(:state => 'parked')
762
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
763
+ @result = @transition.perform
764
+ end
765
+
766
+ def test_should_be_successful
767
+ assert @result
768
+ end
769
+
770
+ def test_should_change_current_state
771
+ assert_equal 'idling', @record.state
772
+ end
773
+
774
+ def test_should_save_record
775
+ assert !@record.new_record?
776
+ end
777
+
778
+ def test_should_still_run_further_after_callbacks
779
+ assert_equal [:around_before, :around_after, :after_1, :after_2], @callbacks
780
+ end
781
+ end
782
+
783
+ class MachineWithValidationsTest < BaseTestCase
784
+ def setup
785
+ @model = new_model
786
+ @machine = StateMachine::Machine.new(@model)
787
+ @machine.state :parked
788
+
789
+ @record = @model.new
790
+ end
791
+
792
+ def test_should_invalidate_using_errors
793
+ @record.state = 'parked'
794
+
795
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, :park]])
796
+ assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
797
+ end
798
+
799
+ def test_should_auto_prefix_custom_attributes_on_invalidation
800
+ @machine.invalidate(@record, :event, :invalid)
801
+
802
+ assert_equal ['State event is invalid'], @record.errors.full_messages
803
+ end
804
+
805
+ def test_should_clear_errors_on_reset
806
+ @record.state = 'parked'
807
+ @record.errors.add(:state, 'is invalid')
808
+
809
+ @machine.reset(@record)
810
+ assert_equal [], @record.errors.full_messages
811
+ end
812
+
813
+ def test_should_be_valid_if_state_is_known
814
+ @record.state = 'parked'
815
+
816
+ assert @record.valid?
817
+ end
818
+
819
+ def test_should_not_be_valid_if_state_is_unknown
820
+ @record.state = 'invalid'
821
+
822
+ assert !@record.valid?
823
+ assert_equal ['State is invalid'], @record.errors.full_messages
824
+ end
825
+ end
826
+
827
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
828
+ def setup
829
+ @model = new_model do
830
+ alias_attribute :status, :state
831
+ end
832
+
833
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
834
+ @machine.state :parked
835
+
836
+ @record = @model.new
837
+ end
838
+
839
+ def test_should_add_validation_errors_to_custom_attribute
840
+ @record.state = 'invalid'
841
+
842
+ assert !@record.valid?
843
+ assert_equal ['State is invalid'], @record.errors.full_messages
844
+
845
+ @record.state = 'parked'
846
+ assert @record.valid?
847
+ end
848
+ end
849
+
850
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
851
+ def setup
852
+ @model = new_model do
853
+ attr_accessor :seatbealt
854
+ end
855
+
856
+ @machine = StateMachine::Machine.new(@model)
857
+ @machine.state :first_gear do
858
+ validates_presence_of :seatbelt, :key => :first_gear
859
+ end
860
+ @machine.state :second_gear do
861
+ validates_presence_of :seatbelt, :key => :second_gear
862
+ end
863
+ @machine.other_states :parked
864
+ end
865
+
866
+ def test_should_be_valid_if_validation_fails_outside_state_scope
867
+ record = @model.new(:state => 'parked', :seatbelt => nil)
868
+ assert record.valid?
869
+ end
870
+
871
+ def test_should_be_invalid_if_validation_fails_within_state_scope
872
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
873
+ assert !record.valid?
874
+ end
875
+
876
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
877
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
878
+ assert record.valid?
879
+ end
880
+ end
881
+
882
+ class MachineWithEventAttributesOnValidationTest < BaseTestCase
883
+ def setup
884
+ @model = new_model
885
+ @machine = StateMachine::Machine.new(@model)
886
+ @machine.event :ignite do
887
+ transition :parked => :idling
888
+ end
889
+
890
+ @record = @model.new
891
+ @record.state = 'parked'
892
+ @record.state_event = 'ignite'
893
+ end
894
+
895
+ def test_should_fail_if_event_is_invalid
896
+ @record.state_event = 'invalid'
897
+ assert !@record.valid?
898
+ assert_equal ['State event is invalid'], @record.errors.full_messages
899
+ end
900
+
901
+ def test_should_fail_if_event_has_no_transition
902
+ @record.state = 'idling'
903
+ assert !@record.valid?
904
+ assert_equal ['State event cannot transition when idling'], @record.errors.full_messages
905
+ end
906
+
907
+ def test_should_be_successful_if_event_has_transition
908
+ assert @record.valid?
909
+ end
910
+
911
+ def test_should_run_before_callbacks
912
+ ran_callback = false
913
+ @machine.before_transition { ran_callback = true }
914
+
915
+ @record.valid?
916
+ assert ran_callback
917
+ end
918
+
919
+ def test_should_run_around_callbacks_before_yield
920
+ ran_callback = false
921
+ @machine.around_transition {|block| ran_callback = true; block.call }
922
+
923
+ @record.valid?
924
+ assert ran_callback
925
+ end
926
+
927
+ def test_should_persist_new_state
928
+ @record.valid?
929
+ assert_equal 'idling', @record.state
930
+ end
931
+
932
+ def test_should_not_run_after_callbacks
933
+ ran_callback = false
934
+ @machine.after_transition { ran_callback = true }
935
+
936
+ @record.valid?
937
+ assert !ran_callback
938
+ end
939
+
940
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
941
+ @model.class_eval do
942
+ attr_accessor :seatbelt
943
+ validates_presence_of :seatbelt
944
+ end
945
+
946
+ ran_callback = false
947
+ @machine.after_transition { ran_callback = true }
948
+
949
+ @record.valid?
950
+ assert !ran_callback
951
+ end
952
+
953
+ def test_should_not_run_around_callbacks_after_yield
954
+ ran_callback = false
955
+ @machine.around_transition {|block| block.call; ran_callback = true }
956
+
957
+ @record.valid?
958
+ assert !ran_callback
959
+ end
960
+
961
+ def test_should_not_run_around_callbacks_after_yield_with_failures_disabled_if_validation_fails
962
+ @model.class_eval do
963
+ attr_accessor :seatbelt
964
+ validates_presence_of :seatbelt
965
+ end
966
+
967
+ ran_callback = false
968
+ @machine.around_transition {|block| block.call; ran_callback = true }
969
+
970
+ @record.valid?
971
+ assert !ran_callback
972
+ end
973
+
974
+ def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_validation_fails
975
+ @model.class_eval do
976
+ attr_accessor :seatbelt
977
+ validates_presence_of :seatbelt
978
+ end
979
+
980
+ ran_callback = false
981
+ @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
982
+
983
+ @record.valid?
984
+ assert ran_callback
985
+ end
986
+
987
+ def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
988
+ @model.class_eval do
989
+ attr_accessor :seatbelt
990
+ validates_presence_of :seatbelt
991
+ end
992
+
993
+ ran_callback = false
994
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
995
+
996
+ @record.valid?
997
+ assert ran_callback
998
+ end
999
+ end
1000
+
1001
+ class MachineWithEventAttributesOnSaveTest < BaseTestCase
1002
+ def setup
1003
+ @model = new_model
1004
+ @machine = StateMachine::Machine.new(@model)
1005
+ @machine.event :ignite do
1006
+ transition :parked => :idling
1007
+ end
1008
+
1009
+ @record = @model.new
1010
+ @record.state = 'parked'
1011
+ @record.state_event = 'ignite'
1012
+ end
1013
+
1014
+ def test_should_fail_if_event_is_invalid
1015
+ @record.state_event = 'invalid'
1016
+ assert_equal false, @record.save
1017
+ end
1018
+
1019
+ def test_should_fail_if_event_has_no_transition
1020
+ @record.state = 'idling'
1021
+ assert_equal false, @record.save
1022
+ end
1023
+
1024
+ def test_should_be_successful_if_event_has_transition
1025
+ assert_equal true, @record.save
1026
+ end
1027
+
1028
+ def test_should_run_before_callbacks
1029
+ ran_callback = false
1030
+ @machine.before_transition { ran_callback = true }
1031
+
1032
+ @record.save
1033
+ assert ran_callback
1034
+ end
1035
+
1036
+ def test_should_run_before_callbacks_once
1037
+ before_count = 0
1038
+ @machine.before_transition { before_count += 1 }
1039
+
1040
+ @record.save
1041
+ assert_equal 1, before_count
1042
+ end
1043
+
1044
+ def test_should_run_around_callbacks_before_yield
1045
+ ran_callback = false
1046
+ @machine.around_transition {|block| ran_callback = true; block.call }
1047
+
1048
+ @record.save
1049
+ assert ran_callback
1050
+ end
1051
+
1052
+ def test_should_run_around_callbacks_before_yield_once
1053
+ around_before_count = 0
1054
+ @machine.around_transition {|block| around_before_count += 1; block.call }
1055
+
1056
+ @record.save
1057
+ assert_equal 1, around_before_count
1058
+ end
1059
+
1060
+ def test_should_persist_new_state
1061
+ @record.save
1062
+ assert_equal 'idling', @record.state
1063
+ end
1064
+
1065
+ def test_should_run_after_callbacks
1066
+ ran_callback = false
1067
+ @machine.after_transition { ran_callback = true }
1068
+
1069
+ @record.save
1070
+ assert ran_callback
1071
+ end
1072
+
1073
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
1074
+ @model.class_eval do
1075
+ validates_inclusion_of :state, :within => %w(first_gear)
1076
+ end
1077
+
1078
+ ran_callback = false
1079
+ @machine.after_transition { ran_callback = true }
1080
+
1081
+ begin; @record.save; rescue; end
1082
+ assert !ran_callback
1083
+ end
1084
+
1085
+ def test_should_run_after_callbacks_with_failures_enabled_if_fails
1086
+ @model.class_eval do
1087
+ validates_inclusion_of :state, :within => %w(first_gear)
1088
+ end
1089
+
1090
+ ran_callback = false
1091
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
1092
+
1093
+ begin; @record.save; rescue; end
1094
+ assert ran_callback
1095
+ end
1096
+
1097
+ def test_should_not_run_around_callbacks_with_failures_disabled_if_fails
1098
+ @model.class_eval do
1099
+ validates_inclusion_of :state, :within => %w(first_gear)
1100
+ end
1101
+
1102
+ ran_callback = false
1103
+ @machine.around_transition {|block| block.call; ran_callback = true }
1104
+
1105
+ begin; @record.save; rescue; end
1106
+ assert !ran_callback
1107
+ end
1108
+
1109
+ def test_should_run_around_callbacks_after_yield
1110
+ ran_callback = false
1111
+ @machine.around_transition {|block| block.call; ran_callback = true }
1112
+
1113
+ @record.save
1114
+ assert ran_callback
1115
+ end
1116
+
1117
+ def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_fails
1118
+ @model.class_eval do
1119
+ validates_inclusion_of :state, :within => %w(first_gear)
1120
+ end
1121
+
1122
+ ran_callback = false
1123
+ @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
1124
+
1125
+ begin; @record.save; rescue; end
1126
+ assert ran_callback
1127
+ end
1128
+ end
1129
+
1130
+ class MachineWithEventAttributesOnSaveBangTest < BaseTestCase
1131
+ def setup
1132
+ @model = new_model
1133
+ @machine = StateMachine::Machine.new(@model)
1134
+ @machine.event :ignite do
1135
+ transition :parked => :idling
1136
+ end
1137
+
1138
+ @record = @model.new
1139
+ @record.state = 'parked'
1140
+ @record.state_event = 'ignite'
1141
+ end
1142
+
1143
+ def test_should_fail_if_event_is_invalid
1144
+ @record.state_event = 'invalid'
1145
+ assert_raise(MongoMapper::DocumentNotValid) { @record.save! }
1146
+ end
1147
+
1148
+ def test_should_fail_if_event_has_no_transition
1149
+ @record.state = 'idling'
1150
+ assert_raise(MongoMapper::DocumentNotValid) { @record.save! }
1151
+ end
1152
+
1153
+ def test_should_be_successful_if_event_has_transition
1154
+ assert_equal true, @record.save!
1155
+ end
1156
+
1157
+ def test_should_run_before_callbacks
1158
+ ran_callback = false
1159
+ @machine.before_transition { ran_callback = true }
1160
+
1161
+ @record.save!
1162
+ assert ran_callback
1163
+ end
1164
+
1165
+ def test_should_run_before_callbacks_once
1166
+ before_count = 0
1167
+ @machine.before_transition { before_count += 1 }
1168
+
1169
+ @record.save!
1170
+ assert_equal 1, before_count
1171
+ end
1172
+
1173
+ def test_should_run_around_callbacks_before_yield
1174
+ ran_callback = false
1175
+ @machine.around_transition {|block| ran_callback = true; block.call }
1176
+
1177
+ @record.save!
1178
+ assert ran_callback
1179
+ end
1180
+
1181
+ def test_should_run_around_callbacks_before_yield_once
1182
+ around_before_count = 0
1183
+ @machine.around_transition {|block| around_before_count += 1; block.call }
1184
+
1185
+ @record.save!
1186
+ assert_equal 1, around_before_count
1187
+ end
1188
+
1189
+ def test_should_persist_new_state
1190
+ @record.save!
1191
+ assert_equal 'idling', @record.state
1192
+ end
1193
+
1194
+ def test_should_persist_new_state
1195
+ @record.save!
1196
+ assert_equal 'idling', @record.state
1197
+ end
1198
+
1199
+ def test_should_run_after_callbacks
1200
+ ran_callback = false
1201
+ @machine.after_transition { ran_callback = true }
1202
+
1203
+ @record.save!
1204
+ assert ran_callback
1205
+ end
1206
+
1207
+ def test_should_run_around_callbacks_after_yield
1208
+ ran_callback = false
1209
+ @machine.around_transition {|block| block.call; ran_callback = true }
1210
+
1211
+ @record.save!
1212
+ assert ran_callback
1213
+ end
1214
+ end
1215
+
1216
+ class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
1217
+ def setup
1218
+ @superclass = new_model do
1219
+ def persist
1220
+ create_or_update
1221
+ end
1222
+ end
1223
+ @model = Class.new(@superclass)
1224
+ @machine = StateMachine::Machine.new(@model, :action => :persist)
1225
+ @machine.event :ignite do
1226
+ transition :parked => :idling
1227
+ end
1228
+
1229
+ @record = @model.new
1230
+ @record.state = 'parked'
1231
+ @record.state_event = 'ignite'
1232
+ end
1233
+
1234
+ def test_should_not_transition_on_valid?
1235
+ @record.valid?
1236
+ assert_equal 'parked', @record.state
1237
+ end
1238
+
1239
+ def test_should_not_transition_on_save
1240
+ @record.save
1241
+ assert_equal 'parked', @record.state
1242
+ end
1243
+
1244
+ def test_should_not_transition_on_save!
1245
+ @record.save!
1246
+ assert_equal 'parked', @record.state
1247
+ end
1248
+
1249
+ def test_should_transition_on_custom_action
1250
+ @record.persist
1251
+ assert_equal 'idling', @record.state
1252
+ end
1253
+ end
1254
+
1255
+ class MachineWithScopesTest < BaseTestCase
1256
+ def setup
1257
+ @model = new_model
1258
+ @machine = StateMachine::Machine.new(@model)
1259
+ @machine.state :parked, :first_gear
1260
+ @machine.state :idling, :value => lambda {'idling'}
1261
+ end
1262
+
1263
+ def test_should_create_singular_with_scope
1264
+ assert @model.respond_to?(:with_state)
1265
+ end
1266
+
1267
+ def test_should_only_include_records_with_state_in_singular_with_scope
1268
+ parked = @model.create :state => 'parked'
1269
+ idling = @model.create :state => 'idling'
1270
+
1271
+ assert_equal [parked], @model.with_state(:parked)
1272
+ end
1273
+
1274
+ def test_should_create_plural_with_scope
1275
+ assert @model.respond_to?(:with_states)
1276
+ end
1277
+
1278
+ def test_should_only_include_records_with_states_in_plural_with_scope
1279
+ parked = @model.create :state => 'parked'
1280
+ idling = @model.create :state => 'idling'
1281
+
1282
+ assert_equal [parked, idling], @model.with_states(:parked, :idling)
1283
+ end
1284
+
1285
+ def test_should_create_singular_without_scope
1286
+ assert @model.respond_to?(:without_state)
1287
+ end
1288
+
1289
+ def test_should_only_include_records_without_state_in_singular_without_scope
1290
+ parked = @model.create :state => 'parked'
1291
+ idling = @model.create :state => 'idling'
1292
+
1293
+ assert_equal [parked], @model.without_state(:idling)
1294
+ end
1295
+
1296
+ def test_should_create_plural_without_scope
1297
+ assert @model.respond_to?(:without_states)
1298
+ end
1299
+
1300
+ def test_should_only_include_records_without_states_in_plural_without_scope
1301
+ parked = @model.create :state => 'parked'
1302
+ idling = @model.create :state => 'idling'
1303
+ first_gear = @model.create :state => 'first_gear'
1304
+
1305
+ assert_equal [parked, idling], @model.without_states(:first_gear)
1306
+ end
1307
+ end
1308
+
1309
+ class MachineWithScopesAndOwnerSubclassTest < BaseTestCase
1310
+ def setup
1311
+ @model = new_model
1312
+ @machine = StateMachine::Machine.new(@model, :state)
1313
+
1314
+ @subclass = Class.new(@model)
1315
+ @subclass_machine = @subclass.state_machine(:state) {}
1316
+ @subclass_machine.state :parked, :idling, :first_gear
1317
+ end
1318
+
1319
+ def test_should_only_include_records_with_subclass_states_in_with_scope
1320
+ parked = @subclass.create :state => 'parked'
1321
+ idling = @subclass.create :state => 'idling'
1322
+
1323
+ assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
1324
+ end
1325
+
1326
+ def test_should_only_include_records_without_subclass_states_in_without_scope
1327
+ parked = @subclass.create :state => 'parked'
1328
+ idling = @subclass.create :state => 'idling'
1329
+ first_gear = @subclass.create :state => 'first_gear'
1330
+
1331
+ assert_equal [parked, idling], @subclass.without_states(:first_gear)
1332
+ end
1333
+ end
1334
+
1335
+ class MachineWithComplexPluralizationScopesTest < BaseTestCase
1336
+ def setup
1337
+ @model = new_model
1338
+ @machine = StateMachine::Machine.new(@model, :status)
1339
+ end
1340
+
1341
+ def test_should_create_singular_with_scope
1342
+ assert @model.respond_to?(:with_status)
1343
+ end
1344
+
1345
+ def test_should_create_plural_with_scope
1346
+ assert @model.respond_to?(:with_statuses)
1347
+ end
1348
+ end
1349
+ end