state_machine 0.7.6 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +17 -0
- data/Rakefile +1 -1
- data/lib/state_machine.rb +11 -8
- data/lib/state_machine/callback.rb +1 -1
- data/lib/state_machine/event.rb +9 -8
- data/lib/state_machine/event_collection.rb +21 -10
- data/lib/state_machine/extensions.rb +2 -11
- data/lib/state_machine/guard.rb +12 -1
- data/lib/state_machine/integrations/active_record.rb +37 -1
- data/lib/state_machine/integrations/data_mapper.rb +17 -2
- data/lib/state_machine/integrations/sequel.rb +32 -1
- data/lib/state_machine/machine.rb +56 -16
- data/lib/state_machine/machine_collection.rb +7 -5
- data/lib/state_machine/state.rb +1 -1
- data/lib/state_machine/transition.rb +41 -14
- data/test/unit/assertions_test.rb +3 -3
- data/test/unit/eval_helpers_test.rb +13 -22
- data/test/unit/event_collection_test.rb +24 -0
- data/test/unit/event_test.rb +94 -0
- data/test/unit/guard_test.rb +46 -0
- data/test/unit/integrations/active_record_test.rb +247 -18
- data/test/unit/integrations/data_mapper_test.rb +143 -1
- data/test/unit/integrations/sequel_test.rb +279 -10
- data/test/unit/machine_collection_test.rb +42 -19
- data/test/unit/machine_test.rb +55 -0
- data/test/unit/state_test.rb +1 -1
- data/test/unit/transition_test.rb +106 -7
- metadata +2 -2
@@ -28,6 +28,7 @@ begin
|
|
28
28
|
def self.name; 'SequelTest::Foo'; end
|
29
29
|
end
|
30
30
|
model.plugin(:validation_class_methods) if model.respond_to?(:plugin)
|
31
|
+
model.plugin(:hook_class_methods) if model.respond_to?(:plugin)
|
31
32
|
model.class_eval(&block) if block_given?
|
32
33
|
model
|
33
34
|
end
|
@@ -184,15 +185,136 @@ begin
|
|
184
185
|
end
|
185
186
|
end
|
186
187
|
|
187
|
-
class
|
188
|
+
class MachineWithStaticInitialStateTest < BaseTestCase
|
188
189
|
def setup
|
189
|
-
@model = new_model
|
190
|
-
|
191
|
-
|
190
|
+
@model = new_model do
|
191
|
+
attr_accessor :value
|
192
|
+
end
|
193
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
192
194
|
end
|
193
195
|
|
194
196
|
def test_should_set_initial_state_on_created_object
|
195
|
-
|
197
|
+
record = @model.new
|
198
|
+
assert_equal 'parked', record.state
|
199
|
+
end
|
200
|
+
|
201
|
+
def test_should_still_set_attributes
|
202
|
+
record = @model.new(:value => 1)
|
203
|
+
assert_equal 1, record.value
|
204
|
+
end
|
205
|
+
|
206
|
+
def test_should_still_allow_initialize_blocks
|
207
|
+
block_args = nil
|
208
|
+
record = @model.new do |*args|
|
209
|
+
block_args = args
|
210
|
+
end
|
211
|
+
|
212
|
+
assert_equal [record], block_args
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_should_not_have_any_changed_columns
|
216
|
+
record = @model.new
|
217
|
+
assert record.changed_columns.empty?
|
218
|
+
end
|
219
|
+
|
220
|
+
def test_should_set_attributes_prior_to_after_initialize_hook
|
221
|
+
state = nil
|
222
|
+
@model.class_eval do
|
223
|
+
define_method(:after_initialize) do
|
224
|
+
state = self.state
|
225
|
+
end
|
226
|
+
end
|
227
|
+
@model.new
|
228
|
+
assert_equal 'parked', state
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_should_set_initial_state_before_setting_attributes
|
232
|
+
@model.class_eval do
|
233
|
+
attr_accessor :state_during_setter
|
234
|
+
|
235
|
+
define_method(:value=) do |value|
|
236
|
+
self.state_during_setter = state
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
record = @model.new(:value => 1)
|
241
|
+
assert_equal 'parked', record.state_during_setter
|
242
|
+
end
|
243
|
+
|
244
|
+
def test_should_not_set_initial_state_after_already_initialized
|
245
|
+
record = @model.new(:value => 1)
|
246
|
+
assert_equal 'parked', record.state
|
247
|
+
|
248
|
+
record.state = 'idling'
|
249
|
+
record.set({})
|
250
|
+
assert_equal 'idling', record.state
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class MachineWithDynamicInitialStateTest < BaseTestCase
|
255
|
+
def setup
|
256
|
+
@model = new_model do
|
257
|
+
attr_accessor :value
|
258
|
+
end
|
259
|
+
@machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked})
|
260
|
+
@machine.state :parked
|
261
|
+
end
|
262
|
+
|
263
|
+
def test_should_set_initial_state_on_created_object
|
264
|
+
record = @model.new
|
265
|
+
assert_equal 'parked', record.state
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_should_still_set_attributes
|
269
|
+
record = @model.new(:value => 1)
|
270
|
+
assert_equal 1, record.value
|
271
|
+
end
|
272
|
+
|
273
|
+
def test_should_still_allow_initialize_blocks
|
274
|
+
block_args = nil
|
275
|
+
record = @model.new do |*args|
|
276
|
+
block_args = args
|
277
|
+
end
|
278
|
+
|
279
|
+
assert_equal [record], block_args
|
280
|
+
end
|
281
|
+
|
282
|
+
def test_should_not_have_any_changed_columns
|
283
|
+
record = @model.new
|
284
|
+
assert record.changed_columns.empty?
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_should_set_attributes_prior_to_after_initialize_hook
|
288
|
+
state = nil
|
289
|
+
@model.class_eval do
|
290
|
+
define_method(:after_initialize) do
|
291
|
+
state = self.state
|
292
|
+
end
|
293
|
+
end
|
294
|
+
@model.new
|
295
|
+
assert_equal 'parked', state
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_should_set_initial_state_after_setting_attributes
|
299
|
+
@model.class_eval do
|
300
|
+
attr_accessor :state_during_setter
|
301
|
+
|
302
|
+
define_method(:value=) do |value|
|
303
|
+
self.state_during_setter = state || 'nil'
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
record = @model.new(:value => 1)
|
308
|
+
assert_equal 'nil', record.state_during_setter
|
309
|
+
end
|
310
|
+
|
311
|
+
def test_should_not_set_initial_state_after_already_initialized
|
312
|
+
record = @model.new(:value => 1)
|
313
|
+
assert_equal 'parked', record.state
|
314
|
+
|
315
|
+
record.state = 'idling'
|
316
|
+
record.set({})
|
317
|
+
assert_equal 'idling', record.state
|
196
318
|
end
|
197
319
|
end
|
198
320
|
|
@@ -279,20 +401,24 @@ begin
|
|
279
401
|
|
280
402
|
class MachineWithCustomAttributeTest < BaseTestCase
|
281
403
|
def setup
|
282
|
-
@model = new_model
|
283
|
-
|
404
|
+
@model = new_model do
|
405
|
+
alias_method :vehicle_status, :state
|
406
|
+
alias_method :vehicle_status=, :state=
|
407
|
+
end
|
408
|
+
|
409
|
+
@machine = StateMachine::Machine.new(@model, :status, :attribute => :vehicle_status)
|
284
410
|
@machine.state :parked
|
285
411
|
|
286
412
|
@record = @model.new
|
287
413
|
end
|
288
414
|
|
289
415
|
def test_should_add_validation_errors_to_custom_attribute
|
290
|
-
@record.
|
416
|
+
@record.vehicle_status = 'invalid'
|
291
417
|
|
292
418
|
assert !@record.valid?
|
293
|
-
assert_equal ['is invalid'], @record.errors.on(:
|
419
|
+
assert_equal ['is invalid'], @record.errors.on(:vehicle_status)
|
294
420
|
|
295
|
-
@record.
|
421
|
+
@record.vehicle_status = 'parked'
|
296
422
|
assert @record.valid?
|
297
423
|
end
|
298
424
|
end
|
@@ -389,6 +515,46 @@ begin
|
|
389
515
|
end
|
390
516
|
end
|
391
517
|
|
518
|
+
class MachineWithLoopbackTest < BaseTestCase
|
519
|
+
def setup
|
520
|
+
changed_columns = nil
|
521
|
+
|
522
|
+
@model = new_model do
|
523
|
+
# Simulate timestamps plugin
|
524
|
+
define_method(:before_update) do
|
525
|
+
changed_columns = self.changed_columns.dup
|
526
|
+
|
527
|
+
super()
|
528
|
+
self.updated_at = Time.now if changed_columns.any?
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
DB.alter_table :foo do
|
533
|
+
add_column :updated_at, :datetime
|
534
|
+
end
|
535
|
+
@model.class_eval { get_db_schema(true) }
|
536
|
+
|
537
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
538
|
+
@machine.event :park
|
539
|
+
|
540
|
+
@record = @model.create(:updated_at => Time.now - 1)
|
541
|
+
@timestamp = @record.updated_at
|
542
|
+
|
543
|
+
@transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
|
544
|
+
@transition.perform
|
545
|
+
|
546
|
+
@changed_columns = changed_columns
|
547
|
+
end
|
548
|
+
|
549
|
+
def test_should_include_state_in_changed_columns
|
550
|
+
assert_equal [:state], @changed_columns
|
551
|
+
end
|
552
|
+
|
553
|
+
def test_should_update_record
|
554
|
+
assert_not_equal @timestamp, @record.updated_at
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
392
558
|
class MachineWithValidationsTest < BaseTestCase
|
393
559
|
def setup
|
394
560
|
@model = new_model
|
@@ -490,6 +656,109 @@ begin
|
|
490
656
|
@record.valid?
|
491
657
|
assert !ran_callback
|
492
658
|
end
|
659
|
+
|
660
|
+
def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
|
661
|
+
@model.class_eval do
|
662
|
+
attr_accessor :seatbelt
|
663
|
+
validates_presence_of :seatbelt
|
664
|
+
end
|
665
|
+
|
666
|
+
ran_callback = false
|
667
|
+
@machine.after_transition { ran_callback = true }
|
668
|
+
|
669
|
+
@record.valid?
|
670
|
+
assert !ran_callback
|
671
|
+
end
|
672
|
+
|
673
|
+
def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
|
674
|
+
@model.class_eval do
|
675
|
+
attr_accessor :seatbelt
|
676
|
+
validates_presence_of :seatbelt
|
677
|
+
end
|
678
|
+
|
679
|
+
ran_callback = false
|
680
|
+
@machine.after_transition(:include_failures => true) { ran_callback = true }
|
681
|
+
|
682
|
+
@record.valid?
|
683
|
+
assert ran_callback
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
class MachineWithEventAttributesOnSaveTest < BaseTestCase
|
688
|
+
def setup
|
689
|
+
@model = new_model
|
690
|
+
@machine = StateMachine::Machine.new(@model)
|
691
|
+
@machine.event :ignite do
|
692
|
+
transition :parked => :idling
|
693
|
+
end
|
694
|
+
|
695
|
+
@record = @model.new
|
696
|
+
@record.state = 'parked'
|
697
|
+
@record.state_event = 'ignite'
|
698
|
+
end
|
699
|
+
|
700
|
+
def test_should_fail_if_event_is_invalid
|
701
|
+
@record.state_event = 'invalid'
|
702
|
+
assert !@record.save
|
703
|
+
end
|
704
|
+
|
705
|
+
def test_should_fail_if_event_has_no_transition
|
706
|
+
@record.state = 'idling'
|
707
|
+
assert !@record.save
|
708
|
+
end
|
709
|
+
|
710
|
+
def test_should_be_successful_if_event_has_transition
|
711
|
+
assert @record.save
|
712
|
+
end
|
713
|
+
|
714
|
+
def test_should_run_before_callbacks
|
715
|
+
ran_callback = false
|
716
|
+
@machine.before_transition { ran_callback = true }
|
717
|
+
|
718
|
+
@record.save
|
719
|
+
assert ran_callback
|
720
|
+
end
|
721
|
+
|
722
|
+
def test_should_run_before_callbacks_once
|
723
|
+
before_count = 0
|
724
|
+
@machine.before_transition { before_count += 1 }
|
725
|
+
|
726
|
+
@record.save
|
727
|
+
assert_equal 1, before_count
|
728
|
+
end
|
729
|
+
|
730
|
+
def test_should_persist_new_state
|
731
|
+
@record.save
|
732
|
+
assert_equal 'idling', @record.state
|
733
|
+
end
|
734
|
+
|
735
|
+
def test_should_run_after_callbacks
|
736
|
+
ran_callback = false
|
737
|
+
@machine.after_transition { ran_callback = true }
|
738
|
+
|
739
|
+
@record.save
|
740
|
+
assert ran_callback
|
741
|
+
end
|
742
|
+
|
743
|
+
def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
|
744
|
+
@model.before_create {|record| false}
|
745
|
+
|
746
|
+
ran_callback = false
|
747
|
+
@machine.after_transition { ran_callback = true }
|
748
|
+
|
749
|
+
@record.save
|
750
|
+
assert !ran_callback
|
751
|
+
end
|
752
|
+
|
753
|
+
def test_should_run_after_callbacks_with_failures_enabled_if_fails
|
754
|
+
@model.before_create {|record| false}
|
755
|
+
|
756
|
+
ran_callback = false
|
757
|
+
@machine.after_transition(:include_failures => true) { ran_callback = true }
|
758
|
+
|
759
|
+
@record.save
|
760
|
+
assert ran_callback
|
761
|
+
end
|
493
762
|
end
|
494
763
|
|
495
764
|
class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
|
@@ -14,37 +14,60 @@ class MachineCollectionStateInitializationTest < Test::Unit::TestCase
|
|
14
14
|
def setup
|
15
15
|
@machines = StateMachine::MachineCollection.new
|
16
16
|
|
17
|
-
@klass = Class.new
|
18
|
-
def initialize(attributes = {})
|
19
|
-
attributes.each do |attribute, value|
|
20
|
-
self.send("#{attribute}=", value)
|
21
|
-
end
|
22
|
-
|
23
|
-
super()
|
24
|
-
end
|
25
|
-
end
|
17
|
+
@klass = Class.new
|
26
18
|
|
27
19
|
@machines[:state] = StateMachine::Machine.new(@klass, :state, :initial => :parked)
|
28
|
-
@machines[:alarm_state] = StateMachine::Machine.new(@klass, :alarm_state, :initial => :active)
|
20
|
+
@machines[:alarm_state] = StateMachine::Machine.new(@klass, :alarm_state, :initial => lambda {|object| :active})
|
29
21
|
@machines[:alarm_state].state :active, :value => lambda {'active'}
|
22
|
+
|
23
|
+
# Prevent the auto-initialization hook from firing
|
24
|
+
@klass.class_eval do
|
25
|
+
def initialize
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@object = @klass.new
|
30
|
+
@object.state = nil
|
31
|
+
@object.alarm_state = nil
|
30
32
|
end
|
31
33
|
|
32
34
|
def test_should_set_states_if_nil
|
33
|
-
|
34
|
-
|
35
|
-
assert_equal '
|
35
|
+
@machines.initialize_states(@object)
|
36
|
+
|
37
|
+
assert_equal 'parked', @object.state
|
38
|
+
assert_equal 'active', @object.alarm_state
|
36
39
|
end
|
37
40
|
|
38
41
|
def test_should_set_states_if_empty
|
39
|
-
object
|
40
|
-
|
41
|
-
|
42
|
+
@object.state = ''
|
43
|
+
@object.alarm_state = ''
|
44
|
+
@machines.initialize_states(@object)
|
45
|
+
|
46
|
+
assert_equal 'parked', @object.state
|
47
|
+
assert_equal 'active', @object.alarm_state
|
42
48
|
end
|
43
49
|
|
44
50
|
def test_should_not_set_states_if_not_empty
|
45
|
-
object
|
46
|
-
|
47
|
-
|
51
|
+
@object.state = 'idling'
|
52
|
+
@object.alarm_state = 'off'
|
53
|
+
@machines.initialize_states(@object)
|
54
|
+
|
55
|
+
assert_equal 'idling', @object.state
|
56
|
+
assert_equal 'off', @object.alarm_state
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_should_only_initialize_static_states_if_dynamic_disabled
|
60
|
+
@machines.initialize_states(@object, :dynamic => false)
|
61
|
+
|
62
|
+
assert_equal 'parked', @object.state
|
63
|
+
assert_nil @object.alarm_state
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_should_only_initialize_dynamic_states_if_dynamic_enabled
|
67
|
+
@machines.initialize_states(@object, :dynamic => true)
|
68
|
+
|
69
|
+
assert_nil @object.state
|
70
|
+
assert_equal 'active', @object.alarm_state
|
48
71
|
end
|
49
72
|
end
|
50
73
|
|
data/test/unit/machine_test.rb
CHANGED
@@ -207,6 +207,10 @@ class MachineWithStaticInitialStateTest < Test::Unit::TestCase
|
|
207
207
|
@machine = StateMachine::Machine.new(@klass, :initial => :parked)
|
208
208
|
end
|
209
209
|
|
210
|
+
def test_should_not_have_dynamic_initial_state
|
211
|
+
assert !@machine.dynamic_initial_state?
|
212
|
+
end
|
213
|
+
|
210
214
|
def test_should_have_an_initial_state
|
211
215
|
object = @klass.new
|
212
216
|
assert_equal 'parked', @machine.initial_state(object).value
|
@@ -231,6 +235,20 @@ class MachineWithStaticInitialStateTest < Test::Unit::TestCase
|
|
231
235
|
assert_equal 'idling', object.state
|
232
236
|
end
|
233
237
|
|
238
|
+
def test_should_set_initial_state_prior_to_initialization
|
239
|
+
base = Class.new do
|
240
|
+
attr_accessor :state_on_init
|
241
|
+
|
242
|
+
def initialize
|
243
|
+
self.state_on_init = state
|
244
|
+
end
|
245
|
+
end
|
246
|
+
klass = Class.new(base)
|
247
|
+
machine = StateMachine::Machine.new(klass, :initial => :parked)
|
248
|
+
|
249
|
+
assert_equal 'parked', klass.new.state_on_init
|
250
|
+
end
|
251
|
+
|
234
252
|
def test_should_be_included_in_known_states
|
235
253
|
assert_equal [:parked], @machine.states.keys
|
236
254
|
end
|
@@ -246,6 +264,10 @@ class MachineWithDynamicInitialStateTest < Test::Unit::TestCase
|
|
246
264
|
@object = @klass.new
|
247
265
|
end
|
248
266
|
|
267
|
+
def test_should_have_dynamic_initial_state
|
268
|
+
assert @machine.dynamic_initial_state?
|
269
|
+
end
|
270
|
+
|
249
271
|
def test_should_use_the_record_for_determining_the_initial_state
|
250
272
|
@object.initial_state = :parked
|
251
273
|
assert_equal :parked, @machine.initial_state(@object).name
|
@@ -258,6 +280,21 @@ class MachineWithDynamicInitialStateTest < Test::Unit::TestCase
|
|
258
280
|
assert_equal 'default', @object.state
|
259
281
|
end
|
260
282
|
|
283
|
+
def test_should_set_initial_state_after_initialization
|
284
|
+
base = Class.new do
|
285
|
+
attr_accessor :state_on_init
|
286
|
+
|
287
|
+
def initialize
|
288
|
+
self.state_on_init = state
|
289
|
+
end
|
290
|
+
end
|
291
|
+
klass = Class.new(base)
|
292
|
+
machine = StateMachine::Machine.new(klass, :initial => lambda {|object| :parked})
|
293
|
+
machine.state :parked
|
294
|
+
|
295
|
+
assert_nil klass.new.state_on_init
|
296
|
+
end
|
297
|
+
|
261
298
|
def test_should_not_be_included_in_known_states
|
262
299
|
assert_equal [:parked, :idling, :default], @machine.states.map {|state| state.name}
|
263
300
|
end
|
@@ -1375,6 +1412,24 @@ class MachineWithTransitionCallbacksTest < Test::Unit::TestCase
|
|
1375
1412
|
assert_equal %w(before after), @object.callbacks
|
1376
1413
|
end
|
1377
1414
|
|
1415
|
+
def test_should_allow_multiple_callbacks
|
1416
|
+
@machine.before_transition lambda {|object| object.callbacks << 'before1'}, lambda {|object| object.callbacks << 'before2'}
|
1417
|
+
@machine.after_transition lambda {|object| object.callbacks << 'after1'}, lambda {|object| object.callbacks << 'after2'}
|
1418
|
+
|
1419
|
+
@event.fire(@object)
|
1420
|
+
assert_equal %w(before1 before2 after1 after2), @object.callbacks
|
1421
|
+
end
|
1422
|
+
|
1423
|
+
def test_should_allow_multiple_callbacks_with_requirements
|
1424
|
+
@machine.before_transition lambda {|object| object.callbacks << 'before_parked1'}, lambda {|object| object.callbacks << 'before_parked2'}, :from => :parked
|
1425
|
+
@machine.before_transition lambda {|object| object.callbacks << 'before_idling1'}, lambda {|object| object.callbacks << 'before_idling2'}, :from => :idling
|
1426
|
+
@machine.after_transition lambda {|object| object.callbacks << 'after_parked1'}, lambda {|object| object.callbacks << 'after_parked2'}, :from => :parked
|
1427
|
+
@machine.after_transition lambda {|object| object.callbacks << 'after_idling1'}, lambda {|object| object.callbacks << 'after_idling2'}, :from => :idling
|
1428
|
+
|
1429
|
+
@event.fire(@object)
|
1430
|
+
assert_equal %w(before_parked1 before_parked2 after_parked1 after_parked2), @object.callbacks
|
1431
|
+
end
|
1432
|
+
|
1378
1433
|
def test_should_support_from_requirement
|
1379
1434
|
@machine.before_transition :from => :parked, :do => lambda {|object| object.callbacks << :parked}
|
1380
1435
|
@machine.before_transition :from => :idling, :do => lambda {|object| object.callbacks << :idling}
|