state_machine 1.0.3 → 1.1.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 (117) hide show
  1. data/.travis.yml +23 -7
  2. data/Appraisals +31 -25
  3. data/CHANGELOG.md +12 -0
  4. data/README.md +3 -1
  5. data/gemfiles/active_model-3.0.0.gemfile.lock +7 -5
  6. data/gemfiles/active_model-3.0.5.gemfile.lock +7 -5
  7. data/gemfiles/active_model-3.1.1.gemfile.lock +6 -4
  8. data/gemfiles/active_record-2.0.0.gemfile +1 -1
  9. data/gemfiles/active_record-2.0.0.gemfile.lock +7 -5
  10. data/gemfiles/active_record-2.0.5.gemfile +1 -1
  11. data/gemfiles/active_record-2.0.5.gemfile.lock +7 -5
  12. data/gemfiles/active_record-2.1.0.gemfile +1 -1
  13. data/gemfiles/active_record-2.1.0.gemfile.lock +7 -5
  14. data/gemfiles/active_record-2.1.2.gemfile +1 -1
  15. data/gemfiles/active_record-2.1.2.gemfile.lock +7 -5
  16. data/gemfiles/active_record-2.2.3.gemfile +1 -1
  17. data/gemfiles/active_record-2.2.3.gemfile.lock +7 -5
  18. data/gemfiles/active_record-2.3.12.gemfile +1 -1
  19. data/gemfiles/active_record-2.3.12.gemfile.lock +7 -5
  20. data/gemfiles/active_record-3.0.0.gemfile +1 -1
  21. data/gemfiles/active_record-3.0.0.gemfile.lock +8 -6
  22. data/gemfiles/active_record-3.0.5.gemfile +1 -1
  23. data/gemfiles/active_record-3.0.5.gemfile.lock +8 -6
  24. data/gemfiles/active_record-3.1.1.gemfile +1 -1
  25. data/gemfiles/active_record-3.1.1.gemfile.lock +7 -5
  26. data/gemfiles/data_mapper-0.10.2.gemfile +3 -3
  27. data/gemfiles/data_mapper-0.10.2.gemfile.lock +14 -5
  28. data/gemfiles/data_mapper-0.9.11.gemfile +3 -3
  29. data/gemfiles/data_mapper-0.9.11.gemfile.lock +7 -5
  30. data/gemfiles/data_mapper-0.9.4.gemfile +3 -3
  31. data/gemfiles/data_mapper-0.9.4.gemfile.lock +13 -13
  32. data/gemfiles/data_mapper-0.9.7.gemfile +3 -3
  33. data/gemfiles/data_mapper-0.9.7.gemfile.lock +13 -13
  34. data/gemfiles/data_mapper-1.0.0.gemfile +3 -3
  35. data/gemfiles/data_mapper-1.0.0.gemfile.lock +17 -8
  36. data/gemfiles/data_mapper-1.0.1.gemfile +3 -3
  37. data/gemfiles/data_mapper-1.0.1.gemfile.lock +17 -8
  38. data/gemfiles/data_mapper-1.0.2.gemfile +3 -3
  39. data/gemfiles/data_mapper-1.0.2.gemfile.lock +17 -8
  40. data/gemfiles/data_mapper-1.1.0.gemfile +3 -3
  41. data/gemfiles/data_mapper-1.1.0.gemfile.lock +17 -8
  42. data/gemfiles/data_mapper-1.2.0.gemfile +3 -3
  43. data/gemfiles/data_mapper-1.2.0.gemfile.lock +13 -4
  44. data/gemfiles/default.gemfile.lock +7 -5
  45. data/gemfiles/graphviz-0.9.0.gemfile.lock +6 -4
  46. data/gemfiles/graphviz-0.9.21.gemfile.lock +6 -4
  47. data/gemfiles/graphviz-1.0.0.gemfile.lock +6 -4
  48. data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +6 -3
  49. data/gemfiles/mongo_mapper-0.5.5.gemfile +1 -1
  50. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +7 -5
  51. data/gemfiles/mongo_mapper-0.5.8.gemfile +1 -1
  52. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +7 -5
  53. data/gemfiles/mongo_mapper-0.6.0.gemfile +1 -1
  54. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +7 -5
  55. data/gemfiles/mongo_mapper-0.6.10.gemfile +1 -1
  56. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +7 -5
  57. data/gemfiles/mongo_mapper-0.7.0.gemfile +1 -1
  58. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +7 -5
  59. data/gemfiles/mongo_mapper-0.7.5.gemfile +1 -1
  60. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +7 -5
  61. data/gemfiles/mongo_mapper-0.8.0.gemfile +2 -2
  62. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +7 -5
  63. data/gemfiles/mongo_mapper-0.8.3.gemfile +2 -2
  64. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +7 -5
  65. data/gemfiles/mongo_mapper-0.8.4.gemfile +1 -1
  66. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +11 -8
  67. data/gemfiles/mongo_mapper-0.8.6.gemfile +1 -1
  68. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +11 -8
  69. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +14 -11
  70. data/gemfiles/mongoid-2.0.0.gemfile.lock +22 -17
  71. data/gemfiles/mongoid-2.1.4.gemfile.lock +21 -16
  72. data/gemfiles/mongoid-2.2.4.gemfile.lock +11 -8
  73. data/gemfiles/mongoid-2.3.3.gemfile.lock +11 -8
  74. data/gemfiles/sequel-2.11.0.gemfile.lock +7 -5
  75. data/gemfiles/sequel-2.12.0.gemfile.lock +7 -5
  76. data/gemfiles/sequel-2.8.0.gemfile.lock +7 -5
  77. data/gemfiles/sequel-3.0.0.gemfile.lock +7 -5
  78. data/gemfiles/sequel-3.13.0.gemfile.lock +7 -5
  79. data/gemfiles/sequel-3.14.0.gemfile.lock +7 -5
  80. data/gemfiles/sequel-3.23.0.gemfile.lock +7 -5
  81. data/gemfiles/sequel-3.24.0.gemfile.lock +7 -5
  82. data/gemfiles/sequel-3.29.0.gemfile.lock +5 -3
  83. data/lib/state_machine.rb +7 -1
  84. data/lib/state_machine/eval_helpers.rb +8 -9
  85. data/lib/state_machine/event.rb +10 -2
  86. data/lib/state_machine/integrations.rb +0 -1
  87. data/lib/state_machine/integrations/active_model.rb +46 -35
  88. data/lib/state_machine/integrations/active_record.rb +8 -0
  89. data/lib/state_machine/integrations/active_record/versions.rb +0 -20
  90. data/lib/state_machine/integrations/data_mapper.rb +22 -21
  91. data/lib/state_machine/integrations/data_mapper/versions.rb +0 -27
  92. data/lib/state_machine/integrations/mongo_mapper.rb +8 -0
  93. data/lib/state_machine/integrations/mongo_mapper/versions.rb +0 -4
  94. data/lib/state_machine/integrations/mongoid.rb +15 -2
  95. data/lib/state_machine/integrations/mongoid/versions.rb +0 -7
  96. data/lib/state_machine/integrations/sequel.rb +14 -0
  97. data/lib/state_machine/machine.rb +12 -0
  98. data/lib/state_machine/state.rb +4 -3
  99. data/lib/state_machine/state_context.rb +10 -9
  100. data/lib/state_machine/transition.rb +6 -1
  101. data/lib/state_machine/version.rb +1 -1
  102. data/state_machine.gemspec +1 -1
  103. data/test/functional/state_machine_test.rb +21 -1
  104. data/test/unit/event_test.rb +10 -0
  105. data/test/unit/integrations/active_model_test.rb +31 -29
  106. data/test/unit/integrations/active_record_test.rb +56 -26
  107. data/test/unit/integrations/data_mapper_test.rb +34 -57
  108. data/test/unit/integrations/mongo_mapper_test.rb +30 -24
  109. data/test/unit/integrations/mongoid_test.rb +114 -29
  110. data/test/unit/integrations/sequel_test.rb +36 -0
  111. data/test/unit/invalid_transition_test.rb +38 -0
  112. data/test/unit/machine_test.rb +38 -0
  113. data/test/unit/state_context_test.rb +29 -9
  114. data/test/unit/state_test.rb +21 -1
  115. data/test/unit/transition_collection_test.rb +72 -26
  116. data/test/unit/transition_test.rb +84 -73
  117. metadata +8 -8
@@ -9,10 +9,6 @@ module StateMachine
9
9
  def action_hook
10
10
  action
11
11
  end
12
-
13
- def mark_dirty(object, value)
14
- object.original_values[self.attribute] = "#{value}-ignored" if object.original_values[self.attribute] == value
15
- end
16
12
  end
17
13
 
18
14
  version '0.9.x - 0.10.x' do
@@ -37,29 +33,6 @@ module StateMachine
37
33
  end
38
34
  end
39
35
 
40
- version '0.10.x' do
41
- def self.active?
42
- ::DataMapper::VERSION =~ /^0\.10\./
43
- end
44
-
45
- def mark_dirty(object, value)
46
- property = owner_class.properties[self.attribute]
47
- object.original_attributes[property] = "#{value}-ignored" unless object.original_attributes.include?(property)
48
- end
49
- end
50
-
51
- version '1.0.x - 1.1.x' do
52
- def self.active?
53
- ::DataMapper::VERSION =~ /^1\.[01]\./
54
- end
55
-
56
- def mark_dirty(object, value)
57
- object.persisted_state = ::DataMapper::Resource::State::Dirty.new(object) if object.persisted_state.is_a?(::DataMapper::Resource::State::Clean)
58
- property = owner_class.properties[self.attribute]
59
- object.persisted_state.original_attributes[property] = value unless object.persisted_state.original_attributes.include?(property)
60
- end
61
- end
62
-
63
36
  version '1.0.0' do
64
37
  def self.active?
65
38
  ::DataMapper::VERSION == '1.0.0'
@@ -158,6 +158,14 @@ module StateMachine
158
158
  # *not* because a matching transition was not available, no error messages
159
159
  # will be added to the state attribute.
160
160
  #
161
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
162
+ # then the failure reason (such as the current validation errors) will be
163
+ # included in the exception that gets raised when the event fails. For
164
+ # example, assuming there's a validation on a field called +name+ on the class:
165
+ #
166
+ # vehicle = Vehicle.new
167
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
168
+ #
161
169
  # == Scopes
162
170
  #
163
171
  # To assist in filtering models with specific states, a series of basic
@@ -64,10 +64,6 @@ module StateMachine
64
64
  true
65
65
  end
66
66
 
67
- def supports_dirty_tracking?(object)
68
- true
69
- end
70
-
71
67
  def callback_terminator
72
68
  end
73
69
 
@@ -156,6 +156,14 @@ module StateMachine
156
156
  # *not* because a matching transition was not available, no error messages
157
157
  # will be added to the state attribute.
158
158
  #
159
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
160
+ # then the failure reason (such as the current validation errors) will be
161
+ # included in the exception that gets raised when the event fails. For
162
+ # example, assuming there's a validation on a field called +name+ on the class:
163
+ #
164
+ # vehicle = Vehicle.new
165
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
166
+ #
159
167
  # == Scopes
160
168
  #
161
169
  # To assist in filtering models with specific states, a series of basic
@@ -363,8 +371,13 @@ module StateMachine
363
371
  def define_state_initializer
364
372
  define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
365
373
  def initialize(*)
366
- @attributes = {}
367
- self.class.state_machines.initialize_states(self) { super }
374
+ @attributes ||= {}
375
+ self.class.state_machines.initialize_states(self, :dynamic => false)
376
+
377
+ super do |*args|
378
+ self.class.state_machines.initialize_states(self, :static => false)
379
+ yield(*args) if block_given?
380
+ end
368
381
  end
369
382
  end_eval
370
383
  end
@@ -49,13 +49,6 @@ module StateMachine
49
49
 
50
50
  result
51
51
  end
52
-
53
- protected
54
- # Mongoid uses its own implementation of dirty tracking instead of
55
- # ActiveModel's and doesn't support the #{attribute}_will_change! APIs
56
- def supports_dirty_tracking?(object)
57
- false
58
- end
59
52
  end
60
53
  end
61
54
  end
@@ -163,6 +163,14 @@ module StateMachine
163
163
  # *not* because a matching transition was not available, no error messages
164
164
  # will be added to the state attribute.
165
165
  #
166
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
167
+ # then the failure reason (such as the current validation errors) will be
168
+ # included in the exception that gets raised when the event fails. For
169
+ # example, assuming there's a validation on a field called +name+ on the class:
170
+ #
171
+ # vehicle = Vehicle.new
172
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
173
+ #
166
174
  # == Scopes
167
175
  #
168
176
  # To assist in filtering models with specific states, a series of class
@@ -297,6 +305,12 @@ module StateMachine
297
305
  object.errors.add(self.attribute(attribute), generate_message(message, values))
298
306
  end
299
307
 
308
+ # Describes the current validation errors on the given object. If none
309
+ # are specific, then the default error is interpeted as a "halt".
310
+ def errors_for(object)
311
+ object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
312
+ end
313
+
300
314
  # Resets any errors previously added when invalidating the given object
301
315
  def reset(object)
302
316
  object.errors.clear
@@ -1814,6 +1814,13 @@ module StateMachine
1814
1814
  def invalidate(object, attribute, message, values = [])
1815
1815
  end
1816
1816
 
1817
+ # Gets a description of the errors for the given object. This is used to
1818
+ # provide more detailed information when an InvalidTransition exception is
1819
+ # raised.
1820
+ def errors_for(object)
1821
+ ''
1822
+ end
1823
+
1817
1824
  # Resets any errors previously added when invalidating the given object.
1818
1825
  #
1819
1826
  # By default, this is a no-op.
@@ -1993,6 +2000,11 @@ module StateMachine
1993
2000
  machine.events.transitions_for(object, *args)
1994
2001
  end
1995
2002
 
2003
+ # Fire an arbitrary event for this machine
2004
+ define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
2005
+ machine.events.fetch(event).fire(object, *args)
2006
+ end
2007
+
1996
2008
  # Add helpers for tracking the event / transition to invoke when the
1997
2009
  # action is called
1998
2010
  if action
@@ -195,7 +195,7 @@ module StateMachine
195
195
  # Calls the method defined by the current state of the machine
196
196
  context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
197
197
  def #{method}(*args, &block)
198
- self.class.state_machine(#{machine_name.inspect}).states.match!(self).call(self, #{method.inspect}, lambda {super}, *args, &block)
198
+ self.class.state_machine(#{machine_name.inspect}).states.fetch(#{name.inspect}).call(self, #{method.inspect}, lambda {super(*args, &block)}, *args, &block)
199
199
  end
200
200
  end_eval
201
201
  end
@@ -213,11 +213,12 @@ module StateMachine
213
213
  # If the method has never been defined for this state, then a NoMethodError
214
214
  # will be raised.
215
215
  def call(object, method, method_missing = nil, *args, &block)
216
- if context_method = methods[method.to_sym]
216
+ if machine.states.matches?(object, name) && context_method = methods[method.to_sym]
217
217
  # Method is defined by the state: proxy it through
218
218
  context_method.bind(object).call(*args, &block)
219
219
  else
220
- # Dispatch to the superclass since this state doesn't handle the method
220
+ # Dispatch to the superclass since the object either isn't in this state
221
+ # or this state doesn't handle the method
221
222
  method_missing.call if method_missing
222
223
  end
223
224
  end
@@ -79,20 +79,21 @@ module StateMachine
79
79
  # *not* need to specify the <tt>:from</tt> option for the transition. For
80
80
  # example:
81
81
  #
82
- # state_machine do
83
- # state :parked do
84
- # transition :to => same, :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
85
- # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
86
- # end
87
- # end
82
+ # state_machine do
83
+ # state :parked do
84
+ # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
85
+ # transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
86
+ # end
87
+ # end
88
88
  #
89
89
  # See StateMachine::Machine#transition for a description of the possible
90
90
  # configurations for defining transitions.
91
91
  def transition(options)
92
- assert_valid_keys(options, :to, :on, :if, :unless)
93
- raise ArgumentError, 'Must specify :to state and :on event' unless options[:to] && options[:on]
92
+ assert_valid_keys(options, :from, :to, :on, :if, :unless)
93
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
94
+ raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
94
95
 
95
- machine.transition(options.merge(:from => state.name))
96
+ machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
96
97
  end
97
98
 
98
99
  # Hooks in condition-merging to methods that don't exist in this module
@@ -15,8 +15,11 @@ module StateMachine
15
15
  @from_state = machine.states.match!(object)
16
16
  @from = machine.read(object, :state)
17
17
  @event = machine.events.fetch(event)
18
+ errors = machine.errors_for(object)
18
19
 
19
- super(object, "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}")
20
+ message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
21
+ message << " (Reason(s): #{errors})" unless errors.empty?
22
+ super(object, message)
20
23
  end
21
24
 
22
25
  # The event that triggered the failed transition
@@ -351,6 +354,8 @@ module StateMachine
351
354
  # around callbacks when the remainder of the callback will be executed at
352
355
  # a later point in time.
353
356
  def pause
357
+ raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' if RUBY_PLATFORM == 'java'
358
+
354
359
  unless @resume_block
355
360
  require 'continuation' unless defined?(callcc)
356
361
  callcc do |block|
@@ -1,3 +1,3 @@
1
1
  module StateMachine
2
- VERSION = '1.0.3'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -17,5 +17,5 @@ Gem::Specification.new do |s|
17
17
 
18
18
  s.add_development_dependency("rake")
19
19
  s.add_development_dependency("rcov")
20
- s.add_development_dependency("appraisal", "~> 0.3.8")
20
+ s.add_development_dependency("appraisal", "~> 0.4.0")
21
21
  end
@@ -40,7 +40,7 @@ class ModelBase
40
40
  end
41
41
 
42
42
  class Vehicle < ModelBase
43
- attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed
43
+ attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed, :last_transition_args
44
44
 
45
45
  def initialize(attributes = {})
46
46
  attributes = {
@@ -58,6 +58,7 @@ class Vehicle < ModelBase
58
58
 
59
59
  # Defines the state machine for the state of the vehicled
60
60
  state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked}, :action => :save do
61
+ before_transition {|vehicle, transition| vehicle.last_transition_args = transition.args}
61
62
  before_transition :parked => any, :do => :put_on_seatbelt
62
63
  before_transition any => :stalled, :do => :increase_insurance_premium
63
64
  after_transition any => :parked, :do => lambda {|vehicle| vehicle.seatbelt_on = false}
@@ -340,6 +341,25 @@ class VehicleUnsavedTest < Test::Unit::TestCase
340
341
  ]], @vehicle.state_paths(:to => :first_gear)
341
342
  end
342
343
 
344
+ def test_should_allow_generic_event_to_fire
345
+ assert @vehicle.fire_state_event(:ignite)
346
+ assert_equal 'idling', @vehicle.state
347
+ end
348
+
349
+ def test_should_pass_arguments_through_to_generic_event_runner
350
+ @vehicle.fire_state_event(:ignite, 1, 2, 3)
351
+ assert_equal [1, 2, 3], @vehicle.last_transition_args
352
+ end
353
+
354
+ def test_should_allow_skipping_action_through_generic_event_runner
355
+ @vehicle.fire_state_event(:ignite, false)
356
+ assert_equal false, @vehicle.saved
357
+ end
358
+
359
+ def test_should_raise_error_with_invalid_event_through_generic_event_runer
360
+ assert_raise(IndexError) { @vehicle.fire_state_event(:invalid) }
361
+ end
362
+
343
363
  def test_should_allow_ignite
344
364
  assert @vehicle.ignite
345
365
  assert_equal 'idling', @vehicle.state
@@ -474,6 +474,11 @@ class EventWithTransitionsTest < Test::Unit::TestCase
474
474
  assert_equal [:parked, :idling, :first_gear, :stalled], @event.known_states
475
475
  end
476
476
 
477
+ def test_should_clear_known_states_on_reset
478
+ @event.reset
479
+ assert_equal [], @event.known_states
480
+ end
481
+
477
482
  def test_should_use_pretty_inspect
478
483
  assert_match "#<StateMachine::Event name=:ignite transitions=[:parked => :idling, :first_gear => :idling]>", @event.inspect
479
484
  end
@@ -690,6 +695,11 @@ class EventWithMatchingEnabledTransitionsTest < Test::Unit::TestCase
690
695
  assert_equal [], @object.errors
691
696
  end
692
697
 
698
+ def test_should_not_be_able_to_fire_on_reset
699
+ @event.reset
700
+ assert !@event.can_fire?(@object)
701
+ end
702
+
693
703
  def teardown
694
704
  StateMachine::Integrations.send(:remove_const, 'Custom')
695
705
  end
@@ -17,7 +17,7 @@ module ActiveModelTest
17
17
  def self.model_attribute(name)
18
18
  define_method(name) { instance_variable_get("@#{name}") }
19
19
  define_method("#{name}=") do |value|
20
- send("#{name}_will_change!") if self.class <= ActiveModel::Dirty && !send("#{name}_changed?")
20
+ send("#{name}_will_change!") if self.class <= ActiveModel::Dirty && value != instance_variable_get("@#{name}")
21
21
  instance_variable_set("@#{name}", value)
22
22
  end
23
23
  end
@@ -79,10 +79,6 @@ module ActiveModelTest
79
79
  assert StateMachine::Integrations::ActiveModel.available?
80
80
  end
81
81
 
82
- def test_should_match_if_class_includes_dirty_feature
83
- assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Dirty })
84
- end
85
-
86
82
  def test_should_match_if_class_includes_observing_feature
87
83
  assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Observing })
88
84
  end
@@ -350,12 +346,12 @@ module ActiveModelTest
350
346
  @transition.perform
351
347
  end
352
348
 
353
- def test_should_include_state_in_changed_attributes
354
- assert_equal %w(state), @record.changed
349
+ def test_should_not_include_state_in_changed_attributes
350
+ assert_equal [], @record.changed
355
351
  end
356
352
 
357
- def test_should_track_attribute_changes
358
- assert_equal %w(parked parked), @record.changes['state']
353
+ def test_should_not_track_attribute_changes
354
+ assert_equal nil, @record.changes['state']
359
355
  end
360
356
  end
361
357
 
@@ -408,12 +404,12 @@ module ActiveModelTest
408
404
  @transition.perform
409
405
  end
410
406
 
411
- def test_should_include_state_in_changed_attributes
412
- assert_equal %w(status), @record.changed
407
+ def test_should_not_include_state_in_changed_attributes
408
+ assert_equal [], @record.changed
413
409
  end
414
410
 
415
- def test_should_track_attribute_changes
416
- assert_equal %w(parked parked), @record.changes['status']
411
+ def test_should_not_track_attribute_changes
412
+ assert_equal nil, @record.changes['status']
417
413
  end
418
414
  end
419
415
 
@@ -430,24 +426,12 @@ module ActiveModelTest
430
426
  @record.state_event = 'ignite'
431
427
  end
432
428
 
433
- def test_should_include_state_in_changed_attributes
434
- assert_equal %w(state), @record.changed
435
- end
436
-
437
- def test_should_track_attribute_change
438
- assert_equal %w(parked parked), @record.changes['state']
439
- end
440
-
441
- def test_should_not_reset_changes_on_multiple_changes
442
- @record.state_event = 'ignite'
443
- assert_equal %w(parked parked), @record.changes['state']
429
+ def test_should_not_include_state_in_changed_attributes
430
+ assert_equal [], @record.changed
444
431
  end
445
432
 
446
- def test_should_not_include_state_in_changed_attributes_if_nil
447
- @record = @model.create
448
- @record.state_event = nil
449
-
450
- assert_equal [], @record.changed
433
+ def test_should_not_track_attribute_change
434
+ assert_equal nil, @record.changes['state']
451
435
  end
452
436
  end
453
437
 
@@ -694,6 +678,24 @@ module ActiveModelTest
694
678
  assert @record.valid?
695
679
  end
696
680
  end
681
+
682
+ class MachineErrorsTest < BaseTestCase
683
+ def setup
684
+ @model = new_model { include ActiveModel::Validations }
685
+ @machine = StateMachine::Machine.new(@model)
686
+ @record = @model.new
687
+ end
688
+
689
+ def test_should_be_able_to_describe_current_errors
690
+ @record.errors.add(:id, 'cannot be blank')
691
+ @record.errors.add(:state, 'is invalid')
692
+ assert_equal ['Id cannot be blank', 'State is invalid'], @machine.errors_for(@record).split(', ').sort
693
+ end
694
+
695
+ def test_should_describe_as_halted_with_no_errors
696
+ assert_equal 'Transition halted', @machine.errors_for(@record)
697
+ end
698
+ end
697
699
 
698
700
  class MachineWithStateDrivenValidationsTest < BaseTestCase
699
701
  def setup
@@ -189,6 +189,15 @@ module ActiveRecordTest
189
189
  assert_equal [record], block_args
190
190
  end
191
191
 
192
+ def test_should_set_attributes_prior_to_initialize_block
193
+ state = nil
194
+ record = @model.new do |record|
195
+ state = record.state
196
+ end
197
+
198
+ assert_equal 'parked', state
199
+ end
200
+
192
201
  def test_should_set_attributes_prior_to_after_initialize_hook
193
202
  state = nil
194
203
  @model.class_eval {define_method(:after_initialize) {}} if ::ActiveRecord::VERSION::MAJOR <= 2
@@ -264,6 +273,15 @@ module ActiveRecordTest
264
273
  assert_equal [record], block_args
265
274
  end
266
275
 
276
+ def test_should_set_attributes_prior_to_initialize_block
277
+ state = nil
278
+ record = @model.new do |record|
279
+ state = record.state
280
+ end
281
+
282
+ assert_equal 'parked', state
283
+ end
284
+
267
285
  def test_should_set_attributes_prior_to_after_initialize_hook
268
286
  state = nil
269
287
  @model.class_eval {define_method(:after_initialize) {}} if ::ActiveRecord::VERSION::MAJOR <= 2
@@ -605,8 +623,14 @@ module ActiveRecordTest
605
623
  @transition.perform
606
624
  end
607
625
 
608
- def test_should_update_record
609
- assert_not_equal @timestamp, @record.updated_at
626
+ if ActiveRecord.const_defined?(:Dirty) || ActiveRecord::AttributeMethods.const_defined?(:Dirty)
627
+ def test_should_not_update_record
628
+ assert_equal @timestamp, @record.updated_at
629
+ end
630
+ else
631
+ def test_should_update_record
632
+ assert_not_equal @timestamp, @record.updated_at
633
+ end
610
634
  end
611
635
  end
612
636
 
@@ -657,12 +681,12 @@ module ActiveRecordTest
657
681
  @transition.perform(false)
658
682
  end
659
683
 
660
- def test_should_include_state_in_changed_attributes
661
- assert_equal %w(state), @record.changed
684
+ def test_should_not_include_state_in_changed_attributes
685
+ assert_equal [], @record.changed
662
686
  end
663
687
 
664
- def test_should_track_attribute_changes
665
- assert_equal %w(parked parked), @record.changes['state']
688
+ def test_should_not_track_attribute_changes
689
+ assert_equal nil, @record.changes['state']
666
690
  end
667
691
  end
668
692
 
@@ -711,12 +735,12 @@ module ActiveRecordTest
711
735
  @transition.perform(false)
712
736
  end
713
737
 
714
- def test_should_include_state_in_changed_attributes
715
- assert_equal %w(status), @record.changed
738
+ def test_should_not_include_state_in_changed_attributes
739
+ assert_equal [], @record.changed
716
740
  end
717
741
 
718
- def test_should_track_attribute_changes
719
- assert_equal %w(parked parked), @record.changes['status']
742
+ def test_should_not_track_attribute_changes
743
+ assert_equal nil, @record.changes['status']
720
744
  end
721
745
  end
722
746
 
@@ -730,24 +754,12 @@ module ActiveRecordTest
730
754
  @record.state_event = 'ignite'
731
755
  end
732
756
 
733
- def test_should_include_state_in_changed_attributes
734
- assert_equal %w(state), @record.changed
735
- end
736
-
737
- def test_should_track_attribute_change
738
- assert_equal %w(parked parked), @record.changes['state']
739
- end
740
-
741
- def test_should_not_reset_changes_on_multiple_changes
742
- @record.state_event = 'ignite'
743
- assert_equal %w(parked parked), @record.changes['state']
757
+ def test_should_not_include_state_in_changed_attributes
758
+ assert_equal [], @record.changed
744
759
  end
745
760
 
746
- def test_should_not_include_state_in_changed_attributes_if_nil
747
- @record = @model.create
748
- @record.state_event = nil
749
-
750
- assert_equal [], @record.changed
761
+ def test_should_not_track_attribute_change
762
+ assert_equal nil, @record.changes['state']
751
763
  end
752
764
  end
753
765
  else
@@ -1111,6 +1123,24 @@ module ActiveRecordTest
1111
1123
  assert @record.valid?
1112
1124
  end
1113
1125
  end
1126
+
1127
+ class MachineErrorsTest < BaseTestCase
1128
+ def setup
1129
+ @model = new_model
1130
+ @machine = StateMachine::Machine.new(@model)
1131
+ @record = @model.new
1132
+ end
1133
+
1134
+ def test_should_be_able_to_describe_current_errors
1135
+ @record.errors.add(:id, 'cannot be blank')
1136
+ @record.errors.add(:state, 'is invalid')
1137
+ assert_equal ['Id cannot be blank', 'State is invalid'], @machine.errors_for(@record).split(', ').sort
1138
+ end
1139
+
1140
+ def test_should_describe_as_halted_with_no_errors
1141
+ assert_equal 'Transition halted', @machine.errors_for(@record)
1142
+ end
1143
+ end
1114
1144
 
1115
1145
  class MachineWithStateDrivenValidationsTest < BaseTestCase
1116
1146
  def setup