state_machine 1.0.2 → 1.0.3

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 (102) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +0 -2
  3. data/.yardopts +3 -2
  4. data/Appraisals +48 -0
  5. data/{CHANGELOG.rdoc → CHANGELOG.md} +63 -46
  6. data/README.md +1029 -0
  7. data/gemfiles/active_model-3.0.0.gemfile.lock +1 -3
  8. data/gemfiles/active_model-3.0.5.gemfile.lock +1 -3
  9. data/gemfiles/active_model-3.1.1.gemfile +7 -0
  10. data/gemfiles/active_model-3.1.1.gemfile.lock +32 -0
  11. data/gemfiles/active_record-2.0.0.gemfile.lock +1 -3
  12. data/gemfiles/active_record-2.0.5.gemfile.lock +1 -3
  13. data/gemfiles/active_record-2.1.0.gemfile.lock +1 -3
  14. data/gemfiles/active_record-2.1.2.gemfile.lock +1 -3
  15. data/gemfiles/active_record-2.2.3.gemfile.lock +1 -3
  16. data/gemfiles/active_record-2.3.12.gemfile.lock +1 -3
  17. data/gemfiles/active_record-3.0.0.gemfile.lock +1 -3
  18. data/gemfiles/active_record-3.0.5.gemfile.lock +1 -3
  19. data/gemfiles/active_record-3.1.1.gemfile +8 -0
  20. data/gemfiles/active_record-3.1.1.gemfile.lock +43 -0
  21. data/gemfiles/data_mapper-0.10.2.gemfile.lock +1 -3
  22. data/gemfiles/data_mapper-0.9.11.gemfile.lock +1 -3
  23. data/gemfiles/data_mapper-0.9.4.gemfile.lock +1 -3
  24. data/gemfiles/data_mapper-0.9.7.gemfile.lock +1 -3
  25. data/gemfiles/data_mapper-1.0.0.gemfile.lock +1 -3
  26. data/gemfiles/data_mapper-1.0.1.gemfile.lock +1 -3
  27. data/gemfiles/data_mapper-1.0.2.gemfile.lock +1 -3
  28. data/gemfiles/data_mapper-1.1.0.gemfile.lock +1 -3
  29. data/gemfiles/data_mapper-1.2.0.gemfile +12 -0
  30. data/gemfiles/data_mapper-1.2.0.gemfile.lock +49 -0
  31. data/gemfiles/default.gemfile.lock +1 -3
  32. data/gemfiles/graphviz-0.9.0.gemfile +7 -0
  33. data/gemfiles/graphviz-0.9.0.gemfile.lock +24 -0
  34. data/gemfiles/graphviz-0.9.21.gemfile +7 -0
  35. data/gemfiles/graphviz-0.9.21.gemfile.lock +24 -0
  36. data/gemfiles/graphviz-1.0.0.gemfile +7 -0
  37. data/gemfiles/graphviz-1.0.0.gemfile.lock +24 -0
  38. data/gemfiles/mongo_mapper-0.10.0.gemfile +7 -0
  39. data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +41 -0
  40. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +1 -3
  41. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +1 -3
  42. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +1 -3
  43. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +1 -3
  44. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +1 -3
  45. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +1 -3
  46. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +1 -3
  47. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +1 -3
  48. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +1 -3
  49. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +1 -3
  50. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +1 -3
  51. data/gemfiles/mongoid-2.0.0.gemfile.lock +1 -3
  52. data/gemfiles/mongoid-2.1.4.gemfile.lock +1 -3
  53. data/gemfiles/mongoid-2.2.4.gemfile +7 -0
  54. data/gemfiles/mongoid-2.2.4.gemfile.lock +40 -0
  55. data/gemfiles/mongoid-2.3.3.gemfile +7 -0
  56. data/gemfiles/mongoid-2.3.3.gemfile.lock +40 -0
  57. data/gemfiles/sequel-2.11.0.gemfile.lock +1 -3
  58. data/gemfiles/sequel-2.12.0.gemfile.lock +1 -3
  59. data/gemfiles/sequel-2.8.0.gemfile.lock +1 -3
  60. data/gemfiles/sequel-3.0.0.gemfile.lock +1 -3
  61. data/gemfiles/sequel-3.13.0.gemfile.lock +1 -3
  62. data/gemfiles/sequel-3.14.0.gemfile.lock +1 -3
  63. data/gemfiles/sequel-3.23.0.gemfile.lock +1 -3
  64. data/gemfiles/sequel-3.24.0.gemfile.lock +1 -3
  65. data/gemfiles/sequel-3.29.0.gemfile +8 -0
  66. data/gemfiles/sequel-3.29.0.gemfile.lock +26 -0
  67. data/lib/state_machine.rb +45 -0
  68. data/lib/state_machine/event.rb +18 -3
  69. data/lib/state_machine/event_collection.rb +1 -1
  70. data/lib/state_machine/integrations/active_model.rb +59 -16
  71. data/lib/state_machine/integrations/active_model/observer.rb +3 -15
  72. data/lib/state_machine/integrations/active_record.rb +46 -9
  73. data/lib/state_machine/integrations/data_mapper.rb +42 -2
  74. data/lib/state_machine/integrations/data_mapper/versions.rb +22 -10
  75. data/lib/state_machine/integrations/mongo_mapper.rb +55 -0
  76. data/lib/state_machine/integrations/mongo_mapper/versions.rb +3 -3
  77. data/lib/state_machine/integrations/mongoid.rb +57 -12
  78. data/lib/state_machine/integrations/mongoid/versions.rb +22 -4
  79. data/lib/state_machine/integrations/sequel.rb +45 -0
  80. data/lib/state_machine/integrations/sequel/versions.rb +3 -0
  81. data/lib/state_machine/machine.rb +148 -34
  82. data/lib/state_machine/node_collection.rb +36 -3
  83. data/lib/state_machine/state.rb +6 -3
  84. data/lib/state_machine/state_collection.rb +1 -1
  85. data/lib/state_machine/version.rb +1 -1
  86. data/lib/tasks/state_machine.rb +11 -9
  87. data/state_machine.gemspec +2 -3
  88. data/test/functional/state_machine_test.rb +54 -1
  89. data/test/unit/event_collection_test.rb +4 -0
  90. data/test/unit/event_test.rb +34 -1
  91. data/test/unit/integrations/active_model_test.rb +80 -0
  92. data/test/unit/integrations/active_record_test.rb +105 -2
  93. data/test/unit/integrations/data_mapper_test.rb +27 -25
  94. data/test/unit/integrations/mongo_mapper_test.rb +80 -25
  95. data/test/unit/integrations/mongoid_test.rb +61 -6
  96. data/test/unit/integrations/sequel_test.rb +8 -2
  97. data/test/unit/machine_test.rb +87 -9
  98. data/test/unit/node_collection_test.rb +129 -12
  99. data/test/unit/state_collection_test.rb +4 -0
  100. data/test/unit/state_test.rb +2 -2
  101. metadata +30 -24
  102. data/README.rdoc +0 -844
@@ -24,6 +24,7 @@ module StateMachine
24
24
  @nodes = []
25
25
  @indices = Array(options[:index]).inject({}) {|indices, attribute| indices[attribute] = {}; indices}
26
26
  @default_index = Array(options[:index]).first
27
+ @contexts = []
27
28
  end
28
29
 
29
30
  # Creates a copy of this collection such that modifications don't affect
@@ -32,9 +33,15 @@ module StateMachine
32
33
  super
33
34
 
34
35
  nodes = @nodes
36
+ contexts = @contexts
35
37
  @nodes = []
38
+ @contexts = []
36
39
  @indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
40
+
41
+ # Add nodes *prior* to copying over the contexts so that they don't get
42
+ # evaluated multiple times
37
43
  concat(nodes.map {|n| n.dup})
44
+ @contexts = contexts.dup
38
45
  end
39
46
 
40
47
  # Changes the current machine associated with the collection. In turn, this
@@ -54,11 +61,26 @@ module StateMachine
54
61
  index(index_name).keys
55
62
  end
56
63
 
64
+ # Tracks a context that should be evaluated for any nodes that get added
65
+ # which match the given set of nodes. Matchers can be used so that the
66
+ # context can get added once and evaluated after multiple adds.
67
+ def context(nodes, &block)
68
+ nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes)
69
+ @contexts << context = {:nodes => nodes, :block => block}
70
+
71
+ # Evaluate the new context for existing nodes
72
+ each {|node| eval_context(context, node)}
73
+
74
+ context
75
+ end
76
+
57
77
  # Adds a new node to the collection. By doing so, this will also add it to
58
- # the configured indices.
78
+ # the configured indices. This will also evaluate any existings contexts
79
+ # that match the new node.
59
80
  def <<(node)
60
81
  @nodes << node
61
82
  @indices.each {|attribute, index| index[value(node, attribute)] = node}
83
+ @contexts.each {|context| eval_context(context, node)}
62
84
  self
63
85
  end
64
86
 
@@ -122,7 +144,12 @@ module StateMachine
122
144
  #
123
145
  # If the key cannot be found, then nil will be returned.
124
146
  def [](key, index_name = @default_index)
125
- index(index_name)[key]
147
+ index = self.index(index_name)
148
+ if index.include?(key)
149
+ index[key]
150
+ elsif @indices.include?(:"#{index_name}_to_s")
151
+ self[key.to_s, :"#{index_name}_to_s"]
152
+ end
126
153
  end
127
154
 
128
155
  # Gets the node indexed by the given key. By default, this will look up the
@@ -141,7 +168,7 @@ module StateMachine
141
168
  self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
142
169
  end
143
170
 
144
- private
171
+ protected
145
172
  # Gets the given index. If the index does not exist, then an ArgumentError
146
173
  # is raised.
147
174
  def index(name)
@@ -153,5 +180,11 @@ module StateMachine
153
180
  def value(node, attribute)
154
181
  node.send(attribute)
155
182
  end
183
+
184
+ # Evaluates the given context for a particular node. This will only
185
+ # evaluate the context if the node matches.
186
+ def eval_context(context, node)
187
+ node.context(&context[:block]) if context[:nodes].matches?(node.name)
188
+ end
156
189
  end
157
190
  end
@@ -96,6 +96,11 @@ module StateMachine
96
96
  @methods = methods.dup
97
97
  end
98
98
 
99
+ # Converts the name of this state to a string
100
+ def name_to_s
101
+ name.to_s
102
+ end
103
+
99
104
  # Determines whether there are any states that can be transitioned to from
100
105
  # this state. If there are none, then this state is considered *final*.
101
106
  # Any objects in a final state will remain so forever given the current
@@ -179,9 +184,7 @@ module StateMachine
179
184
  # This can be called multiple times. Each time a new context is created,
180
185
  # a new module will be included in the owner class.
181
186
  def context(&block)
182
- owner_class = machine.owner_class
183
187
  machine_name = machine.name
184
- name = self.name
185
188
 
186
189
  # Evaluate the method definitions
187
190
  context = StateContext.new(self)
@@ -199,7 +202,7 @@ module StateMachine
199
202
 
200
203
  # Include the context so that it can be bound to the owner class (the
201
204
  # context is considered an ancestor, so it's allowed to be bound)
202
- owner_class.class_eval { include context }
205
+ machine.owner_class.class_eval { include context }
203
206
 
204
207
  context
205
208
  end
@@ -4,7 +4,7 @@ module StateMachine
4
4
  # Represents a collection of states in a state machine
5
5
  class StateCollection < NodeCollection
6
6
  def initialize(machine) #:nodoc:
7
- super(machine, :index => [:name, :qualified_name, :value])
7
+ super(machine, :index => [:name, :name_to_s, :qualified_name, :value])
8
8
  end
9
9
 
10
10
  # Determines whether the given object is in a specific state. If the
@@ -1,3 +1,3 @@
1
1
  module StateMachine
2
- VERSION = '1.0.2'
2
+ VERSION = '1.0.3'
3
3
  end
@@ -1,9 +1,19 @@
1
1
  namespace :state_machine do
2
- desc 'Draws a set of state machines using GraphViz. Target files to load with FILE=x,y,z; Machine class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x; Orientation with ORIENTATION=x'
2
+ desc 'Draws state machines using GraphViz (options: CLASS=User,Vehicle; FILE=user.rb,vehicle.rb [not required in Rails / Merb]; FONT=Arial; FORMAT=png; ORIENTATION=portrait'
3
3
  task :draw do
4
+ # Build drawing options
5
+ options = {}
6
+ options[:file] = ENV['FILE'] if ENV['FILE']
7
+ options[:path] = ENV['TARGET'] if ENV['TARGET']
8
+ options[:format] = ENV['FORMAT'] if ENV['FORMAT']
9
+ options[:font] = ENV['FONT'] if ENV['FONT']
10
+ options[:orientation] = ENV['ORIENTATION'] if ENV['ORIENTATION']
11
+
4
12
  if defined?(Rails)
13
+ puts "Files are automatically loaded in Rails; ignoring FILE option" if options.delete(:file)
5
14
  Rake::Task['environment'].invoke
6
15
  elsif defined?(Merb)
16
+ puts "Files are automatically loaded in Merb; ignoring FILE option" if options.delete(:file)
7
17
  Rake::Task['merb_env'].invoke
8
18
 
9
19
  # Fix ruby-graphviz being incompatible with Merb's process title
@@ -14,14 +24,6 @@ namespace :state_machine do
14
24
  require 'state_machine'
15
25
  end
16
26
 
17
- # Build drawing options
18
- options = {}
19
- options[:file] = ENV['FILE'] if ENV['FILE']
20
- options[:path] = ENV['TARGET'] if ENV['TARGET']
21
- options[:format] = ENV['FORMAT'] if ENV['FORMAT']
22
- options[:font] = ENV['FONT'] if ENV['FONT']
23
- options[:orientation] = ENV['ORIENTATION'] if ENV['ORIENTATION']
24
-
25
27
  StateMachine::Machine.draw(ENV['CLASS'], options)
26
28
  end
27
29
  end
@@ -12,11 +12,10 @@ Gem::Specification.new do |s|
12
12
  s.require_paths = ["lib"]
13
13
  s.files = `git ls-files`.split("\n")
14
14
  s.test_files = `git ls-files -- test/*`.split("\n")
15
- s.rdoc_options = %w(--line-numbers --inline-source --title state_machine --main README.rdoc)
16
- s.extra_rdoc_files = %w(README.rdoc CHANGELOG.rdoc LICENSE)
15
+ s.rdoc_options = %w(--line-numbers --inline-source --title state_machine --main README.md)
16
+ s.extra_rdoc_files = %w(README.md CHANGELOG.md LICENSE)
17
17
 
18
18
  s.add_development_dependency("rake")
19
19
  s.add_development_dependency("rcov")
20
20
  s.add_development_dependency("appraisal", "~> 0.3.8")
21
- s.add_development_dependency("ruby-graphviz", "~> 1.0")
22
21
  end
@@ -74,6 +74,10 @@ class Vehicle < ModelBase
74
74
  vehicle.time_elapsed = Time.now - time
75
75
  end
76
76
 
77
+ event all do
78
+ transition :locked => :parked
79
+ end
80
+
77
81
  event :park do
78
82
  transition [:idling, :first_gear] => :parked
79
83
  end
@@ -191,7 +195,7 @@ end
191
195
  class TrafficLight
192
196
  state_machine :initial => :stop do
193
197
  event :cycle do
194
- transition :stop => :proceed, :proceed=> :caution, :caution => :stop
198
+ transition :stop => :proceed, :proceed => :caution, :caution => :stop
195
199
  end
196
200
 
197
201
  state :stop do
@@ -208,10 +212,20 @@ class TrafficLight
208
212
  end
209
213
  end
210
214
 
215
+ state all - :proceed do
216
+ def capture_violations?
217
+ true
218
+ end
219
+ end
220
+
211
221
  state :proceed do
212
222
  def color(transform)
213
223
  'green'
214
224
  end
225
+
226
+ def capture_violations?
227
+ false
228
+ end
215
229
  end
216
230
 
217
231
  state :caution do
@@ -713,6 +727,33 @@ class VehicleRepairedTest < Test::Unit::TestCase
713
727
  end
714
728
  end
715
729
 
730
+ class VehicleLockedTest < Test::Unit::TestCase
731
+ def setup
732
+ @vehicle = Vehicle.new
733
+ @vehicle.state = 'locked'
734
+ end
735
+
736
+ def test_should_be_parked_after_park
737
+ @vehicle.park
738
+ assert @vehicle.parked?
739
+ end
740
+
741
+ def test_should_be_parked_after_ignite
742
+ @vehicle.ignite
743
+ assert @vehicle.parked?
744
+ end
745
+
746
+ def test_should_be_parked_after_shift_up
747
+ @vehicle.shift_up
748
+ assert @vehicle.parked?
749
+ end
750
+
751
+ def test_should_be_parked_after_shift_down
752
+ @vehicle.shift_down
753
+ assert @vehicle.parked?
754
+ end
755
+ end
756
+
716
757
  class VehicleWithParallelEventsTest < Test::Unit::TestCase
717
758
  def setup
718
759
  @vehicle = Vehicle.new
@@ -968,6 +1009,10 @@ class TrafficLightStopTest < Test::Unit::TestCase
968
1009
  color = @light.color {|value| value.upcase!}
969
1010
  assert_equal 'RED', color
970
1011
  end
1012
+
1013
+ def test_should_use_stop_capture_violations
1014
+ assert_equal true, @light.capture_violations?
1015
+ end
971
1016
  end
972
1017
 
973
1018
  class TrafficLightProceedTest < Test::Unit::TestCase
@@ -979,6 +1024,10 @@ class TrafficLightProceedTest < Test::Unit::TestCase
979
1024
  def test_should_use_proceed_color
980
1025
  assert_equal 'green', @light.color
981
1026
  end
1027
+
1028
+ def test_should_use_proceed_capture_violations
1029
+ assert_equal false, @light.capture_violations?
1030
+ end
982
1031
  end
983
1032
 
984
1033
  class TrafficLightCautionTest < Test::Unit::TestCase
@@ -990,4 +1039,8 @@ class TrafficLightCautionTest < Test::Unit::TestCase
990
1039
  def test_should_use_caution_color
991
1040
  assert_equal 'yellow', @light.color
992
1041
  end
1042
+
1043
+ def test_should_use_caution_capture_violations
1044
+ assert_equal true, @light.capture_violations?
1045
+ end
993
1046
  end
@@ -40,6 +40,10 @@ class EventCollectionTest < Test::Unit::TestCase
40
40
  assert_equal @open, @events[:enable]
41
41
  end
42
42
 
43
+ def test_should_index_by_string_name
44
+ assert_equal @open, @events['enable']
45
+ end
46
+
43
47
  def test_should_index_by_qualified_name
44
48
  assert_equal @open, @events[:enable_alarm, :qualified_name]
45
49
  end
@@ -173,7 +173,7 @@ class EventWithConflictingHelpersBeforeDefinitionTest < Test::Unit::TestCase
173
173
 
174
174
  def test_should_output_warning
175
175
  expected = %w(can_ignite? ignite_transition ignite ignite!).map do |method|
176
- "Instance method \"#{method}\" is already defined in #{@superclass.to_s}, use generic helper instead.\n"
176
+ "Instance method \"#{method}\" is already defined in #{@superclass.to_s}, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true.\n"
177
177
  end.join
178
178
 
179
179
  assert_equal expected, $stderr.string
@@ -341,6 +341,20 @@ class EventWithNamespaceTest < Test::Unit::TestCase
341
341
  end
342
342
  end
343
343
 
344
+ class EventContextTest < Test::Unit::TestCase
345
+ def setup
346
+ @klass = Class.new
347
+ @machine = StateMachine::Machine.new(@klass)
348
+ @machine.events << @event = StateMachine::Event.new(@machine, :ignite, :human_name => 'start')
349
+ end
350
+
351
+ def test_should_evaluate_within_the_event
352
+ scope = nil
353
+ @event.context { scope = self }
354
+ assert_equal @event, scope
355
+ end
356
+ end
357
+
344
358
  class EventTransitionsTest < Test::Unit::TestCase
345
359
  def setup
346
360
  @machine = StateMachine::Machine.new(Class.new)
@@ -568,6 +582,25 @@ class EventWithMatchingDisabledTransitionsTest < Test::Unit::TestCase
568
582
  assert_equal ['cannot transition via "start"'], @object.errors
569
583
  end
570
584
 
585
+ def test_should_invalid_with_human_state_name_if_specified
586
+ klass = Class.new do
587
+ attr_accessor :errors
588
+ end
589
+
590
+ machine = StateMachine::Machine.new(klass, :integration => :custom, :messages => {:invalid_transition => 'cannot transition via "%s" from "%s"'})
591
+ parked, idling = machine.state :parked, :idling
592
+ parked.human_name = 'stopped'
593
+
594
+ machine.events << event = StateMachine::Event.new(machine, :ignite)
595
+ event.transition(:parked => :idling, :if => lambda {false})
596
+
597
+ object = @klass.new
598
+ object.state = 'parked'
599
+
600
+ event.fire(object)
601
+ assert_equal ['cannot transition via "ignite" from "stopped"'], object.errors
602
+ end
603
+
571
604
  def test_should_reset_existing_error
572
605
  @object.errors = ['invalid']
573
606
 
@@ -798,6 +798,54 @@ module ActiveModelTest
798
798
  @transition.perform
799
799
  assert_equal [instance], instance.notifications
800
800
  end
801
+
802
+ def test_should_support_nil_from_states
803
+ callbacks = [
804
+ :before_ignite_from_nil_to_idling,
805
+ :before_ignite_from_nil,
806
+ :before_transition_state_from_nil_to_idling,
807
+ :before_transition_state_from_nil
808
+ ]
809
+
810
+ notified = false
811
+ observer = new_observer(@model) do
812
+ callbacks.each do |callback|
813
+ define_method(callback) do |*args|
814
+ notifications << callback
815
+ end
816
+ end
817
+ end
818
+
819
+ instance = observer.instance
820
+
821
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, nil, :idling)
822
+ transition.perform
823
+ assert_equal callbacks, instance.notifications
824
+ end
825
+
826
+ def test_should_support_nil_to_states
827
+ callbacks = [
828
+ :before_ignite_from_parked_to_nil,
829
+ :before_ignite_to_nil,
830
+ :before_transition_state_from_parked_to_nil,
831
+ :before_transition_state_to_nil
832
+ ]
833
+
834
+ notified = false
835
+ observer = new_observer(@model) do
836
+ callbacks.each do |callback|
837
+ define_method(callback) do |*args|
838
+ notifications << callback
839
+ end
840
+ end
841
+ end
842
+
843
+ instance = observer.instance
844
+
845
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, nil)
846
+ transition.perform
847
+ assert_equal callbacks, instance.notifications
848
+ end
801
849
  end
802
850
 
803
851
  class MachineWithNamespacedObserversTest < BaseTestCase
@@ -994,6 +1042,17 @@ module ActiveModelTest
994
1042
  assert_equal 'shutdown', machine.state(:parked).human_name
995
1043
  end
996
1044
 
1045
+ def test_should_allow_customized_state_key_scoped_to_class
1046
+ I18n.backend.store_translations(:en, {
1047
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:states => {:parked => 'shutdown'}}}}
1048
+ })
1049
+
1050
+ machine = StateMachine::Machine.new(@model)
1051
+ machine.state :parked
1052
+
1053
+ assert_equal 'shutdown', machine.state(:parked).human_name
1054
+ end
1055
+
997
1056
  def test_should_allow_customized_state_key_scoped_to_machine
998
1057
  I18n.backend.store_translations(:en, {
999
1058
  :activemodel => {:state_machines => {:state => {:states => {:parked => 'shutdown'}}}}
@@ -1016,6 +1075,16 @@ module ActiveModelTest
1016
1075
  assert_equal 'shutdown', machine.state(:parked).human_name
1017
1076
  end
1018
1077
 
1078
+ def test_should_support_nil_state_key
1079
+ I18n.backend.store_translations(:en, {
1080
+ :activemodel => {:state_machines => {:states => {:nil => 'empty'}}}
1081
+ })
1082
+
1083
+ machine = StateMachine::Machine.new(@model)
1084
+
1085
+ assert_equal 'empty', machine.state(nil).human_name
1086
+ end
1087
+
1019
1088
  def test_should_allow_customized_event_key_scoped_to_class_and_machine
1020
1089
  I18n.backend.store_translations(:en, {
1021
1090
  :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:events => {:park => 'stop'}}}}}
@@ -1027,6 +1096,17 @@ module ActiveModelTest
1027
1096
  assert_equal 'stop', machine.event(:park).human_name
1028
1097
  end
1029
1098
 
1099
+ def test_should_allow_customized_event_key_scoped_to_class
1100
+ I18n.backend.store_translations(:en, {
1101
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:events => {:park => 'stop'}}}}
1102
+ })
1103
+
1104
+ machine = StateMachine::Machine.new(@model)
1105
+ machine.event :park
1106
+
1107
+ assert_equal 'stop', machine.event(:park).human_name
1108
+ end
1109
+
1030
1110
  def test_should_allow_customized_event_key_scoped_to_machine
1031
1111
  I18n.backend.store_translations(:en, {
1032
1112
  :activemodel => {:state_machines => {:state => {:events => {:park => 'stop'}}}}