state_machine 0.8.1 → 0.9.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 (42) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +162 -23
  4. data/Rakefile +3 -18
  5. data/lib/state_machine.rb +3 -4
  6. data/lib/state_machine/callback.rb +65 -13
  7. data/lib/state_machine/eval_helpers.rb +20 -4
  8. data/lib/state_machine/initializers.rb +4 -0
  9. data/lib/state_machine/initializers/merb.rb +1 -0
  10. data/lib/state_machine/initializers/rails.rb +7 -0
  11. data/lib/state_machine/integrations.rb +21 -6
  12. data/lib/state_machine/integrations/active_model.rb +414 -0
  13. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  14. data/lib/state_machine/integrations/{active_record → active_model}/observer.rb +7 -7
  15. data/lib/state_machine/integrations/active_record.rb +65 -129
  16. data/lib/state_machine/integrations/active_record/locale.rb +4 -11
  17. data/lib/state_machine/integrations/data_mapper.rb +24 -6
  18. data/lib/state_machine/integrations/data_mapper/observer.rb +36 -0
  19. data/lib/state_machine/integrations/mongo_mapper.rb +295 -0
  20. data/lib/state_machine/integrations/sequel.rb +33 -7
  21. data/lib/state_machine/machine.rb +121 -23
  22. data/lib/state_machine/machine_collection.rb +12 -103
  23. data/lib/state_machine/transition.rb +125 -164
  24. data/lib/state_machine/transition_collection.rb +244 -0
  25. data/lib/tasks/state_machine.rb +12 -15
  26. data/test/functional/state_machine_test.rb +11 -1
  27. data/test/unit/callback_test.rb +305 -32
  28. data/test/unit/eval_helpers_test.rb +103 -1
  29. data/test/unit/event_test.rb +2 -1
  30. data/test/unit/guard_test.rb +2 -1
  31. data/test/unit/integrations/active_model_test.rb +909 -0
  32. data/test/unit/integrations/active_record_test.rb +1542 -1292
  33. data/test/unit/integrations/data_mapper_test.rb +1369 -1041
  34. data/test/unit/integrations/mongo_mapper_test.rb +1349 -0
  35. data/test/unit/integrations/sequel_test.rb +1214 -985
  36. data/test/unit/integrations_test.rb +8 -0
  37. data/test/unit/machine_collection_test.rb +140 -513
  38. data/test/unit/machine_test.rb +212 -10
  39. data/test/unit/state_test.rb +2 -1
  40. data/test/unit/transition_collection_test.rb +2098 -0
  41. data/test/unit/transition_test.rb +704 -552
  42. metadata +16 -3
@@ -28,7 +28,7 @@ class EvalHelpersSymbolTest < EvalHelpersBaseTest
28
28
  end
29
29
 
30
30
  def test_should_call_method_on_object_with_no_arguments
31
- assert evaluate_method(@object, :callback, 1, 2, 3)
31
+ assert_equal true, evaluate_method(@object, :callback, 1, 2, 3)
32
32
  end
33
33
  end
34
34
 
@@ -46,6 +46,34 @@ class EvalHelpersSymbolWithArgumentsTest < EvalHelpersBaseTest
46
46
  end
47
47
  end
48
48
 
49
+ class EvalHelpersSymbolWithBlockTest < EvalHelpersBaseTest
50
+ def setup
51
+ class << (@object = Object.new)
52
+ def callback
53
+ yield
54
+ end
55
+ end
56
+ end
57
+
58
+ def test_should_call_method_on_object_with_block
59
+ assert_equal true, evaluate_method(@object, :callback) { true }
60
+ end
61
+ end
62
+
63
+ class EvalHelpersSymbolWithArgumentsAndBlockTest < EvalHelpersBaseTest
64
+ def setup
65
+ class << (@object = Object.new)
66
+ def callback(*args)
67
+ args << yield
68
+ end
69
+ end
70
+ end
71
+
72
+ def test_should_call_method_on_object_with_all_arguments_and_block
73
+ assert_equal [1, 2, 3, true], evaluate_method(@object, :callback, 1, 2, 3) { true }
74
+ end
75
+ end
76
+
49
77
  class EvalHelpersSymbolTaintedMethodTest < EvalHelpersBaseTest
50
78
  def setup
51
79
  class << (@object = Object.new)
@@ -81,6 +109,16 @@ class EvalHelpersStringTest < EvalHelpersBaseTest
81
109
  end
82
110
  end
83
111
 
112
+ class EvalHelpersStringWithBlockTest < EvalHelpersBaseTest
113
+ def setup
114
+ @object = Object.new
115
+ end
116
+
117
+ def test_should_call_method_on_object_with_block
118
+ assert_equal 1, evaluate_method(@object, 'yield') { 1 }
119
+ end
120
+ end
121
+
84
122
  class EvalHelpersProcTest < EvalHelpersBaseTest
85
123
  def setup
86
124
  @object = Object.new
@@ -118,3 +156,67 @@ class EvalHelpersProcWithArgumentsTest < EvalHelpersBaseTest
118
156
  assert_equal [@object, 1, 2, 3], evaluate_method(@object, @proc, 1, 2, 3)
119
157
  end
120
158
  end
159
+
160
+ class EvalHelpersProcWithBlockTest < EvalHelpersBaseTest
161
+ def setup
162
+ @object = Object.new
163
+ @proc = lambda {|obj, block| block.call}
164
+ end
165
+
166
+ def test_should_call_method_on_object_with_block
167
+ assert_equal true, evaluate_method(@object, @proc, 1, 2, 3) { true }
168
+ end
169
+ end
170
+
171
+ class EvalHelpersProcWithBlockWithoutArgumentsTest < EvalHelpersBaseTest
172
+ def setup
173
+ @object = Object.new
174
+ @proc = lambda {|*args| args}
175
+ class << @proc
176
+ def arity
177
+ 0
178
+ end
179
+ end
180
+ end
181
+
182
+ def test_should_call_proc_without_arguments
183
+ block = lambda { true }
184
+ assert_equal [], evaluate_method(@object, @proc, 1, 2, 3, &block)
185
+ end
186
+ end
187
+
188
+ class EvalHelpersProcWithBlockWithoutObjectTest < EvalHelpersBaseTest
189
+ def setup
190
+ @object = Object.new
191
+ @proc = lambda {|block| [block]}
192
+ end
193
+
194
+ def test_should_call_proc_with_block_only
195
+ block = lambda { true }
196
+ assert_equal [block], evaluate_method(@object, @proc, 1, 2, 3, &block)
197
+ end
198
+ end
199
+
200
+ class EvalHelpersProcBlockAndImplicitArgumentsTest < EvalHelpersBaseTest
201
+ def setup
202
+ @object = Object.new
203
+ @proc = lambda {|*args| args}
204
+ end
205
+
206
+ def test_should_call_method_on_object_with_all_arguments_and_block
207
+ block = lambda { true }
208
+ assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block)
209
+ end
210
+ end
211
+
212
+ class EvalHelpersProcBlockAndExplicitArgumentsTest < EvalHelpersBaseTest
213
+ def setup
214
+ @object = Object.new
215
+ @proc = lambda {|object, arg1, arg2, arg3, block| [object, arg1, arg2, arg3, block]}
216
+ end
217
+
218
+ def test_should_call_method_on_object_with_all_arguments_and_block
219
+ block = lambda { true }
220
+ assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block)
221
+ end
222
+ end
@@ -710,6 +710,7 @@ end
710
710
  begin
711
711
  # Load library
712
712
  require 'rubygems'
713
+ gem 'ruby-graphviz', '>=0.9.0'
713
714
  require 'graphviz'
714
715
 
715
716
  class EventDrawingTest < Test::Unit::TestCase
@@ -739,5 +740,5 @@ begin
739
740
  end
740
741
  end
741
742
  rescue LoadError
742
- $stderr.puts 'Skipping GraphViz StateMachine::Event tests. `gem install ruby-graphviz` and try again.'
743
+ $stderr.puts 'Skipping GraphViz StateMachine::Event tests. `gem install ruby-graphviz` >= v0.9.0 and try again.'
743
744
  end
@@ -776,6 +776,7 @@ end
776
776
  begin
777
777
  # Load library
778
778
  require 'rubygems'
779
+ gem 'ruby-graphviz', '>=0.9.0'
779
780
  require 'graphviz'
780
781
 
781
782
  class GuardDrawingTest < Test::Unit::TestCase
@@ -904,5 +905,5 @@ begin
904
905
  end
905
906
  end
906
907
  rescue LoadError
907
- $stderr.puts 'Skipping GraphViz StateMachine::Guard tests. `gem install ruby-graphviz` and try again.'
908
+ $stderr.puts 'Skipping GraphViz StateMachine::Guard tests. `gem install ruby-graphviz` >= v0.9.0 and try again.'
908
909
  end
@@ -0,0 +1,909 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ # Load library
4
+ require 'rubygems'
5
+
6
+ gem 'activemodel', ENV['VERSION'] ? "=#{ENV['VERSION']}" : '>=3.0.0.beta'
7
+ require 'active_model'
8
+ require 'active_model/observing'
9
+ require 'active_support/all'
10
+
11
+ module ActiveModelTest
12
+ class BaseTestCase < Test::Unit::TestCase
13
+ def default_test
14
+ end
15
+
16
+ protected
17
+ # Creates a new ActiveRecord model (and the associated table)
18
+ def new_model(&block)
19
+ # Simple ActiveModel superclass
20
+ parent = Class.new do
21
+ def self.model_attribute(name)
22
+ define_method(name) { instance_variable_get("@#{name}") }
23
+ define_method("#{name}=") do |value|
24
+ send("#{name}_will_change!") if self.class <= ActiveModel::Dirty && !send("#{name}_changed?")
25
+ instance_variable_set("@#{name}", value)
26
+ end
27
+ end
28
+
29
+ def self.create
30
+ object = new
31
+ object.save
32
+ object
33
+ end
34
+
35
+ def initialize(attrs = {})
36
+ attrs.each {|attr, value| send("#{attr}=", value)}
37
+ @changed_attributes = {}
38
+ end
39
+
40
+ def attributes
41
+ @attributes ||= {}
42
+ end
43
+
44
+ def save
45
+ @changed_attributes = {}
46
+ true
47
+ end
48
+ end
49
+
50
+ model = Class.new(parent) do
51
+ def self.name
52
+ 'ActiveModelTest::Foo'
53
+ end
54
+
55
+ model_attribute :state
56
+ end
57
+ model.class_eval(&block) if block_given?
58
+ model
59
+ end
60
+
61
+ # Creates a new ActiveRecord observer
62
+ def new_observer(model, &block)
63
+ observer = Class.new(ActiveModel::Observer) do
64
+ attr_accessor :notifications
65
+
66
+ def initialize
67
+ super
68
+ @notifications = []
69
+ end
70
+ end
71
+ observer.observe(model)
72
+ observer.class_eval(&block) if block_given?
73
+ observer
74
+ end
75
+ end
76
+
77
+ class IntegrationTest < BaseTestCase
78
+ def test_should_match_if_class_includes_dirty_feature
79
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Dirty })
80
+ end
81
+
82
+ def test_should_match_if_class_includes_observing_feature
83
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Observing })
84
+ end
85
+
86
+ def test_should_match_if_class_includes_validations_feature
87
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Validations })
88
+ end
89
+
90
+ def test_should_not_match_if_class_does_not_include_active_model_features
91
+ assert !StateMachine::Integrations::ActiveModel.matches?(new_model)
92
+ end
93
+
94
+ def test_should_have_no_defaults
95
+ assert_equal e = {}, StateMachine::Integrations::ActiveModel.defaults
96
+ end
97
+ end
98
+
99
+ class MachineByDefaultTest < BaseTestCase
100
+ def setup
101
+ @model = new_model
102
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
103
+ end
104
+
105
+ def test_should_not_have_action
106
+ assert_nil @machine.action
107
+ end
108
+
109
+ def test_should_use_transactions
110
+ assert_equal true, @machine.use_transactions
111
+ end
112
+
113
+ def test_should_not_have_any_before_callbacks
114
+ assert_equal 0, @machine.callbacks[:before].size
115
+ end
116
+
117
+ def test_should_not_have_any_after_callbacks
118
+ assert_equal 0, @machine.callbacks[:after].size
119
+ end
120
+ end
121
+
122
+ class MachineWithStaticInitialStateTest < BaseTestCase
123
+ def setup
124
+ @model = new_model
125
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
126
+ end
127
+
128
+ def test_should_set_initial_state_on_created_object
129
+ record = @model.new
130
+ assert_equal 'parked', record.state
131
+ end
132
+ end
133
+
134
+ class MachineWithDynamicInitialStateTest < BaseTestCase
135
+ def setup
136
+ @model = new_model
137
+ @machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked}, :integration => :active_model)
138
+ @machine.state :parked
139
+ end
140
+
141
+ def test_should_set_initial_state_on_created_object
142
+ record = @model.new
143
+ assert_equal 'parked', record.state
144
+ end
145
+ end
146
+
147
+ class MachineWithModelStateAttributeTest < BaseTestCase
148
+ def setup
149
+ @model = new_model
150
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
151
+ @machine.other_states(:idling)
152
+
153
+ @record = @model.new
154
+ end
155
+
156
+ def test_should_have_an_attribute_predicate
157
+ assert @record.respond_to?(:state?)
158
+ end
159
+
160
+ def test_should_raise_exception_for_predicate_without_parameters
161
+ assert_raise(IndexError) { @record.state? }
162
+ end
163
+
164
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
165
+ assert !@record.state?(:idling)
166
+ end
167
+
168
+ def test_should_return_true_for_predicate_if_matches_current_value
169
+ assert @record.state?(:parked)
170
+ end
171
+
172
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
173
+ assert_raise(IndexError) { @record.state?(:invalid) }
174
+ end
175
+ end
176
+
177
+ class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
178
+ def setup
179
+ @model = new_model do
180
+ def initialize
181
+ end
182
+ end
183
+
184
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked, :integration => :active_model)
185
+ @machine.other_states(:idling)
186
+ @record = @model.new
187
+ end
188
+
189
+ def test_should_not_define_a_reader_attribute_for_the_attribute
190
+ assert !@record.respond_to?(:status)
191
+ end
192
+
193
+ def test_should_not_define_a_writer_attribute_for_the_attribute
194
+ assert !@record.respond_to?(:status=)
195
+ end
196
+
197
+ def test_should_define_an_attribute_predicate
198
+ assert @record.respond_to?(:status?)
199
+ end
200
+ end
201
+
202
+ class MachineWithInitializedStateTest < BaseTestCase
203
+ def setup
204
+ @model = new_model
205
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
206
+ @machine.state nil, :idling
207
+ end
208
+
209
+ def test_should_should_use_initialized_state_when_static
210
+ record = @model.new(:state => nil)
211
+ assert_nil record.state
212
+ end
213
+
214
+ def test_should_should_not_use_initialized_state_when_dynamic
215
+ @machine.initial_state = lambda {:parked}
216
+ record = @model.new(:state => nil)
217
+ assert_equal 'parked', record.state
218
+ end
219
+ end
220
+
221
+ class MachineWithDirtyAttributesTest < BaseTestCase
222
+ def setup
223
+ @model = new_model do
224
+ include ActiveModel::Dirty
225
+ define_attribute_methods [:state]
226
+ end
227
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
228
+ @machine.event :ignite
229
+ @machine.state :idling
230
+
231
+ @record = @model.create
232
+
233
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
234
+ @transition.perform
235
+ end
236
+
237
+ def test_should_include_state_in_changed_attributes
238
+ assert_equal %w(state), @record.changed
239
+ end
240
+
241
+ def test_should_track_attribute_change
242
+ assert_equal %w(parked idling), @record.changes['state']
243
+ end
244
+
245
+ def test_should_not_reset_changes_on_multiple_transitions
246
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
247
+ transition.perform
248
+
249
+ assert_equal %w(parked idling), @record.changes['state']
250
+ end
251
+ end
252
+
253
+ class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
254
+ def setup
255
+ @model = new_model do
256
+ include ActiveModel::Dirty
257
+ define_attribute_methods [:state]
258
+ end
259
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
260
+ @machine.event :park
261
+
262
+ @record = @model.create
263
+
264
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
265
+ @transition.perform
266
+ end
267
+
268
+ def test_should_include_state_in_changed_attributes
269
+ assert_equal %w(state), @record.changed
270
+ end
271
+
272
+ def test_should_track_attribute_changes
273
+ assert_equal %w(parked parked), @record.changes['state']
274
+ end
275
+ end
276
+
277
+ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
278
+ def setup
279
+ @model = new_model do
280
+ include ActiveModel::Dirty
281
+ model_attribute :status
282
+ define_attribute_methods [:status]
283
+ end
284
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
285
+ @machine.event :ignite
286
+ @machine.state :idling
287
+
288
+ @record = @model.create
289
+
290
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
291
+ @transition.perform
292
+ end
293
+
294
+ def test_should_include_state_in_changed_attributes
295
+ assert_equal %w(status), @record.changed
296
+ end
297
+
298
+ def test_should_track_attribute_change
299
+ assert_equal %w(parked idling), @record.changes['status']
300
+ end
301
+
302
+ def test_should_not_reset_changes_on_multiple_transitions
303
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
304
+ transition.perform
305
+
306
+ assert_equal %w(parked idling), @record.changes['status']
307
+ end
308
+ end
309
+
310
+ class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
311
+ def setup
312
+ @model = new_model do
313
+ include ActiveModel::Dirty
314
+ model_attribute :status
315
+ define_attribute_methods [:status]
316
+ end
317
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
318
+ @machine.event :park
319
+
320
+ @record = @model.create
321
+
322
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
323
+ @transition.perform
324
+ end
325
+
326
+ def test_should_include_state_in_changed_attributes
327
+ assert_equal %w(status), @record.changed
328
+ end
329
+
330
+ def test_should_track_attribute_changes
331
+ assert_equal %w(parked parked), @record.changes['status']
332
+ end
333
+ end
334
+
335
+ class MachineWithCallbacksTest < BaseTestCase
336
+ def setup
337
+ @model = new_model
338
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
339
+ @machine.other_states :idling
340
+ @machine.event :ignite
341
+
342
+ @record = @model.new(:state => 'parked')
343
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
344
+ end
345
+
346
+ def test_should_run_before_callbacks
347
+ called = false
348
+ @machine.before_transition {called = true}
349
+
350
+ @transition.perform
351
+ assert called
352
+ end
353
+
354
+ def test_should_pass_record_to_before_callbacks_with_one_argument
355
+ record = nil
356
+ @machine.before_transition {|arg| record = arg}
357
+
358
+ @transition.perform
359
+ assert_equal @record, record
360
+ end
361
+
362
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
363
+ callback_args = nil
364
+ @machine.before_transition {|*args| callback_args = args}
365
+
366
+ @transition.perform
367
+ assert_equal [@record, @transition], callback_args
368
+ end
369
+
370
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
371
+ context = nil
372
+ @machine.before_transition {context = self}
373
+
374
+ @transition.perform
375
+ assert_equal self, context
376
+ end
377
+
378
+ def test_should_run_after_callbacks
379
+ called = false
380
+ @machine.after_transition {called = true}
381
+
382
+ @transition.perform
383
+ assert called
384
+ end
385
+
386
+ def test_should_pass_record_to_after_callbacks_with_one_argument
387
+ record = nil
388
+ @machine.after_transition {|arg| record = arg}
389
+
390
+ @transition.perform
391
+ assert_equal @record, record
392
+ end
393
+
394
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
395
+ callback_args = nil
396
+ @machine.after_transition {|*args| callback_args = args}
397
+
398
+ @transition.perform
399
+ assert_equal [@record, @transition], callback_args
400
+ end
401
+
402
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
403
+ context = nil
404
+ @machine.after_transition {context = self}
405
+
406
+ @transition.perform
407
+ assert_equal self, context
408
+ end
409
+
410
+ def test_should_run_around_callbacks
411
+ before_called = false
412
+ after_called = false
413
+ @machine.around_transition {|block| before_called = true; block.call; after_called = true}
414
+
415
+ @transition.perform
416
+ assert before_called
417
+ assert after_called
418
+ end
419
+
420
+ def test_should_include_transition_states_in_known_states
421
+ @machine.before_transition :to => :first_gear, :do => lambda {}
422
+
423
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
424
+ end
425
+
426
+ def test_should_allow_symbolic_callbacks
427
+ callback_args = nil
428
+
429
+ klass = class << @record; self; end
430
+ klass.send(:define_method, :after_ignite) do |*args|
431
+ callback_args = args
432
+ end
433
+
434
+ @machine.before_transition(:after_ignite)
435
+
436
+ @transition.perform
437
+ assert_equal [@transition], callback_args
438
+ end
439
+
440
+ def test_should_allow_string_callbacks
441
+ class << @record
442
+ attr_reader :callback_result
443
+ end
444
+
445
+ @machine.before_transition('@callback_result = [1, 2, 3]')
446
+ @transition.perform
447
+
448
+ assert_equal [1, 2, 3], @record.callback_result
449
+ end
450
+ end
451
+
452
+ class MachineWithFailedBeforeCallbacksTest < BaseTestCase
453
+ def setup
454
+ @callbacks = []
455
+
456
+ @model = new_model
457
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
458
+ @machine.state :parked, :idling
459
+ @machine.event :ignite
460
+ @machine.before_transition {@callbacks << :before_1; false}
461
+ @machine.before_transition {@callbacks << :before_2}
462
+ @machine.after_transition {@callbacks << :after}
463
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
464
+
465
+ @record = @model.new(:state => 'parked')
466
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
467
+ @result = @transition.perform
468
+ end
469
+
470
+ def test_should_not_be_successful
471
+ assert !@result
472
+ end
473
+
474
+ def test_should_not_change_current_state
475
+ assert_equal 'parked', @record.state
476
+ end
477
+
478
+ def test_should_not_run_further_callbacks
479
+ assert_equal [:before_1], @callbacks
480
+ end
481
+ end
482
+
483
+ class MachineWithFailedAfterCallbacksTest < BaseTestCase
484
+ def setup
485
+ @callbacks = []
486
+
487
+ @model = new_model
488
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
489
+ @machine.state :parked, :idling
490
+ @machine.event :ignite
491
+ @machine.after_transition {@callbacks << :after_1; false}
492
+ @machine.after_transition {@callbacks << :after_2}
493
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
494
+
495
+ @record = @model.new(:state => 'parked')
496
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
497
+ @result = @transition.perform
498
+ end
499
+
500
+ def test_should_be_successful
501
+ assert @result
502
+ end
503
+
504
+ def test_should_change_current_state
505
+ assert_equal 'idling', @record.state
506
+ end
507
+
508
+ def test_should_not_run_further_after_callbacks
509
+ assert_equal [:around_before, :around_after, :after_1], @callbacks
510
+ end
511
+ end
512
+
513
+ class MachineWithValidationsTest < BaseTestCase
514
+ def setup
515
+ @model = new_model { include ActiveModel::Validations }
516
+ @machine = StateMachine::Machine.new(@model, :action => :save)
517
+ @machine.state :parked
518
+
519
+ @record = @model.new
520
+ end
521
+
522
+ def test_should_invalidate_using_errors
523
+ I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
524
+ @record.state = 'parked'
525
+
526
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, :park]])
527
+ assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
528
+ end
529
+
530
+ def test_should_auto_prefix_custom_attributes_on_invalidation
531
+ @machine.invalidate(@record, :event, :invalid)
532
+
533
+ assert_equal ['State event is invalid'], @record.errors.full_messages
534
+ end
535
+
536
+ def test_should_clear_errors_on_reset
537
+ @record.state = 'parked'
538
+ @record.errors.add(:state, 'is invalid')
539
+
540
+ @machine.reset(@record)
541
+ assert_equal [], @record.errors.full_messages
542
+ end
543
+
544
+ def test_should_be_valid_if_state_is_known
545
+ @record.state = 'parked'
546
+
547
+ assert @record.valid?
548
+ end
549
+
550
+ def test_should_not_be_valid_if_state_is_unknown
551
+ @record.state = 'invalid'
552
+
553
+ assert !@record.valid?
554
+ assert_equal ['State is invalid'], @record.errors.full_messages
555
+ end
556
+ end
557
+
558
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
559
+ def setup
560
+ @model = new_model { include ActiveModel::Validations }
561
+
562
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
563
+ @machine.state :parked
564
+
565
+ @record = @model.new
566
+ end
567
+
568
+ def test_should_add_validation_errors_to_custom_attribute
569
+ @record.state = 'invalid'
570
+
571
+ assert !@record.valid?
572
+ assert_equal ['State is invalid'], @record.errors.full_messages
573
+
574
+ @record.state = 'parked'
575
+ assert @record.valid?
576
+ end
577
+ end
578
+
579
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
580
+ def setup
581
+ @model = new_model do
582
+ include ActiveModel::Validations
583
+ attr_accessor :seatbelt
584
+ end
585
+
586
+ @machine = StateMachine::Machine.new(@model)
587
+ @machine.state :first_gear, :second_gear do
588
+ validates_presence_of :seatbelt
589
+ end
590
+ @machine.other_states :parked
591
+ end
592
+
593
+ def test_should_be_valid_if_validation_fails_outside_state_scope
594
+ record = @model.new(:state => 'parked', :seatbelt => nil)
595
+ assert record.valid?
596
+ end
597
+
598
+ def test_should_be_invalid_if_validation_fails_within_state_scope
599
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
600
+ assert !record.valid?
601
+ end
602
+
603
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
604
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
605
+ assert record.valid?
606
+ end
607
+ end
608
+
609
+ class MachineWithObserversTest < BaseTestCase
610
+ def setup
611
+ @model = new_model { include ActiveModel::Observing }
612
+ @machine = StateMachine::Machine.new(@model)
613
+ @machine.state :parked, :idling
614
+ @machine.event :ignite
615
+ @record = @model.new(:state => 'parked')
616
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
617
+ end
618
+
619
+ def test_should_call_all_transition_callback_permutations
620
+ callbacks = [
621
+ :before_ignite_from_parked_to_idling,
622
+ :before_ignite_from_parked,
623
+ :before_ignite_to_idling,
624
+ :before_ignite,
625
+ :before_transition_state_from_parked_to_idling,
626
+ :before_transition_state_from_parked,
627
+ :before_transition_state_to_idling,
628
+ :before_transition_state,
629
+ :before_transition
630
+ ]
631
+
632
+ notified = false
633
+ observer = new_observer(@model) do
634
+ callbacks.each do |callback|
635
+ define_method(callback) do |*args|
636
+ notifications << callback
637
+ end
638
+ end
639
+ end
640
+
641
+ instance = observer.instance
642
+
643
+ @transition.perform
644
+ assert_equal callbacks, instance.notifications
645
+ end
646
+
647
+ def test_should_pass_record_and_transition_to_before_callbacks
648
+ observer = new_observer(@model) do
649
+ def before_transition(*args)
650
+ notifications << args
651
+ end
652
+ end
653
+ instance = observer.instance
654
+
655
+ @transition.perform
656
+ assert_equal [[@record, @transition]], instance.notifications
657
+ end
658
+
659
+ def test_should_pass_record_and_transition_to_after_callbacks
660
+ observer = new_observer(@model) do
661
+ def after_transition(*args)
662
+ notifications << args
663
+ end
664
+ end
665
+ instance = observer.instance
666
+
667
+ @transition.perform
668
+ assert_equal [[@record, @transition]], instance.notifications
669
+ end
670
+
671
+ def test_should_call_methods_outside_the_context_of_the_record
672
+ observer = new_observer(@model) do
673
+ def before_ignite(*args)
674
+ notifications << self
675
+ end
676
+ end
677
+ instance = observer.instance
678
+
679
+ @transition.perform
680
+ assert_equal [instance], instance.notifications
681
+ end
682
+ end
683
+
684
+ class MachineWithNamespacedObserversTest < BaseTestCase
685
+ def setup
686
+ @model = new_model { include ActiveModel::Observing }
687
+ @machine = StateMachine::Machine.new(@model, :state, :namespace => 'alarm')
688
+ @machine.state :active, :off
689
+ @machine.event :enable
690
+ @record = @model.new(:state => 'off')
691
+ @transition = StateMachine::Transition.new(@record, @machine, :enable, :off, :active)
692
+ end
693
+
694
+ def test_should_call_namespaced_before_event_method
695
+ observer = new_observer(@model) do
696
+ def before_enable_alarm(*args)
697
+ notifications << args
698
+ end
699
+ end
700
+ instance = observer.instance
701
+
702
+ @transition.perform
703
+ assert_equal [[@record, @transition]], instance.notifications
704
+ end
705
+
706
+ def test_should_call_namespaced_after_event_method
707
+ observer = new_observer(@model) do
708
+ def after_enable_alarm(*args)
709
+ notifications << args
710
+ end
711
+ end
712
+ instance = observer.instance
713
+
714
+ @transition.perform
715
+ assert_equal [[@record, @transition]], instance.notifications
716
+ end
717
+ end
718
+
719
+ class MachineWithMixedCallbacksTest < BaseTestCase
720
+ def setup
721
+ @model = new_model { include ActiveModel::Observing }
722
+ @machine = StateMachine::Machine.new(@model)
723
+ @machine.state :parked, :idling
724
+ @machine.event :ignite
725
+ @record = @model.new(:state => 'parked')
726
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
727
+
728
+ @notifications = []
729
+
730
+ # Create callbacks
731
+ @machine.before_transition {@notifications << :callback_before_transition}
732
+ @machine.after_transition {@notifications << :callback_after_transition}
733
+ @machine.around_transition {|block| @notifications << :callback_around_before_transition; block.call; @notifications << :callback_around_after_transition}
734
+
735
+ # Create observer callbacks
736
+ observer = new_observer(@model) do
737
+ def before_ignite(*args)
738
+ notifications << :observer_before_ignite
739
+ end
740
+
741
+ def before_transition(*args)
742
+ notifications << :observer_before_transition
743
+ end
744
+
745
+ def after_ignite(*args)
746
+ notifications << :observer_after_ignite
747
+ end
748
+
749
+ def after_transition(*args)
750
+ notifications << :observer_after_transition
751
+ end
752
+ end
753
+ instance = observer.instance
754
+ instance.notifications = @notifications
755
+
756
+ @transition.perform
757
+ end
758
+
759
+ def test_should_invoke_callbacks_in_specific_order
760
+ expected = [
761
+ :callback_before_transition,
762
+ :callback_around_before_transition,
763
+ :observer_before_ignite,
764
+ :observer_before_transition,
765
+ :callback_around_after_transition,
766
+ :callback_after_transition,
767
+ :observer_after_ignite,
768
+ :observer_after_transition
769
+ ]
770
+
771
+ assert_equal expected, @notifications
772
+ end
773
+ end
774
+
775
+ class MachineWithInternationalizationTest < BaseTestCase
776
+ def setup
777
+ I18n.backend = I18n::Backend::Simple.new
778
+
779
+ # Initialize the backend
780
+ I18n.backend.translate(:en, 'activemodel.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
781
+
782
+ @model = new_model { include ActiveModel::Validations }
783
+ end
784
+
785
+ def test_should_use_defaults
786
+ I18n.backend.store_translations(:en, {
787
+ :activemodel => {:errors => {:messages => {:invalid_transition => 'cannot {{event}}'}}}
788
+ })
789
+
790
+ machine = StateMachine::Machine.new(@model, :action => :save)
791
+ machine.state :parked, :idling
792
+ machine.event :ignite
793
+
794
+ record = @model.new(:state => 'idling')
795
+
796
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
797
+ assert_equal ['State cannot ignite'], record.errors.full_messages
798
+ end
799
+
800
+ def test_should_allow_customized_error_key
801
+ I18n.backend.store_translations(:en, {
802
+ :activemodel => {:errors => {:messages => {:bad_transition => 'cannot {{event}}'}}}
803
+ })
804
+
805
+ machine = StateMachine::Machine.new(@model, :action => :save, :messages => {:invalid_transition => :bad_transition})
806
+ machine.state :parked, :idling
807
+
808
+ record = @model.new
809
+ record.state = 'idling'
810
+
811
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
812
+ assert_equal ['State cannot ignite'], record.errors.full_messages
813
+ end
814
+
815
+ def test_should_allow_customized_error_string
816
+ machine = StateMachine::Machine.new(@model, :action => :save, :messages => {:invalid_transition => 'cannot {{event}}'})
817
+ machine.state :parked, :idling
818
+
819
+ record = @model.new(:state => 'idling')
820
+
821
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
822
+ assert_equal ['State cannot ignite'], record.errors.full_messages
823
+ end
824
+
825
+ def test_should_allow_customized_state_key_scoped_to_class_and_machine
826
+ I18n.backend.store_translations(:en, {
827
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:states => {:parked => 'shutdown'}}}}}
828
+ })
829
+
830
+ machine = StateMachine::Machine.new(@model, :initial => :parked, :action => :save)
831
+ record = @model.new
832
+
833
+ machine.invalidate(record, :event, :invalid_event, [[:state, :parked]])
834
+ assert_equal ['State event cannot transition when shutdown'], record.errors.full_messages
835
+ end
836
+
837
+ def test_should_allow_customized_state_key_scoped_to_machine
838
+ I18n.backend.store_translations(:en, {
839
+ :activemodel => {:state_machines => {:state => {:states => {:parked => 'shutdown'}}}}
840
+ })
841
+
842
+ machine = StateMachine::Machine.new(@model, :initial => :parked, :action => :save)
843
+ record = @model.new
844
+
845
+ machine.invalidate(record, :event, :invalid_event, [[:state, :parked]])
846
+ assert_equal ['State event cannot transition when shutdown'], record.errors.full_messages
847
+ end
848
+
849
+ def test_should_allow_customized_state_key_unscoped
850
+ I18n.backend.store_translations(:en, {
851
+ :activemodel => {:state_machines => {:states => {:parked => 'shutdown'}}}
852
+ })
853
+
854
+ machine = StateMachine::Machine.new(@model, :initial => :parked, :action => :save)
855
+ record = @model.new
856
+
857
+ machine.invalidate(record, :event, :invalid_event, [[:state, :parked]])
858
+ assert_equal ['State event cannot transition when shutdown'], record.errors.full_messages
859
+ end
860
+
861
+ def test_should_allow_customized_event_key_scoped_to_class_and_machine
862
+ I18n.backend.store_translations(:en, {
863
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:events => {:park => 'stop'}}}}}
864
+ })
865
+
866
+ machine = StateMachine::Machine.new(@model, :action => :save)
867
+ machine.event :park
868
+ record = @model.new
869
+
870
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
871
+ assert_equal ['State cannot transition via "stop"'], record.errors.full_messages
872
+ end
873
+
874
+ def test_should_allow_customized_event_key_scoped_to_machine
875
+ I18n.backend.store_translations(:en, {
876
+ :activemodel => {:state_machines => {:state => {:events => {:park => 'stop'}}}}
877
+ })
878
+
879
+ machine = StateMachine::Machine.new(@model, :action => :save)
880
+ machine.event :park
881
+ record = @model.new
882
+
883
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
884
+ assert_equal ['State cannot transition via "stop"'], record.errors.full_messages
885
+ end
886
+
887
+ def test_should_allow_customized_event_key_unscoped
888
+ I18n.backend.store_translations(:en, {
889
+ :activemodel => {:state_machines => {:events => {:park => 'stop'}}}
890
+ })
891
+
892
+ machine = StateMachine::Machine.new(@model, :action => :save)
893
+ machine.event :park
894
+ record = @model.new
895
+
896
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
897
+ assert_equal ['State cannot transition via "stop"'], record.errors.full_messages
898
+ end
899
+
900
+ def test_should_only_add_locale_once_in_load_path
901
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_model/locale\.rb$}}.length
902
+
903
+ # Create another ActiveRecord model that will triger the i18n feature
904
+ new_model
905
+
906
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_model/locale\.rb$}}.length
907
+ end
908
+ end
909
+ end