state_machine 0.8.1 → 0.9.0

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