state_machine 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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