hsume2-state_machine 1.0.1

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