state_machine 0.7.6 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,10 +4,12 @@ module StateMachine
4
4
  # Initializes the state of each machine in the given object. Initial
5
5
  # values are only set if the machine's attribute doesn't already exist
6
6
  # (which must mean the defaults are being skipped)
7
- def initialize_states(object)
7
+ def initialize_states(object, options = {})
8
8
  each_value do |machine|
9
- value = machine.read(object, :state)
10
- machine.write(object, :state, machine.initial_state(object).value) if value.nil? || value.respond_to?(:empty?) && value.empty?
9
+ if !options.include?(:dynamic) || machine.dynamic_initial_state? == options[:dynamic]
10
+ value = machine.read(object, :state)
11
+ machine.write(object, :state, machine.initial_state(object).value) if value.nil? || value.respond_to?(:empty?) && value.empty?
12
+ end
11
13
  end
12
14
  end
13
15
 
@@ -45,7 +47,7 @@ module StateMachine
45
47
  end
46
48
 
47
49
  # Runs one or more event attributes in parallel during the invocation of
48
- # an action on the given object. After transition callbacks can be
50
+ # an action on the given object. after_transition callbacks can be
49
51
  # optionally disabled if the events are being only partially fired (for
50
52
  # example, when validating records in ORM integrations).
51
53
  #
@@ -56,7 +58,7 @@ module StateMachine
56
58
  #
57
59
  # class Vehicle
58
60
  # include DataMapper::Resource
59
- # property :id, Integer, :serial => true
61
+ # property :id, Serial
60
62
  #
61
63
  # state_machine :initial => :parked do
62
64
  # event :ignite do
@@ -191,7 +191,7 @@ module StateMachine
191
191
  context_method.bind(object).call(*args, &block)
192
192
  else
193
193
  # Raise exception as if the method never existed on the original object
194
- raise NoMethodError, "undefined method '#{method}' for #{object} in state #{machine.states.match(object).name.inspect}"
194
+ raise NoMethodError, "undefined method '#{method}' for #{object} with #{name || 'nil'} #{machine.name}"
195
195
  end
196
196
  end
197
197
 
@@ -16,8 +16,8 @@ module StateMachine
16
16
  # 1. Before callbacks
17
17
  # 2. Persist state
18
18
  # 3. Invoke action
19
- # 4. After callbacks if configured
20
- # 5. Rollback if action is unsuccessful
19
+ # 4. After callbacks (if configured)
20
+ # 5. Rollback (if action is unsuccessful)
21
21
  #
22
22
  # Configuration options:
23
23
  # * <tt>:action</tt> - Whether to run the action configured for each transition
@@ -46,7 +46,7 @@ module StateMachine
46
46
  # Block was given: use the result for each transition
47
47
  result = yield
48
48
  transitions.each {|transition| results[transition.action] = result}
49
- result
49
+ !!result
50
50
  elsif options[:action] == false
51
51
  # Skip the action
52
52
  true
@@ -64,8 +64,9 @@ module StateMachine
64
64
  raise
65
65
  end
66
66
 
67
- # Always run after callbacks regardless of whether the actions failed
68
- transitions.each {|transition| transition.after(results[transition.action])} unless options[:after] == false
67
+ # Run after callbacks even when the actions failed. The :after option
68
+ # is ignored if the transitions were unsuccessful.
69
+ transitions.each {|transition| transition.after(results[transition.action], success)} unless options[:after] == false && success
69
70
 
70
71
  # Rollback the transitions if the transaction was unsuccessful
71
72
  transitions.each {|transition| transition.rollback} unless success
@@ -124,7 +125,7 @@ module StateMachine
124
125
  attr_reader :result
125
126
 
126
127
  # Creates a new, specific transition
127
- def initialize(object, machine, event, from_name, to_name) #:nodoc:
128
+ def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
128
129
  @object = object
129
130
  @machine = machine
130
131
  @args = []
@@ -136,7 +137,7 @@ module StateMachine
136
137
 
137
138
  # From state information
138
139
  from_state = machine.states.fetch(from_name)
139
- @from = machine.read(object, :state)
140
+ @from = read_state ? machine.read(object, :state) : from_state.value
140
141
  @from_name = from_state.name
141
142
  @qualified_from_name = from_state.qualified_name
142
143
 
@@ -218,6 +219,9 @@ module StateMachine
218
219
  # callbacks that are configured to match the event, from state, and to
219
220
  # state will be invoked.
220
221
  #
222
+ # Once the callbacks are run, they cannot be run again until this transition
223
+ # is reset.
224
+ #
221
225
  # == Example
222
226
  #
223
227
  # class Vehicle
@@ -233,7 +237,11 @@ module StateMachine
233
237
  result = false
234
238
 
235
239
  catch(:halt) do
236
- callback(:before)
240
+ unless @before_run
241
+ callback(:before)
242
+ @before_run = true
243
+ end
244
+
237
245
  result = true
238
246
  end
239
247
 
@@ -241,7 +249,8 @@ module StateMachine
241
249
  end
242
250
 
243
251
  # Transitions the current value of the state to that specified by the
244
- # transition.
252
+ # transition. Once the state is persisted, it cannot be persisted again
253
+ # until this transition is reset.
245
254
  #
246
255
  # == Example
247
256
  #
@@ -259,16 +268,22 @@ module StateMachine
259
268
  #
260
269
  # vehicle.state # => 'idling'
261
270
  def persist
262
- machine.write(object, :state, to)
271
+ unless @persisted
272
+ machine.write(object, :state, to)
273
+ @persisted = true
274
+ end
263
275
  end
264
276
 
265
277
  # Runs the machine's +after+ callbacks for this transition. Only
266
278
  # callbacks that are configured to match the event, from state, and to
267
279
  # state will be invoked.
268
280
  #
269
- # The result is used to indicate whether the associated machine action
281
+ # The result can be used to indicate whether the associated machine action
270
282
  # was executed successfully.
271
283
  #
284
+ # Once the callbacks are run, they cannot be run again until this transition
285
+ # is reset.
286
+ #
272
287
  # == Halting
273
288
  #
274
289
  # If any callback throws a <tt>:halt</tt> exception, it will be caught
@@ -291,11 +306,14 @@ module StateMachine
291
306
  # vehicle = Vehicle.new
292
307
  # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
293
308
  # transition.after(true)
294
- def after(result = nil)
309
+ def after(result = nil, success = true)
295
310
  @result = result
296
311
 
297
312
  catch(:halt) do
298
- callback(:after)
313
+ unless @after_run
314
+ callback(:after, :success => success)
315
+ @after_run = true
316
+ end
299
317
  end
300
318
 
301
319
  true
@@ -326,9 +344,16 @@ module StateMachine
326
344
  # transition.rollback
327
345
  # vehicle.state # => "parked"
328
346
  def rollback
347
+ reset
329
348
  machine.write(object, :state, from)
330
349
  end
331
350
 
351
+ # Resets any tracking of which callbacks have already been run and whether
352
+ # the state has already been persisted
353
+ def reset
354
+ @before_run = @persisted = @after_run = false
355
+ end
356
+
332
357
  # Generates a nicely formatted description of this transitions's contents.
333
358
  #
334
359
  # For example,
@@ -358,7 +383,9 @@ module StateMachine
358
383
  #
359
384
  # Additional callback parameters can be specified. By default, this
360
385
  # transition is also passed into callbacks.
361
- def callback(type)
386
+ def callback(type, context = {})
387
+ context = self.context.merge(context)
388
+
362
389
  machine.callbacks[type].each do |callback|
363
390
  callback.call(object, context, self)
364
391
  end
@@ -1,13 +1,13 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
2
 
3
- class AssertionsTest < Test::Unit::TestCase
3
+ class AssertionsBaseTest < Test::Unit::TestCase
4
4
  include StateMachine::Assertions
5
5
 
6
6
  def default_test
7
7
  end
8
8
  end
9
9
 
10
- class AssertValidKeysTest < AssertionsTest
10
+ class AssertValidKeysTest < AssertionsBaseTest
11
11
  def test_should_not_raise_exception_if_key_is_valid
12
12
  assert_nothing_raised { assert_valid_keys({:name => 'foo', :value => 'bar'}, :name, :value, :force) }
13
13
  end
@@ -18,7 +18,7 @@ class AssertValidKeysTest < AssertionsTest
18
18
  end
19
19
  end
20
20
 
21
- class AssertExclusiveKeysTest < AssertionsTest
21
+ class AssertExclusiveKeysTest < AssertionsBaseTest
22
22
  def test_should_not_raise_exception_if_no_keys_found
23
23
  assert_nothing_raised { assert_exclusive_keys({:on => :park}, :only, :except) }
24
24
  end
@@ -1,8 +1,13 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
2
 
3
- class EvalHelpersTest < Test::Unit::TestCase
3
+ class EvalHelpersBaseTest < Test::Unit::TestCase
4
4
  include StateMachine::EvalHelpers
5
5
 
6
+ def default_test
7
+ end
8
+ end
9
+
10
+ class EvalHelpersTest < EvalHelpersBaseTest
6
11
  def setup
7
12
  @object = Object.new
8
13
  end
@@ -13,9 +18,7 @@ class EvalHelpersTest < Test::Unit::TestCase
13
18
  end
14
19
  end
15
20
 
16
- class EvalHelpersSymbolTest < Test::Unit::TestCase
17
- include StateMachine::EvalHelpers
18
-
21
+ class EvalHelpersSymbolTest < EvalHelpersBaseTest
19
22
  def setup
20
23
  class << (@object = Object.new)
21
24
  def callback
@@ -29,9 +32,7 @@ class EvalHelpersSymbolTest < Test::Unit::TestCase
29
32
  end
30
33
  end
31
34
 
32
- class EvalHelpersSymbolWithArgumentsTest < Test::Unit::TestCase
33
- include StateMachine::EvalHelpers
34
-
35
+ class EvalHelpersSymbolWithArgumentsTest < EvalHelpersBaseTest
35
36
  def setup
36
37
  class << (@object = Object.new)
37
38
  def callback(*args)
@@ -45,9 +46,7 @@ class EvalHelpersSymbolWithArgumentsTest < Test::Unit::TestCase
45
46
  end
46
47
  end
47
48
 
48
- class EvalHelpersSymbolTaintedMethodTest < Test::Unit::TestCase
49
- include StateMachine::EvalHelpers
50
-
49
+ class EvalHelpersSymbolTaintedMethodTest < EvalHelpersBaseTest
51
50
  def setup
52
51
  class << (@object = Object.new)
53
52
  def callback
@@ -63,9 +62,7 @@ class EvalHelpersSymbolTaintedMethodTest < Test::Unit::TestCase
63
62
  end
64
63
  end
65
64
 
66
- class EvalHelpersStringTest < Test::Unit::TestCase
67
- include StateMachine::EvalHelpers
68
-
65
+ class EvalHelpersStringTest < EvalHelpersBaseTest
69
66
  def setup
70
67
  @object = Object.new
71
68
  end
@@ -84,9 +81,7 @@ class EvalHelpersStringTest < Test::Unit::TestCase
84
81
  end
85
82
  end
86
83
 
87
- class EvalHelpersProcTest < Test::Unit::TestCase
88
- include StateMachine::EvalHelpers
89
-
84
+ class EvalHelpersProcTest < EvalHelpersBaseTest
90
85
  def setup
91
86
  @object = Object.new
92
87
  @proc = lambda {|obj| obj}
@@ -97,9 +92,7 @@ class EvalHelpersProcTest < Test::Unit::TestCase
97
92
  end
98
93
  end
99
94
 
100
- class EvalHelpersProcWithoutArgumentsTest < Test::Unit::TestCase
101
- include StateMachine::EvalHelpers
102
-
95
+ class EvalHelpersProcWithoutArgumentsTest < EvalHelpersBaseTest
103
96
  def setup
104
97
  @object = Object.new
105
98
  @proc = lambda {|*args| args}
@@ -115,9 +108,7 @@ class EvalHelpersProcWithoutArgumentsTest < Test::Unit::TestCase
115
108
  end
116
109
  end
117
110
 
118
- class EvalHelpersProcWithArgumentsTest < Test::Unit::TestCase
119
- include StateMachine::EvalHelpers
120
-
111
+ class EvalHelpersProcWithArgumentsTest < EvalHelpersBaseTest
121
112
  def setup
122
113
  @object = Object.new
123
114
  @proc = lambda {|*args| args}
@@ -81,6 +81,12 @@ class EventCollectionWithEventsWithTransitionsTest < Test::Unit::TestCase
81
81
  object.state = 'idling'
82
82
  assert_equal [], @events.transitions_for(object)
83
83
  end
84
+
85
+ def test_should_filter_valid_transitions_for_an_object_if_requirements_specified
86
+ object = @klass.new
87
+ assert_equal [{:object => object, :attribute => :state, :event => :ignite, :from => 'stalled', :to => 'idling'}], @events.transitions_for(object, :from => :stalled).map {|transition| transition.attributes}
88
+ assert_equal [], @events.transitions_for(object, :from => :idling).map {|transition| transition.attributes}
89
+ end
84
90
  end
85
91
 
86
92
  class EventCollectionWithMultipleEventsTest < Test::Unit::TestCase
@@ -166,6 +172,13 @@ class EventCollectionAttributeWithMachineActionTest < Test::Unit::TestCase
166
172
 
167
173
  assert_instance_of StateMachine::Transition, @events.attribute_transition_for(@object)
168
174
  end
175
+
176
+ def test_should_have_valid_transition_if_already_defined_in_transition_cache
177
+ @object.state_event = nil
178
+ @object.send(:state_event_transition=, transition = @ignite.transition_for(@object))
179
+
180
+ assert_equal transition, @events.attribute_transition_for(@object)
181
+ end
169
182
  end
170
183
 
171
184
  class EventCollectionAttributeWithNamespacedMachineTest < Test::Unit::TestCase
@@ -249,6 +262,17 @@ class EventCollectionWithValidationsTest < Test::Unit::TestCase
249
262
  assert_equal ['cannot transition when idling'], @object.errors
250
263
  end
251
264
 
265
+ def test_should_invalidate_with_friendly_name_if_invalid_event_specified
266
+ # Add a valid nil state
267
+ @machine.state nil
268
+
269
+ @object.state = nil
270
+ @object.state_event = 'ignite'
271
+ @events.attribute_transition_for(@object, true)
272
+
273
+ assert_equal ['cannot transition when nil'], @object.errors
274
+ end
275
+
252
276
  def test_should_not_invalidate_event_can_be_fired
253
277
  @ignite.transition :parked => :idling
254
278
  @object.state_event = 'ignite'
@@ -201,6 +201,12 @@ class EventTransitionsTest < Test::Unit::TestCase
201
201
  assert_equal 'Invalid key(s): on', exception.message
202
202
  end
203
203
 
204
+ def test_should_automatically_set_on_option
205
+ guard = @event.transition(:to => :idling)
206
+ assert_instance_of StateMachine::WhitelistMatcher, guard.event_requirement
207
+ assert_equal [:ignite], guard.event_requirement.values
208
+ end
209
+
204
210
  def test_should_not_allow_except_to_option
205
211
  exception = assert_raise(ArgumentError) {@event.transition(:except_to => :parked)}
206
212
  assert_equal 'Invalid key(s): except_to', exception.message
@@ -542,6 +548,7 @@ class EventWithMultipleTransitionsTest < Test::Unit::TestCase
542
548
  @event = StateMachine::Event.new(@machine, :ignite)
543
549
  @event.transition(:idling => :idling)
544
550
  @event.transition(:parked => :idling) # This one should get used
551
+ @event.transition(:parked => :parked)
545
552
 
546
553
  @object = @klass.new
547
554
  @object.state = 'parked'
@@ -559,6 +566,32 @@ class EventWithMultipleTransitionsTest < Test::Unit::TestCase
559
566
  assert_equal :ignite, transition.event
560
567
  end
561
568
 
569
+ def test_should_allow_specific_transition_selection_using_from
570
+ transition = @event.transition_for(@object, :from => :idling)
571
+
572
+ assert_not_nil transition
573
+ assert_equal 'idling', transition.from
574
+ assert_equal 'idling', transition.to
575
+ assert_equal :ignite, transition.event
576
+ end
577
+
578
+ def test_should_allow_specific_transition_selection_using_to
579
+ transition = @event.transition_for(@object, :from => :parked, :to => :parked)
580
+
581
+ assert_not_nil transition
582
+ assert_equal 'parked', transition.from
583
+ assert_equal 'parked', transition.to
584
+ assert_equal :ignite, transition.event
585
+ end
586
+
587
+ def test_should_allow_specific_transition_selection_using_on
588
+ transition = @event.transition_for(@object, :on => :park)
589
+ assert_nil transition
590
+
591
+ transition = @event.transition_for(@object, :on => :ignite)
592
+ assert_not_nil transition
593
+ end
594
+
562
595
  def test_should_fire
563
596
  assert @event.fire(@object)
564
597
  end
@@ -569,6 +602,67 @@ class EventWithMultipleTransitionsTest < Test::Unit::TestCase
569
602
  end
570
603
  end
571
604
 
605
+ class EventWithMachineActionTest < Test::Unit::TestCase
606
+ def setup
607
+ @klass = Class.new do
608
+ attr_reader :saved
609
+
610
+ def save
611
+ @saved = true
612
+ end
613
+ end
614
+
615
+ @machine = StateMachine::Machine.new(@klass, :action => :save)
616
+ @machine.state :parked, :idling
617
+
618
+ @machine.events << @event = StateMachine::Event.new(@machine, :ignite)
619
+ @event.transition(:parked => :idling)
620
+
621
+ @object = @klass.new
622
+ @object.state = 'parked'
623
+ end
624
+
625
+ def test_should_run_action_on_fire
626
+ @event.fire(@object)
627
+ assert @object.saved
628
+ end
629
+
630
+ def test_should_not_run_action_if_configured_to_skip
631
+ @event.fire(@object, false)
632
+ assert !@object.saved
633
+ end
634
+ end
635
+
636
+ class EventWithInvalidCurrentStateTest < Test::Unit::TestCase
637
+ def setup
638
+ @klass = Class.new
639
+ @machine = StateMachine::Machine.new(@klass)
640
+ @machine.state :parked, :idling
641
+ @machine.event :ignite
642
+
643
+ @event = StateMachine::Event.new(@machine, :ignite)
644
+ @event.transition(:parked => :idling)
645
+
646
+ @object = @klass.new
647
+ @object.state = 'invalid'
648
+ end
649
+
650
+ def test_should_raise_exception_when_checking_availability
651
+ exception = assert_raise(ArgumentError) { @event.can_fire?(@object) }
652
+ assert_equal '"invalid" is not a known state value', exception.message
653
+ end
654
+
655
+ def test_should_raise_exception_when_finding_transition
656
+ exception = assert_raise(ArgumentError) { @event.transition_for(@object) }
657
+ assert_equal '"invalid" is not a known state value', exception.message
658
+ end
659
+
660
+ def test_should_raise_exception_when_firing
661
+ exception = assert_raise(ArgumentError) { @event.fire(@object) }
662
+ assert_equal '"invalid" is not a known state value', exception.message
663
+ end
664
+ end
665
+
572
666
  begin
573
667
  # Load library
574
668
  require 'rubygems'