state_machine 0.8.1 → 0.9.0

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