state_machine 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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