hsume2-state_machine 1.0.1

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 (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,1097 @@
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 ActiveModel 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 ActiveModel 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_have_an_integration_name
79
+ assert_equal :active_model, StateMachine::Integrations::ActiveModel.integration_name
80
+ end
81
+
82
+ def test_should_be_available
83
+ assert StateMachine::Integrations::ActiveModel.available?
84
+ end
85
+
86
+ def test_should_match_if_class_includes_dirty_feature
87
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Dirty })
88
+ end
89
+
90
+ def test_should_match_if_class_includes_observing_feature
91
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Observing })
92
+ end
93
+
94
+ def test_should_match_if_class_includes_validations_feature
95
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Validations })
96
+ end
97
+
98
+ def test_should_not_match_if_class_does_not_include_active_model_features
99
+ assert !StateMachine::Integrations::ActiveModel.matches?(new_model)
100
+ end
101
+
102
+ def test_should_have_no_defaults
103
+ assert_equal e = {}, StateMachine::Integrations::ActiveModel.defaults
104
+ end
105
+
106
+ def test_should_have_a_locale_path
107
+ assert_not_nil StateMachine::Integrations::ActiveModel.locale_path
108
+ end
109
+ end
110
+
111
+ class MachineByDefaultTest < BaseTestCase
112
+ def setup
113
+ @model = new_model
114
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
115
+ end
116
+
117
+ def test_should_not_have_action
118
+ assert_nil @machine.action
119
+ end
120
+
121
+ def test_should_use_transactions
122
+ assert_equal true, @machine.use_transactions
123
+ end
124
+
125
+ def test_should_not_have_any_before_callbacks
126
+ assert_equal 0, @machine.callbacks[:before].size
127
+ end
128
+
129
+ def test_should_not_have_any_after_callbacks
130
+ assert_equal 0, @machine.callbacks[:after].size
131
+ end
132
+ end
133
+
134
+ class MachineWithStatesTest < BaseTestCase
135
+ def setup
136
+ @model = new_model
137
+ @machine = StateMachine::Machine.new(@model)
138
+ @machine.state :first_gear
139
+ end
140
+
141
+ def test_should_humanize_name
142
+ assert_equal 'first gear', @machine.state(:first_gear).human_name
143
+ end
144
+ end
145
+
146
+ class MachineWithStaticInitialStateTest < BaseTestCase
147
+ def setup
148
+ @model = new_model
149
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
150
+ end
151
+
152
+ def test_should_set_initial_state_on_created_object
153
+ record = @model.new
154
+ assert_equal 'parked', record.state
155
+ end
156
+ end
157
+
158
+ class MachineWithDynamicInitialStateTest < BaseTestCase
159
+ def setup
160
+ @model = new_model
161
+ @machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked}, :integration => :active_model)
162
+ @machine.state :parked
163
+ end
164
+
165
+ def test_should_set_initial_state_on_created_object
166
+ record = @model.new
167
+ assert_equal 'parked', record.state
168
+ end
169
+ end
170
+
171
+ class MachineWithEventsTest < BaseTestCase
172
+ def setup
173
+ @model = new_model
174
+ @machine = StateMachine::Machine.new(@model)
175
+ @machine.event :shift_up
176
+ end
177
+
178
+ def test_should_humanize_name
179
+ assert_equal 'shift up', @machine.event(:shift_up).human_name
180
+ end
181
+ end
182
+
183
+ class MachineWithModelStateAttributeTest < BaseTestCase
184
+ def setup
185
+ @model = new_model
186
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
187
+ @machine.other_states(:idling)
188
+
189
+ @record = @model.new
190
+ end
191
+
192
+ def test_should_have_an_attribute_predicate
193
+ assert @record.respond_to?(:state?)
194
+ end
195
+
196
+ def test_should_raise_exception_for_predicate_without_parameters
197
+ exception = assert_raise(ArgumentError) { @record.state? }
198
+ assert_equal 'wrong number of arguments (1 for 2)', exception.message
199
+ end
200
+
201
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
202
+ assert !@record.state?(:idling)
203
+ end
204
+
205
+ def test_should_return_true_for_predicate_if_matches_current_value
206
+ assert @record.state?(:parked)
207
+ end
208
+
209
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
210
+ assert_raise(IndexError) { @record.state?(:invalid) }
211
+ end
212
+ end
213
+
214
+ class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
215
+ def setup
216
+ @model = new_model do
217
+ def initialize
218
+ end
219
+ end
220
+
221
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked, :integration => :active_model)
222
+ @machine.other_states(:idling)
223
+ @record = @model.new
224
+ end
225
+
226
+ def test_should_not_define_a_reader_attribute_for_the_attribute
227
+ assert !@record.respond_to?(:status)
228
+ end
229
+
230
+ def test_should_not_define_a_writer_attribute_for_the_attribute
231
+ assert !@record.respond_to?(:status=)
232
+ end
233
+
234
+ def test_should_define_an_attribute_predicate
235
+ assert @record.respond_to?(:status?)
236
+ end
237
+ end
238
+
239
+ class MachineWithInitializedStateTest < BaseTestCase
240
+ def setup
241
+ @model = new_model
242
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
243
+ @machine.state nil, :idling
244
+ end
245
+
246
+ def test_should_allow_nil_initial_state_when_static
247
+ @machine.state nil
248
+
249
+ record = @model.new(:state => nil)
250
+ assert_nil record.state
251
+ end
252
+
253
+ def test_should_allow_nil_initial_state_when_dynamic
254
+ @machine.state nil
255
+
256
+ @machine.initial_state = lambda {:parked}
257
+ record = @model.new(:state => nil)
258
+ assert_nil record.state
259
+ end
260
+
261
+ def test_should_allow_different_initial_state_when_static
262
+ record = @model.new(:state => 'idling')
263
+ assert_equal 'idling', record.state
264
+ end
265
+
266
+ def test_should_allow_different_initial_state_when_dynamic
267
+ @machine.initial_state = lambda {:parked}
268
+ record = @model.new(:state => 'idling')
269
+ assert_equal 'idling', record.state
270
+ end
271
+
272
+ def test_should_use_default_state_if_protected
273
+ @model.class_eval do
274
+ include ActiveModel::MassAssignmentSecurity
275
+ attr_protected :state
276
+
277
+ def initialize(attrs = {})
278
+ initialize_state_machines do
279
+ sanitize_for_mass_assignment(attrs).each {|attr, value| send("#{attr}=", value)} if attrs
280
+ @changed_attributes = {}
281
+ end
282
+ end
283
+ end
284
+
285
+ record = @model.new(:state => 'idling')
286
+ assert_equal 'parked', record.state
287
+
288
+ record = @model.new(nil)
289
+ assert_equal 'parked', record.state
290
+ end
291
+ end
292
+
293
+ class MachineMultipleTest < BaseTestCase
294
+ def setup
295
+ @model = new_model do
296
+ model_attribute :status
297
+ end
298
+
299
+ @state_machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
300
+ @status_machine = StateMachine::Machine.new(@model, :status, :initial => :idling, :integration => :active_model)
301
+ end
302
+
303
+ def test_should_should_initialize_each_state
304
+ record = @model.new
305
+ assert_equal 'parked', record.state
306
+ assert_equal 'idling', record.status
307
+ end
308
+ end
309
+
310
+ class MachineWithDirtyAttributesTest < BaseTestCase
311
+ def setup
312
+ @model = new_model do
313
+ include ActiveModel::Dirty
314
+ define_attribute_methods [:state]
315
+ end
316
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
317
+ @machine.event :ignite
318
+ @machine.state :idling
319
+
320
+ @record = @model.create
321
+
322
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
323
+ @transition.perform
324
+ end
325
+
326
+ def test_should_include_state_in_changed_attributes
327
+ assert_equal %w(state), @record.changed
328
+ end
329
+
330
+ def test_should_track_attribute_change
331
+ assert_equal %w(parked idling), @record.changes['state']
332
+ end
333
+
334
+ def test_should_not_reset_changes_on_multiple_transitions
335
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
336
+ transition.perform
337
+
338
+ assert_equal %w(parked idling), @record.changes['state']
339
+ end
340
+ end
341
+
342
+ class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
343
+ def setup
344
+ @model = new_model do
345
+ include ActiveModel::Dirty
346
+ define_attribute_methods [:state]
347
+ end
348
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
349
+ @machine.event :park
350
+
351
+ @record = @model.create
352
+
353
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
354
+ @transition.perform
355
+ end
356
+
357
+ def test_should_include_state_in_changed_attributes
358
+ assert_equal %w(state), @record.changed
359
+ end
360
+
361
+ def test_should_track_attribute_changes
362
+ assert_equal %w(parked parked), @record.changes['state']
363
+ end
364
+ end
365
+
366
+ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
367
+ def setup
368
+ @model = new_model do
369
+ include ActiveModel::Dirty
370
+ model_attribute :status
371
+ define_attribute_methods [:status]
372
+ end
373
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
374
+ @machine.event :ignite
375
+ @machine.state :idling
376
+
377
+ @record = @model.create
378
+
379
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
380
+ @transition.perform
381
+ end
382
+
383
+ def test_should_include_state_in_changed_attributes
384
+ assert_equal %w(status), @record.changed
385
+ end
386
+
387
+ def test_should_track_attribute_change
388
+ assert_equal %w(parked idling), @record.changes['status']
389
+ end
390
+
391
+ def test_should_not_reset_changes_on_multiple_transitions
392
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
393
+ transition.perform
394
+
395
+ assert_equal %w(parked idling), @record.changes['status']
396
+ end
397
+ end
398
+
399
+ class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
400
+ def setup
401
+ @model = new_model do
402
+ include ActiveModel::Dirty
403
+ model_attribute :status
404
+ define_attribute_methods [:status]
405
+ end
406
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
407
+ @machine.event :park
408
+
409
+ @record = @model.create
410
+
411
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
412
+ @transition.perform
413
+ end
414
+
415
+ def test_should_include_state_in_changed_attributes
416
+ assert_equal %w(status), @record.changed
417
+ end
418
+
419
+ def test_should_track_attribute_changes
420
+ assert_equal %w(parked parked), @record.changes['status']
421
+ end
422
+ end
423
+
424
+ class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
425
+ def setup
426
+ @model = new_model do
427
+ include ActiveModel::Dirty
428
+ define_attribute_methods [:state]
429
+ end
430
+ @machine = StateMachine::Machine.new(@model, :action => :save, :initial => :parked)
431
+ @machine.event :ignite
432
+
433
+ @record = @model.create
434
+ @record.state_event = 'ignite'
435
+ end
436
+
437
+ def test_should_include_state_in_changed_attributes
438
+ assert_equal %w(state), @record.changed
439
+ end
440
+
441
+ def test_should_track_attribute_change
442
+ assert_equal %w(parked parked), @record.changes['state']
443
+ end
444
+
445
+ def test_should_not_reset_changes_on_multiple_changes
446
+ @record.state_event = 'ignite'
447
+ assert_equal %w(parked parked), @record.changes['state']
448
+ end
449
+
450
+ def test_should_not_include_state_in_changed_attributes_if_nil
451
+ @record = @model.create
452
+ @record.state_event = nil
453
+
454
+ assert_equal [], @record.changed
455
+ end
456
+ end
457
+
458
+ class MachineWithCallbacksTest < BaseTestCase
459
+ def setup
460
+ @model = new_model
461
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
462
+ @machine.other_states :idling
463
+ @machine.event :ignite
464
+
465
+ @record = @model.new(:state => 'parked')
466
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
467
+ end
468
+
469
+ def test_should_run_before_callbacks
470
+ called = false
471
+ @machine.before_transition {called = true}
472
+
473
+ @transition.perform
474
+ assert called
475
+ end
476
+
477
+ def test_should_pass_record_to_before_callbacks_with_one_argument
478
+ record = nil
479
+ @machine.before_transition {|arg| record = arg}
480
+
481
+ @transition.perform
482
+ assert_equal @record, record
483
+ end
484
+
485
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
486
+ callback_args = nil
487
+ @machine.before_transition {|*args| callback_args = args}
488
+
489
+ @transition.perform
490
+ assert_equal [@record, @transition], callback_args
491
+ end
492
+
493
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
494
+ context = nil
495
+ @machine.before_transition {context = self}
496
+
497
+ @transition.perform
498
+ assert_equal self, context
499
+ end
500
+
501
+ def test_should_run_after_callbacks
502
+ called = false
503
+ @machine.after_transition {called = true}
504
+
505
+ @transition.perform
506
+ assert called
507
+ end
508
+
509
+ def test_should_pass_record_to_after_callbacks_with_one_argument
510
+ record = nil
511
+ @machine.after_transition {|arg| record = arg}
512
+
513
+ @transition.perform
514
+ assert_equal @record, record
515
+ end
516
+
517
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
518
+ callback_args = nil
519
+ @machine.after_transition {|*args| callback_args = args}
520
+
521
+ @transition.perform
522
+ assert_equal [@record, @transition], callback_args
523
+ end
524
+
525
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
526
+ context = nil
527
+ @machine.after_transition {context = self}
528
+
529
+ @transition.perform
530
+ assert_equal self, context
531
+ end
532
+
533
+ def test_should_run_around_callbacks
534
+ before_called = false
535
+ after_called = false
536
+ @machine.around_transition {|block| before_called = true; block.call; after_called = true}
537
+
538
+ @transition.perform
539
+ assert before_called
540
+ assert after_called
541
+ end
542
+
543
+ def test_should_include_transition_states_in_known_states
544
+ @machine.before_transition :to => :first_gear, :do => lambda {}
545
+
546
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
547
+ end
548
+
549
+ def test_should_allow_symbolic_callbacks
550
+ callback_args = nil
551
+
552
+ klass = class << @record; self; end
553
+ klass.send(:define_method, :after_ignite) do |*args|
554
+ callback_args = args
555
+ end
556
+
557
+ @machine.before_transition(:after_ignite)
558
+
559
+ @transition.perform
560
+ assert_equal [@transition], callback_args
561
+ end
562
+
563
+ def test_should_allow_string_callbacks
564
+ class << @record
565
+ attr_reader :callback_result
566
+ end
567
+
568
+ @machine.before_transition('@callback_result = [1, 2, 3]')
569
+ @transition.perform
570
+
571
+ assert_equal [1, 2, 3], @record.callback_result
572
+ end
573
+ end
574
+
575
+ class MachineWithFailedBeforeCallbacksTest < BaseTestCase
576
+ def setup
577
+ @callbacks = []
578
+
579
+ @model = new_model
580
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
581
+ @machine.state :parked, :idling
582
+ @machine.event :ignite
583
+ @machine.before_transition {@callbacks << :before_1; false}
584
+ @machine.before_transition {@callbacks << :before_2}
585
+ @machine.after_transition {@callbacks << :after}
586
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
587
+
588
+ @record = @model.new(:state => 'parked')
589
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
590
+ @result = @transition.perform
591
+ end
592
+
593
+ def test_should_not_be_successful
594
+ assert !@result
595
+ end
596
+
597
+ def test_should_not_change_current_state
598
+ assert_equal 'parked', @record.state
599
+ end
600
+
601
+ def test_should_not_run_further_callbacks
602
+ assert_equal [:before_1], @callbacks
603
+ end
604
+ end
605
+
606
+ class MachineWithFailedAfterCallbacksTest < BaseTestCase
607
+ def setup
608
+ @callbacks = []
609
+
610
+ @model = new_model
611
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
612
+ @machine.state :parked, :idling
613
+ @machine.event :ignite
614
+ @machine.after_transition {@callbacks << :after_1; false}
615
+ @machine.after_transition {@callbacks << :after_2}
616
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
617
+
618
+ @record = @model.new(:state => 'parked')
619
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
620
+ @result = @transition.perform
621
+ end
622
+
623
+ def test_should_be_successful
624
+ assert @result
625
+ end
626
+
627
+ def test_should_change_current_state
628
+ assert_equal 'idling', @record.state
629
+ end
630
+
631
+ def test_should_not_run_further_after_callbacks
632
+ assert_equal [:around_before, :around_after, :after_1], @callbacks
633
+ end
634
+ end
635
+
636
+ class MachineWithValidationsTest < BaseTestCase
637
+ def setup
638
+ @model = new_model { include ActiveModel::Validations }
639
+ @machine = StateMachine::Machine.new(@model, :action => :save)
640
+ @machine.state :parked
641
+
642
+ @record = @model.new
643
+ end
644
+
645
+ def test_should_invalidate_using_errors
646
+ I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
647
+ @record.state = 'parked'
648
+
649
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, 'park']])
650
+ assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
651
+ end
652
+
653
+ def test_should_auto_prefix_custom_attributes_on_invalidation
654
+ @machine.invalidate(@record, :event, :invalid)
655
+
656
+ assert_equal ['State event is invalid'], @record.errors.full_messages
657
+ end
658
+
659
+ def test_should_clear_errors_on_reset
660
+ @record.state = 'parked'
661
+ @record.errors.add(:state, 'is invalid')
662
+
663
+ @machine.reset(@record)
664
+ assert_equal [], @record.errors.full_messages
665
+ end
666
+
667
+ def test_should_be_valid_if_state_is_known
668
+ @record.state = 'parked'
669
+
670
+ assert @record.valid?
671
+ end
672
+
673
+ def test_should_not_be_valid_if_state_is_unknown
674
+ @record.state = 'invalid'
675
+
676
+ assert !@record.valid?
677
+ assert_equal ['State is invalid'], @record.errors.full_messages
678
+ end
679
+ end
680
+
681
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
682
+ def setup
683
+ @model = new_model { include ActiveModel::Validations }
684
+
685
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
686
+ @machine.state :parked
687
+
688
+ @record = @model.new
689
+ end
690
+
691
+ def test_should_add_validation_errors_to_custom_attribute
692
+ @record.state = 'invalid'
693
+
694
+ assert !@record.valid?
695
+ assert_equal ['State is invalid'], @record.errors.full_messages
696
+
697
+ @record.state = 'parked'
698
+ assert @record.valid?
699
+ end
700
+ end
701
+
702
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
703
+ def setup
704
+ @model = new_model do
705
+ include ActiveModel::Validations
706
+ attr_accessor :seatbelt
707
+ end
708
+
709
+ @machine = StateMachine::Machine.new(@model)
710
+ @machine.state :first_gear, :second_gear do
711
+ validates_presence_of :seatbelt
712
+ end
713
+ @machine.other_states :parked
714
+ end
715
+
716
+ def test_should_be_valid_if_validation_fails_outside_state_scope
717
+ record = @model.new(:state => 'parked', :seatbelt => nil)
718
+ assert record.valid?
719
+ end
720
+
721
+ def test_should_be_invalid_if_validation_fails_within_state_scope
722
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
723
+ assert !record.valid?
724
+ end
725
+
726
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
727
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
728
+ assert record.valid?
729
+ end
730
+ end
731
+
732
+ class MachineWithObserversTest < BaseTestCase
733
+ def setup
734
+ @model = new_model { include ActiveModel::Observing }
735
+ @machine = StateMachine::Machine.new(@model)
736
+ @machine.state :parked, :idling
737
+ @machine.event :ignite
738
+ @record = @model.new(:state => 'parked')
739
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
740
+ end
741
+
742
+ def test_should_call_all_transition_callback_permutations
743
+ callbacks = [
744
+ :before_ignite_from_parked_to_idling,
745
+ :before_ignite_from_parked,
746
+ :before_ignite_to_idling,
747
+ :before_ignite,
748
+ :before_transition_state_from_parked_to_idling,
749
+ :before_transition_state_from_parked,
750
+ :before_transition_state_to_idling,
751
+ :before_transition_state,
752
+ :before_transition
753
+ ]
754
+
755
+ notified = false
756
+ observer = new_observer(@model) do
757
+ callbacks.each do |callback|
758
+ define_method(callback) do |*args|
759
+ notifications << callback
760
+ end
761
+ end
762
+ end
763
+
764
+ instance = observer.instance
765
+
766
+ @transition.perform
767
+ assert_equal callbacks, instance.notifications
768
+ end
769
+
770
+ def test_should_pass_record_and_transition_to_before_callbacks
771
+ observer = new_observer(@model) do
772
+ def before_transition(*args)
773
+ notifications << args
774
+ end
775
+ end
776
+ instance = observer.instance
777
+
778
+ @transition.perform
779
+ assert_equal [[@record, @transition]], instance.notifications
780
+ end
781
+
782
+ def test_should_pass_record_and_transition_to_after_callbacks
783
+ observer = new_observer(@model) do
784
+ def after_transition(*args)
785
+ notifications << args
786
+ end
787
+ end
788
+ instance = observer.instance
789
+
790
+ @transition.perform
791
+ assert_equal [[@record, @transition]], instance.notifications
792
+ end
793
+
794
+ def test_should_call_methods_outside_the_context_of_the_record
795
+ observer = new_observer(@model) do
796
+ def before_ignite(*args)
797
+ notifications << self
798
+ end
799
+ end
800
+ instance = observer.instance
801
+
802
+ @transition.perform
803
+ assert_equal [instance], instance.notifications
804
+ end
805
+ end
806
+
807
+ class MachineWithNamespacedObserversTest < BaseTestCase
808
+ def setup
809
+ @model = new_model { include ActiveModel::Observing }
810
+ @machine = StateMachine::Machine.new(@model, :state, :namespace => 'alarm')
811
+ @machine.state :active, :off
812
+ @machine.event :enable
813
+ @record = @model.new(:state => 'off')
814
+ @transition = StateMachine::Transition.new(@record, @machine, :enable, :off, :active)
815
+ end
816
+
817
+ def test_should_call_namespaced_before_event_method
818
+ observer = new_observer(@model) do
819
+ def before_enable_alarm(*args)
820
+ notifications << args
821
+ end
822
+ end
823
+ instance = observer.instance
824
+
825
+ @transition.perform
826
+ assert_equal [[@record, @transition]], instance.notifications
827
+ end
828
+
829
+ def test_should_call_namespaced_after_event_method
830
+ observer = new_observer(@model) do
831
+ def after_enable_alarm(*args)
832
+ notifications << args
833
+ end
834
+ end
835
+ instance = observer.instance
836
+
837
+ @transition.perform
838
+ assert_equal [[@record, @transition]], instance.notifications
839
+ end
840
+ end
841
+
842
+ class MachineWithFailureCallbacksTest < BaseTestCase
843
+ def setup
844
+ @model = new_model { include ActiveModel::Observing }
845
+ @machine = StateMachine::Machine.new(@model)
846
+ @machine.state :parked, :idling
847
+ @machine.event :ignite
848
+ @record = @model.new(:state => 'parked')
849
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
850
+
851
+ @notifications = []
852
+
853
+ # Create callbacks
854
+ @machine.before_transition {false}
855
+ @machine.after_failure {@notifications << :callback_after_failure}
856
+
857
+ # Create observer callbacks
858
+ observer = new_observer(@model) do
859
+ def after_failure_to_ignite(*args)
860
+ notifications << :observer_after_failure_ignite
861
+ end
862
+
863
+ def after_failure_to_transition(*args)
864
+ notifications << :observer_after_failure_transition
865
+ end
866
+ end
867
+ instance = observer.instance
868
+ instance.notifications = @notifications
869
+
870
+ @transition.perform
871
+ end
872
+
873
+ def test_should_invoke_callbacks_in_specific_order
874
+ expected = [
875
+ :callback_after_failure,
876
+ :observer_after_failure_ignite,
877
+ :observer_after_failure_transition
878
+ ]
879
+
880
+ assert_equal expected, @notifications
881
+ end
882
+ end
883
+
884
+ class MachineWithMixedCallbacksTest < BaseTestCase
885
+ def setup
886
+ @model = new_model { include ActiveModel::Observing }
887
+ @machine = StateMachine::Machine.new(@model)
888
+ @machine.state :parked, :idling
889
+ @machine.event :ignite
890
+ @record = @model.new(:state => 'parked')
891
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
892
+
893
+ @notifications = []
894
+
895
+ # Create callbacks
896
+ @machine.before_transition {@notifications << :callback_before_transition}
897
+ @machine.after_transition {@notifications << :callback_after_transition}
898
+ @machine.around_transition {|block| @notifications << :callback_around_before_transition; block.call; @notifications << :callback_around_after_transition}
899
+
900
+ # Create observer callbacks
901
+ observer = new_observer(@model) do
902
+ def before_ignite(*args)
903
+ notifications << :observer_before_ignite
904
+ end
905
+
906
+ def before_transition(*args)
907
+ notifications << :observer_before_transition
908
+ end
909
+
910
+ def after_ignite(*args)
911
+ notifications << :observer_after_ignite
912
+ end
913
+
914
+ def after_transition(*args)
915
+ notifications << :observer_after_transition
916
+ end
917
+ end
918
+ instance = observer.instance
919
+ instance.notifications = @notifications
920
+
921
+ @transition.perform
922
+ end
923
+
924
+ def test_should_invoke_callbacks_in_specific_order
925
+ expected = [
926
+ :callback_before_transition,
927
+ :callback_around_before_transition,
928
+ :observer_before_ignite,
929
+ :observer_before_transition,
930
+ :callback_around_after_transition,
931
+ :callback_after_transition,
932
+ :observer_after_ignite,
933
+ :observer_after_transition
934
+ ]
935
+
936
+ assert_equal expected, @notifications
937
+ end
938
+ end
939
+
940
+ class MachineWithInternationalizationTest < BaseTestCase
941
+ def setup
942
+ I18n.backend = I18n::Backend::Simple.new
943
+
944
+ # Initialize the backend
945
+ I18n.backend.translate(:en, 'activemodel.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
946
+
947
+ @model = new_model { include ActiveModel::Validations }
948
+ end
949
+
950
+ def test_should_use_defaults
951
+ I18n.backend.store_translations(:en, {
952
+ :activemodel => {:errors => {:messages => {:invalid_transition => 'cannot %{event}'}}}
953
+ })
954
+
955
+ machine = StateMachine::Machine.new(@model, :action => :save)
956
+ machine.state :parked, :idling
957
+ machine.event :ignite
958
+
959
+ record = @model.new(:state => 'idling')
960
+
961
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
962
+ assert_equal ['State cannot ignite'], record.errors.full_messages
963
+ end
964
+
965
+ def test_should_allow_customized_error_key
966
+ I18n.backend.store_translations(:en, {
967
+ :activemodel => {:errors => {:messages => {:bad_transition => 'cannot %{event}'}}}
968
+ })
969
+
970
+ machine = StateMachine::Machine.new(@model, :action => :save, :messages => {:invalid_transition => :bad_transition})
971
+ machine.state :parked, :idling
972
+
973
+ record = @model.new
974
+ record.state = 'idling'
975
+
976
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
977
+ assert_equal ['State cannot ignite'], record.errors.full_messages
978
+ end
979
+
980
+ def test_should_allow_customized_error_string
981
+ machine = StateMachine::Machine.new(@model, :action => :save, :messages => {:invalid_transition => 'cannot %{event}'})
982
+ machine.state :parked, :idling
983
+
984
+ record = @model.new(:state => 'idling')
985
+
986
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
987
+ assert_equal ['State cannot ignite'], record.errors.full_messages
988
+ end
989
+
990
+ def test_should_allow_customized_state_key_scoped_to_class_and_machine
991
+ I18n.backend.store_translations(:en, {
992
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:states => {:parked => 'shutdown'}}}}}
993
+ })
994
+
995
+ machine = StateMachine::Machine.new(@model)
996
+ machine.state :parked
997
+
998
+ assert_equal 'shutdown', machine.state(:parked).human_name
999
+ end
1000
+
1001
+ def test_should_allow_customized_state_key_scoped_to_machine
1002
+ I18n.backend.store_translations(:en, {
1003
+ :activemodel => {:state_machines => {:state => {:states => {:parked => 'shutdown'}}}}
1004
+ })
1005
+
1006
+ machine = StateMachine::Machine.new(@model)
1007
+ machine.state :parked
1008
+
1009
+ assert_equal 'shutdown', machine.state(:parked).human_name
1010
+ end
1011
+
1012
+ def test_should_allow_customized_state_key_unscoped
1013
+ I18n.backend.store_translations(:en, {
1014
+ :activemodel => {:state_machines => {:states => {:parked => 'shutdown'}}}
1015
+ })
1016
+
1017
+ machine = StateMachine::Machine.new(@model)
1018
+ machine.state :parked
1019
+
1020
+ assert_equal 'shutdown', machine.state(:parked).human_name
1021
+ end
1022
+
1023
+ def test_should_allow_customized_event_key_scoped_to_class_and_machine
1024
+ I18n.backend.store_translations(:en, {
1025
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:events => {:park => 'stop'}}}}}
1026
+ })
1027
+
1028
+ machine = StateMachine::Machine.new(@model)
1029
+ machine.event :park
1030
+
1031
+ assert_equal 'stop', machine.event(:park).human_name
1032
+ end
1033
+
1034
+ def test_should_allow_customized_event_key_scoped_to_machine
1035
+ I18n.backend.store_translations(:en, {
1036
+ :activemodel => {:state_machines => {:state => {:events => {:park => 'stop'}}}}
1037
+ })
1038
+
1039
+ machine = StateMachine::Machine.new(@model)
1040
+ machine.event :park
1041
+
1042
+ assert_equal 'stop', machine.event(:park).human_name
1043
+ end
1044
+
1045
+ def test_should_allow_customized_event_key_unscoped
1046
+ I18n.backend.store_translations(:en, {
1047
+ :activemodel => {:state_machines => {:events => {:park => 'stop'}}}
1048
+ })
1049
+
1050
+ machine = StateMachine::Machine.new(@model)
1051
+ machine.event :park
1052
+
1053
+ assert_equal 'stop', machine.event(:park).human_name
1054
+ end
1055
+
1056
+ def test_should_only_add_locale_once_in_load_path
1057
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_model/locale\.rb$}}.length
1058
+
1059
+ # Create another ActiveModel model that will triger the i18n feature
1060
+ new_model
1061
+
1062
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_model/locale\.rb$}}.length
1063
+ end
1064
+
1065
+ def test_should_add_locale_to_beginning_of_load_path
1066
+ @original_load_path = I18n.load_path
1067
+ I18n.backend = I18n::Backend::Simple.new
1068
+
1069
+ app_locale = File.dirname(__FILE__) + '/../../files/en.yml'
1070
+ default_locale = File.dirname(__FILE__) + '/../../../lib/state_machine/integrations/active_model/locale.rb'
1071
+ I18n.load_path = [app_locale]
1072
+
1073
+ StateMachine::Machine.new(@model)
1074
+
1075
+ assert_equal [default_locale, app_locale].map {|path| File.expand_path(path)}, I18n.load_path.map {|path| File.expand_path(path)}
1076
+ ensure
1077
+ I18n.load_path = @original_load_path
1078
+ end
1079
+
1080
+ def test_should_prefer_other_locales_first
1081
+ @original_load_path = I18n.load_path
1082
+ I18n.backend = I18n::Backend::Simple.new
1083
+ I18n.load_path = [File.dirname(__FILE__) + '/../../files/en.yml']
1084
+
1085
+ machine = StateMachine::Machine.new(@model)
1086
+ machine.state :parked, :idling
1087
+ machine.event :ignite
1088
+
1089
+ record = @model.new(:state => 'idling')
1090
+
1091
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
1092
+ assert_equal ['State cannot ignite'], record.errors.full_messages
1093
+ ensure
1094
+ I18n.load_path = @original_load_path
1095
+ end
1096
+ end
1097
+ end