state_machine 0.9.4 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/CHANGELOG.rdoc +20 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +74 -4
  4. data/Rakefile +3 -3
  5. data/lib/state_machine.rb +51 -24
  6. data/lib/state_machine/{guard.rb → branch.rb} +34 -40
  7. data/lib/state_machine/callback.rb +13 -18
  8. data/lib/state_machine/error.rb +13 -0
  9. data/lib/state_machine/eval_helpers.rb +3 -0
  10. data/lib/state_machine/event.rb +67 -30
  11. data/lib/state_machine/event_collection.rb +20 -3
  12. data/lib/state_machine/extensions.rb +3 -3
  13. data/lib/state_machine/integrations.rb +7 -0
  14. data/lib/state_machine/integrations/active_model.rb +149 -59
  15. data/lib/state_machine/integrations/active_model/versions.rb +30 -0
  16. data/lib/state_machine/integrations/active_record.rb +74 -148
  17. data/lib/state_machine/integrations/active_record/locale.rb +0 -7
  18. data/lib/state_machine/integrations/active_record/versions.rb +149 -0
  19. data/lib/state_machine/integrations/base.rb +64 -0
  20. data/lib/state_machine/integrations/data_mapper.rb +50 -39
  21. data/lib/state_machine/integrations/data_mapper/observer.rb +47 -12
  22. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  23. data/lib/state_machine/integrations/mongo_mapper.rb +37 -64
  24. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  25. data/lib/state_machine/integrations/mongo_mapper/versions.rb +102 -0
  26. data/lib/state_machine/integrations/mongoid.rb +297 -0
  27. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  28. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  29. data/lib/state_machine/integrations/sequel.rb +99 -55
  30. data/lib/state_machine/integrations/sequel/versions.rb +40 -0
  31. data/lib/state_machine/machine.rb +273 -136
  32. data/lib/state_machine/machine_collection.rb +21 -13
  33. data/lib/state_machine/node_collection.rb +6 -1
  34. data/lib/state_machine/path.rb +120 -0
  35. data/lib/state_machine/path_collection.rb +90 -0
  36. data/lib/state_machine/state.rb +28 -9
  37. data/lib/state_machine/state_collection.rb +1 -1
  38. data/lib/state_machine/transition.rb +65 -6
  39. data/lib/state_machine/transition_collection.rb +1 -1
  40. data/test/files/en.yml +8 -0
  41. data/test/functional/state_machine_test.rb +15 -2
  42. data/test/unit/branch_test.rb +890 -0
  43. data/test/unit/callback_test.rb +9 -36
  44. data/test/unit/error_test.rb +43 -0
  45. data/test/unit/event_collection_test.rb +67 -33
  46. data/test/unit/event_test.rb +165 -38
  47. data/test/unit/integrations/active_model_test.rb +103 -3
  48. data/test/unit/integrations/active_record_test.rb +90 -43
  49. data/test/unit/integrations/base_test.rb +87 -0
  50. data/test/unit/integrations/data_mapper_test.rb +105 -44
  51. data/test/unit/integrations/mongo_mapper_test.rb +261 -64
  52. data/test/unit/integrations/mongoid_test.rb +1529 -0
  53. data/test/unit/integrations/sequel_test.rb +33 -49
  54. data/test/unit/integrations_test.rb +4 -0
  55. data/test/unit/invalid_event_test.rb +15 -2
  56. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  57. data/test/unit/invalid_transition_test.rb +72 -2
  58. data/test/unit/machine_collection_test.rb +55 -61
  59. data/test/unit/machine_test.rb +388 -26
  60. data/test/unit/node_collection_test.rb +14 -4
  61. data/test/unit/path_collection_test.rb +266 -0
  62. data/test/unit/path_test.rb +485 -0
  63. data/test/unit/state_collection_test.rb +30 -0
  64. data/test/unit/state_test.rb +82 -35
  65. data/test/unit/transition_collection_test.rb +48 -44
  66. data/test/unit/transition_test.rb +198 -41
  67. metadata +111 -74
  68. data/test/unit/guard_test.rb +0 -909
@@ -14,7 +14,7 @@ module ActiveModelTest
14
14
  end
15
15
 
16
16
  protected
17
- # Creates a new ActiveRecord model (and the associated table)
17
+ # Creates a new ActiveModel model (and the associated table)
18
18
  def new_model(&block)
19
19
  # Simple ActiveModel superclass
20
20
  parent = Class.new do
@@ -58,7 +58,7 @@ module ActiveModelTest
58
58
  model
59
59
  end
60
60
 
61
- # Creates a new ActiveRecord observer
61
+ # Creates a new ActiveModel observer
62
62
  def new_observer(model, &block)
63
63
  observer = Class.new(ActiveModel::Observer) do
64
64
  attr_accessor :notifications
@@ -79,6 +79,10 @@ module ActiveModelTest
79
79
  assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Dirty })
80
80
  end
81
81
 
82
+ def test_should_match_if_class_includes_mass_assignment_security_feature
83
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::MassAssignmentSecurity })
84
+ end
85
+
82
86
  def test_should_match_if_class_includes_observing_feature
83
87
  assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Observing })
84
88
  end
@@ -240,6 +244,26 @@ module ActiveModelTest
240
244
  record = @model.new(:state => nil)
241
245
  assert_equal 'parked', record.state
242
246
  end
247
+
248
+ def test_should_use_default_state_if_protected
249
+ @model.class_eval do
250
+ include ActiveModel::MassAssignmentSecurity
251
+ attr_protected :state
252
+
253
+ def initialize(attrs = {})
254
+ initialize_state_machines(:attributes => attrs) do
255
+ sanitize_for_mass_assignment(attrs).each {|attr, value| send("#{attr}=", value)} if attrs
256
+ @changed_attributes = {}
257
+ end
258
+ end
259
+ end
260
+
261
+ record = @model.new(:state => 'idling')
262
+ assert_equal 'parked', record.state
263
+
264
+ record = @model.new(nil)
265
+ assert_equal 'parked', record.state
266
+ end
243
267
  end
244
268
 
245
269
  class MachineWithDirtyAttributesTest < BaseTestCase
@@ -356,6 +380,40 @@ module ActiveModelTest
356
380
  end
357
381
  end
358
382
 
383
+ class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
384
+ def setup
385
+ @model = new_model do
386
+ include ActiveModel::Dirty
387
+ define_attribute_methods [:state]
388
+ end
389
+ @machine = StateMachine::Machine.new(@model, :action => :save, :initial => :parked)
390
+ @machine.event :ignite
391
+
392
+ @record = @model.create
393
+ @record.state_event = 'ignite'
394
+ end
395
+
396
+ def test_should_include_state_in_changed_attributes
397
+ assert_equal %w(state), @record.changed
398
+ end
399
+
400
+ def test_should_track_attribute_change
401
+ assert_equal %w(parked parked), @record.changes['state']
402
+ end
403
+
404
+ def test_should_not_reset_changes_on_multiple_changes
405
+ @record.state_event = 'ignite'
406
+ assert_equal %w(parked parked), @record.changes['state']
407
+ end
408
+
409
+ def test_should_not_include_state_in_changed_attributes_if_nil
410
+ @record = @model.create
411
+ @record.state_event = nil
412
+
413
+ assert_equal [], @record.changed
414
+ end
415
+ end
416
+
359
417
  class MachineWithCallbacksTest < BaseTestCase
360
418
  def setup
361
419
  @model = new_model
@@ -740,6 +798,48 @@ module ActiveModelTest
740
798
  end
741
799
  end
742
800
 
801
+ class MachineWithFailureCallbacksTest < BaseTestCase
802
+ def setup
803
+ @model = new_model { include ActiveModel::Observing }
804
+ @machine = StateMachine::Machine.new(@model)
805
+ @machine.state :parked, :idling
806
+ @machine.event :ignite
807
+ @record = @model.new(:state => 'parked')
808
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
809
+
810
+ @notifications = []
811
+
812
+ # Create callbacks
813
+ @machine.before_transition {false}
814
+ @machine.after_failure {@notifications << :callback_after_failure}
815
+
816
+ # Create observer callbacks
817
+ observer = new_observer(@model) do
818
+ def after_failure_to_ignite(*args)
819
+ notifications << :observer_after_failure_ignite
820
+ end
821
+
822
+ def after_failure_to_transition(*args)
823
+ notifications << :observer_after_failure_transition
824
+ end
825
+ end
826
+ instance = observer.instance
827
+ instance.notifications = @notifications
828
+
829
+ @transition.perform
830
+ end
831
+
832
+ def test_should_invoke_callbacks_in_specific_order
833
+ expected = [
834
+ :callback_after_failure,
835
+ :observer_after_failure_ignite,
836
+ :observer_after_failure_transition
837
+ ]
838
+
839
+ assert_equal expected, @notifications
840
+ end
841
+ end
842
+
743
843
  class MachineWithMixedCallbacksTest < BaseTestCase
744
844
  def setup
745
845
  @model = new_model { include ActiveModel::Observing }
@@ -915,7 +1015,7 @@ module ActiveModelTest
915
1015
  def test_should_only_add_locale_once_in_load_path
916
1016
  assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_model/locale\.rb$}}.length
917
1017
 
918
- # Create another ActiveRecord model that will triger the i18n feature
1018
+ # Create another ActiveModel model that will triger the i18n feature
919
1019
  new_model
920
1020
 
921
1021
  assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_model/locale\.rb$}}.length
@@ -3,6 +3,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
3
3
  # Load library
4
4
  require 'rubygems'
5
5
 
6
+ gem 'i18n', '<0.5' if ENV['VERSION'] && ENV['VERSION'] >= '2.3.5' && ENV['VERSION'] < '3.0.0'
6
7
  gem 'activerecord', ENV['VERSION'] ? "=#{ENV['VERSION']}" : '>=2.0.0'
7
8
  require 'active_record'
8
9
 
@@ -40,9 +41,12 @@ module ActiveRecordTest
40
41
  connection.create_table(table_name, :force => true) {|t| t.string(:state)} if create_table
41
42
  set_table_name(table_name.to_s)
42
43
 
43
- def self.name; "ActiveRecordTest::#{table_name.capitalize}"; end
44
+ (class << self; self; end).class_eval do
45
+ define_method(:name) { "ActiveRecordTest::#{table_name.to_s.capitalize}" }
46
+ end
44
47
  end
45
48
  model.class_eval(&block) if block_given?
49
+ model.reset_column_information if create_table
46
50
  model
47
51
  end
48
52
 
@@ -639,6 +643,37 @@ module ActiveRecordTest
639
643
  assert_equal %w(parked parked), @record.changes['status']
640
644
  end
641
645
  end
646
+
647
+ class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
648
+ def setup
649
+ @model = new_model
650
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
651
+ @machine.event :ignite
652
+
653
+ @record = @model.create
654
+ @record.state_event = 'ignite'
655
+ end
656
+
657
+ def test_should_include_state_in_changed_attributes
658
+ assert_equal %w(state), @record.changed
659
+ end
660
+
661
+ def test_should_track_attribute_change
662
+ assert_equal %w(parked parked), @record.changes['state']
663
+ end
664
+
665
+ def test_should_not_reset_changes_on_multiple_changes
666
+ @record.state_event = 'ignite'
667
+ assert_equal %w(parked parked), @record.changes['state']
668
+ end
669
+
670
+ def test_should_not_include_state_in_changed_attributes_if_nil
671
+ @record = @model.create
672
+ @record.state_event = nil
673
+
674
+ assert_equal [], @record.changed
675
+ end
676
+ end
642
677
  else
643
678
  $stderr.puts 'Skipping ActiveRecord Dirty tests. `gem install active_record` >= v2.1.0 and try again.'
644
679
  end
@@ -858,13 +893,8 @@ module ActiveRecordTest
858
893
  @callbacks = []
859
894
  @machine.before_transition {@callbacks << :before}
860
895
  @machine.after_transition {@callbacks << :after}
861
- @machine.after_transition(:include_failures => true) {@callbacks << :after_failure}
896
+ @machine.after_failure {@callbacks << :after_failure}
862
897
  @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
863
- @machine.around_transition(:include_failures => true) do |block|
864
- @callbacks << :around_before_failure
865
- block.call
866
- @callbacks << :around_after_failure
867
- end
868
898
 
869
899
  @record = @model.new(:state => 'parked')
870
900
  @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
@@ -884,7 +914,7 @@ module ActiveRecordTest
884
914
  end
885
915
 
886
916
  def test_should_run_before_callbacks_and_after_callbacks_with_failures
887
- assert_equal [:before, :around_before, :around_before_failure, :around_after_failure, :after_failure], @callbacks
917
+ assert_equal [:before, :around_before, :after_failure], @callbacks
888
918
  end
889
919
  end
890
920
 
@@ -1090,14 +1120,14 @@ module ActiveRecordTest
1090
1120
  assert !ran_callback
1091
1121
  end
1092
1122
 
1093
- def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
1123
+ def test_should_run_after_callbacks_if_validation_fails
1094
1124
  @model.class_eval do
1095
1125
  attr_accessor :seatbelt
1096
1126
  validates_presence_of :seatbelt
1097
1127
  end
1098
1128
 
1099
1129
  ran_callback = false
1100
- @machine.after_transition(:include_failures => true) { ran_callback = true }
1130
+ @machine.after_failure { ran_callback = true }
1101
1131
 
1102
1132
  @record.valid?
1103
1133
  assert ran_callback
@@ -1124,19 +1154,6 @@ module ActiveRecordTest
1124
1154
  assert !ran_callback
1125
1155
  end
1126
1156
 
1127
- def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_validation_fails
1128
- @model.class_eval do
1129
- attr_accessor :seatbelt
1130
- validates_presence_of :seatbelt
1131
- end
1132
-
1133
- ran_callback = false
1134
- @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
1135
-
1136
- @record.valid?
1137
- assert ran_callback
1138
- end
1139
-
1140
1157
  def test_should_not_run_before_transitions_within_transaction
1141
1158
  @machine.before_transition { @model.create; raise ActiveRecord::Rollback }
1142
1159
 
@@ -1227,17 +1244,17 @@ module ActiveRecordTest
1227
1244
  assert !ran_callback
1228
1245
  end
1229
1246
 
1230
- def test_should_run_after_callbacks_with_failures_enabled_if_fails
1247
+ def test_should_run_failure_callbacks__if_fails
1231
1248
  @model.before_create {|record| false}
1232
1249
 
1233
1250
  ran_callback = false
1234
- @machine.after_transition(:include_failures => true) { ran_callback = true }
1251
+ @machine.after_failure { ran_callback = true }
1235
1252
 
1236
1253
  begin; @record.save; rescue; end
1237
1254
  assert ran_callback
1238
1255
  end
1239
1256
 
1240
- def test_should_not_run_around_callbacks_with_failures_disabled_if_fails
1257
+ def test_should_not_run_around_callbacks_if_fails
1241
1258
  @model.before_create {|record| false}
1242
1259
 
1243
1260
  ran_callback = false
@@ -1255,16 +1272,6 @@ module ActiveRecordTest
1255
1272
  assert ran_callback
1256
1273
  end
1257
1274
 
1258
- def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_fails
1259
- @model.before_create {|record| false}
1260
-
1261
- ran_callback = false
1262
- @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
1263
-
1264
- begin; @record.save; rescue; end
1265
- assert ran_callback
1266
- end
1267
-
1268
1275
  def test_should_run_before_transitions_within_transaction
1269
1276
  @machine.before_transition { @model.create; raise ActiveRecord::Rollback }
1270
1277
 
@@ -1493,24 +1500,21 @@ module ActiveRecordTest
1493
1500
  assert_equal [instance], instance.notifications
1494
1501
  end
1495
1502
 
1496
- def test_should_use_original_observer_behavior_to_handle_non_state_machine_callbacks
1503
+ def test_should_continue_to_handle_non_state_machine_callbacks
1497
1504
  observer = new_observer(@model) do
1498
1505
  def before_save(object)
1506
+ notifications << [:before_save, @object]
1499
1507
  end
1500
1508
 
1501
1509
  def before_ignite(*args)
1502
- end
1503
-
1504
- def update_without_multiple_args(observed_method, object)
1505
- notifications << [observed_method, object] if [:before_save, :before_ignite].include?(observed_method)
1506
- super
1510
+ notifications << :before_ignite
1507
1511
  end
1508
1512
  end
1509
1513
 
1510
1514
  instance = observer.instance
1511
1515
 
1512
1516
  @transition.perform
1513
- assert_equal [[:before_save, @record]], instance.notifications
1517
+ assert_equal [:before_ignite, [:before_save, @object]], instance.notifications
1514
1518
  end
1515
1519
  end
1516
1520
 
@@ -1549,6 +1553,48 @@ module ActiveRecordTest
1549
1553
  end
1550
1554
  end
1551
1555
 
1556
+ class MachineWithFailureCallbacksTest < BaseTestCase
1557
+ def setup
1558
+ @model = new_model
1559
+ @machine = StateMachine::Machine.new(@model)
1560
+ @machine.state :parked, :idling
1561
+ @machine.event :ignite
1562
+ @record = @model.new(:state => 'parked')
1563
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
1564
+
1565
+ @notifications = []
1566
+
1567
+ # Create callbacks
1568
+ @machine.before_transition {false}
1569
+ @machine.after_failure {@notifications << :callback_after_failure}
1570
+
1571
+ # Create observer callbacks
1572
+ observer = new_observer(@model) do
1573
+ def after_failure_to_ignite(*args)
1574
+ notifications << :observer_after_failure_ignite
1575
+ end
1576
+
1577
+ def after_failure_to_transition(*args)
1578
+ notifications << :observer_after_failure_transition
1579
+ end
1580
+ end
1581
+ instance = observer.instance
1582
+ instance.notifications = @notifications
1583
+
1584
+ @transition.perform
1585
+ end
1586
+
1587
+ def test_should_invoke_callbacks_in_specific_order
1588
+ expected = [
1589
+ :callback_after_failure,
1590
+ :observer_after_failure_ignite,
1591
+ :observer_after_failure_transition
1592
+ ]
1593
+
1594
+ assert_equal expected, @notifications
1595
+ end
1596
+ end
1597
+
1552
1598
  class MachineWithMixedCallbacksTest < BaseTestCase
1553
1599
  def setup
1554
1600
  @model = new_model
@@ -1756,6 +1802,7 @@ module ActiveRecordTest
1756
1802
  I18n.backend = I18n::Backend::Simple.new
1757
1803
 
1758
1804
  # Initialize the backend
1805
+ StateMachine::Machine.new(new_model)
1759
1806
  I18n.backend.translate(:en, 'activerecord.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
1760
1807
 
1761
1808
  @model = new_model
@@ -0,0 +1,87 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ # Load library
4
+ require 'rubygems'
5
+
6
+ module BaseTest
7
+ class IntegrationTest < Test::Unit::TestCase
8
+ def test_should_not_match_any_classes
9
+ assert !StateMachine::Integrations::Base.matches?(Class.new)
10
+ end
11
+ end
12
+
13
+ class IncludedTest < Test::Unit::TestCase
14
+ def setup
15
+ @integration = Module.new
16
+ StateMachine::Integrations.const_set('Custom', @integration)
17
+
18
+ @integration.class_eval do
19
+ include StateMachine::Integrations::Base
20
+ end
21
+ end
22
+
23
+ def test_should_not_have_any_defaults
24
+ assert_nil @integration.defaults
25
+ end
26
+
27
+ def test_should_not_have_any_versions
28
+ assert_equal [], @integration.versions
29
+ end
30
+
31
+ def test_should_track_version
32
+ version1 = @integration.version '1.0' do
33
+ def self.active?
34
+ true
35
+ end
36
+ end
37
+
38
+ version2 = @integration.version '2.0' do
39
+ def self.active?
40
+ false
41
+ end
42
+ end
43
+
44
+ assert_equal [version1, version2], @integration.versions
45
+ end
46
+
47
+ def test_should_allow_active_versions_to_override_default_behavior
48
+ @integration.class_eval do
49
+ def version1_included?
50
+ false
51
+ end
52
+
53
+ def version2_included?
54
+ false
55
+ end
56
+ end
57
+
58
+ version1 = @integration.version '1.0' do
59
+ def self.active?
60
+ true
61
+ end
62
+
63
+ def version1_included?
64
+ true
65
+ end
66
+ end
67
+
68
+ version2 = @integration.version '2.0' do
69
+ def self.active?
70
+ false
71
+ end
72
+
73
+ def version2_included?
74
+ true
75
+ end
76
+ end
77
+
78
+ @machine = StateMachine::Machine.new(Class.new, :integration => :custom)
79
+ assert @machine.version1_included?
80
+ assert !@machine.version2_included?
81
+ end
82
+
83
+ def teardown
84
+ StateMachine::Integrations.send(:remove_const, 'Custom')
85
+ end
86
+ end
87
+ end