state_machine 0.7.6 → 0.8.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.
- 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}
|