verborghs-state_machine 0.9.4

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 (89) hide show
  1. data/CHANGELOG.rdoc +360 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +635 -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/assertions.rb +36 -0
  28. data/lib/state_machine/callback.rb +241 -0
  29. data/lib/state_machine/condition_proxy.rb +106 -0
  30. data/lib/state_machine/eval_helpers.rb +83 -0
  31. data/lib/state_machine/event.rb +267 -0
  32. data/lib/state_machine/event_collection.rb +122 -0
  33. data/lib/state_machine/extensions.rb +149 -0
  34. data/lib/state_machine/guard.rb +230 -0
  35. data/lib/state_machine/initializers/merb.rb +1 -0
  36. data/lib/state_machine/initializers/rails.rb +5 -0
  37. data/lib/state_machine/initializers.rb +4 -0
  38. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  40. data/lib/state_machine/integrations/active_model.rb +445 -0
  41. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  42. data/lib/state_machine/integrations/active_record.rb +522 -0
  43. data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
  44. data/lib/state_machine/integrations/data_mapper.rb +379 -0
  45. data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
  46. data/lib/state_machine/integrations/sequel.rb +356 -0
  47. data/lib/state_machine/integrations.rb +83 -0
  48. data/lib/state_machine/machine.rb +1645 -0
  49. data/lib/state_machine/machine_collection.rb +64 -0
  50. data/lib/state_machine/matcher.rb +123 -0
  51. data/lib/state_machine/matcher_helpers.rb +54 -0
  52. data/lib/state_machine/node_collection.rb +152 -0
  53. data/lib/state_machine/state.rb +260 -0
  54. data/lib/state_machine/state_collection.rb +112 -0
  55. data/lib/state_machine/transition.rb +399 -0
  56. data/lib/state_machine/transition_collection.rb +244 -0
  57. data/lib/state_machine.rb +421 -0
  58. data/lib/tasks/state_machine.rake +1 -0
  59. data/lib/tasks/state_machine.rb +27 -0
  60. data/test/files/en.yml +9 -0
  61. data/test/files/switch.rb +11 -0
  62. data/test/functional/state_machine_test.rb +980 -0
  63. data/test/test_helper.rb +4 -0
  64. data/test/unit/assertions_test.rb +40 -0
  65. data/test/unit/callback_test.rb +728 -0
  66. data/test/unit/condition_proxy_test.rb +328 -0
  67. data/test/unit/eval_helpers_test.rb +222 -0
  68. data/test/unit/event_collection_test.rb +324 -0
  69. data/test/unit/event_test.rb +795 -0
  70. data/test/unit/guard_test.rb +909 -0
  71. data/test/unit/integrations/active_model_test.rb +956 -0
  72. data/test/unit/integrations/active_record_test.rb +1918 -0
  73. data/test/unit/integrations/data_mapper_test.rb +1814 -0
  74. data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
  75. data/test/unit/integrations/sequel_test.rb +1492 -0
  76. data/test/unit/integrations_test.rb +50 -0
  77. data/test/unit/invalid_event_test.rb +7 -0
  78. data/test/unit/invalid_transition_test.rb +7 -0
  79. data/test/unit/machine_collection_test.rb +565 -0
  80. data/test/unit/machine_test.rb +2349 -0
  81. data/test/unit/matcher_helpers_test.rb +37 -0
  82. data/test/unit/matcher_test.rb +155 -0
  83. data/test/unit/node_collection_test.rb +207 -0
  84. data/test/unit/state_collection_test.rb +280 -0
  85. data/test/unit/state_machine_test.rb +31 -0
  86. data/test/unit/state_test.rb +848 -0
  87. data/test/unit/transition_collection_test.rb +2098 -0
  88. data/test/unit/transition_test.rb +1384 -0
  89. metadata +176 -0
@@ -0,0 +1,1918 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ # Load library
4
+ require 'rubygems'
5
+
6
+ gem 'activerecord', ENV['VERSION'] ? "=#{ENV['VERSION']}" : '>=2.0.0'
7
+ require 'active_record'
8
+
9
+ FIXTURES_ROOT = File.dirname(__FILE__) + '/../../fixtures/'
10
+
11
+ # Load TestCase helpers
12
+ require 'active_support/test_case'
13
+ require 'active_record/fixtures'
14
+
15
+ begin
16
+ require 'active_record/test_case'
17
+ rescue LoadError
18
+ class ActiveRecord::TestCase < ActiveSupport::TestCase
19
+ self.fixture_path = FIXTURES_ROOT
20
+ self.use_instantiated_fixtures = false
21
+ self.use_transactional_fixtures = true
22
+ end
23
+ end
24
+
25
+ # Establish database connection
26
+ ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3', 'database' => ':memory:'})
27
+ ActiveRecord::Base.logger = Logger.new("#{File.dirname(__FILE__)}/../../active_record.log")
28
+
29
+ module ActiveRecordTest
30
+ class BaseTestCase < ActiveRecord::TestCase
31
+ def default_test
32
+ end
33
+
34
+ protected
35
+ # Creates a new ActiveRecord model (and the associated table)
36
+ def new_model(create_table = :foo, &block)
37
+ table_name = create_table || :foo
38
+
39
+ model = Class.new(ActiveRecord::Base) do
40
+ connection.create_table(table_name, :force => true) {|t| t.string(:state)} if create_table
41
+ set_table_name(table_name.to_s)
42
+
43
+ def self.name; "ActiveRecordTest::#{table_name.capitalize}"; end
44
+ end
45
+ model.class_eval(&block) if block_given?
46
+ model
47
+ end
48
+
49
+ # Creates a new ActiveRecord observer
50
+ def new_observer(model, &block)
51
+ observer = Class.new(ActiveRecord::Observer) do
52
+ attr_accessor :notifications
53
+
54
+ def initialize
55
+ super
56
+ @notifications = []
57
+ end
58
+ end
59
+
60
+ (class << observer; self; end).class_eval do
61
+ define_method(:name) do
62
+ "#{model.name}Observer"
63
+ end
64
+ end
65
+
66
+ observer.observe(model)
67
+ observer.class_eval(&block) if block_given?
68
+ observer
69
+ end
70
+ end
71
+
72
+ class IntegrationTest < BaseTestCase
73
+ def test_should_match_if_class_inherits_from_active_record
74
+ assert StateMachine::Integrations::ActiveRecord.matches?(new_model)
75
+ end
76
+
77
+ def test_should_not_match_if_class_does_not_inherit_from_active_record
78
+ assert !StateMachine::Integrations::ActiveRecord.matches?(Class.new)
79
+ end
80
+
81
+ def test_should_have_defaults
82
+ assert_equal e = {:action => :save}, StateMachine::Integrations::ActiveRecord.defaults
83
+ end
84
+ end
85
+
86
+ class MachineWithoutDatabaseTest < BaseTestCase
87
+ def setup
88
+ @model = new_model(false) do
89
+ # Simulate the database not being available entirely
90
+ def self.connection
91
+ raise ActiveRecord::ConnectionNotEstablished
92
+ end
93
+ end
94
+ end
95
+
96
+ def test_should_allow_machine_creation
97
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
98
+ end
99
+ end
100
+
101
+ class MachineUnmigratedTest < BaseTestCase
102
+ def setup
103
+ @model = new_model(false)
104
+
105
+ # Drop the table so that it definitely doesn't exist
106
+ @model.connection.drop_table(:foo) if @model.table_exists?
107
+ end
108
+
109
+ def test_should_allow_machine_creation
110
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
111
+ end
112
+ end
113
+
114
+ class MachineByDefaultTest < BaseTestCase
115
+ def setup
116
+ @model = new_model
117
+ @machine = StateMachine::Machine.new(@model)
118
+ end
119
+
120
+ def test_should_use_save_as_action
121
+ assert_equal :save, @machine.action
122
+ end
123
+
124
+ def test_should_use_transactions
125
+ assert_equal true, @machine.use_transactions
126
+ end
127
+
128
+ def test_should_create_notifier_before_callback
129
+ assert_equal 1, @machine.callbacks[:before].size
130
+ end
131
+
132
+ def test_should_create_notifier_after_callback
133
+ assert_equal 1, @machine.callbacks[:after].size
134
+ end
135
+ end
136
+
137
+ class MachineWithStatesTest < BaseTestCase
138
+ def setup
139
+ @model = new_model
140
+ @machine = StateMachine::Machine.new(@model)
141
+ @machine.state :first_gear
142
+ end
143
+
144
+ def test_should_humanize_name
145
+ assert_equal 'first gear', @machine.state(:first_gear).human_name
146
+ end
147
+ end
148
+
149
+ class MachineWithStaticInitialStateTest < BaseTestCase
150
+ def setup
151
+ @model = new_model do
152
+ attr_accessor :value
153
+ end
154
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
155
+ end
156
+
157
+ def test_should_set_initial_state_on_created_object
158
+ record = @model.new
159
+ assert_equal 'parked', record.state
160
+ end
161
+
162
+ def test_should_set_initial_state_with_nil_attributes
163
+ record = @model.new(nil)
164
+ assert_equal 'parked', record.state
165
+ end
166
+
167
+ def test_should_still_set_attributes
168
+ record = @model.new(:value => 1)
169
+ assert_equal 1, record.value
170
+ end
171
+
172
+ def test_should_still_allow_initialize_blocks
173
+ block_args = nil
174
+ record = @model.new do |*args|
175
+ block_args = args
176
+ end
177
+
178
+ assert_equal [record], block_args
179
+ end
180
+
181
+ def test_should_set_attributes_prior_to_after_initialize_hook
182
+ state = nil
183
+ @model.class_eval {define_method(:after_initialize) {}} if ::ActiveRecord::VERSION::MAJOR <= 2
184
+ @model.after_initialize do |record|
185
+ state = record.state
186
+ end
187
+ @model.new
188
+ assert_equal 'parked', state
189
+ end
190
+
191
+ def test_should_set_initial_state_before_setting_attributes
192
+ @model.class_eval do
193
+ attr_accessor :state_during_setter
194
+
195
+ define_method(:value=) do |value|
196
+ self.state_during_setter = state
197
+ end
198
+ end
199
+
200
+ record = @model.new(:value => 1)
201
+ assert_equal 'parked', record.state_during_setter
202
+ end
203
+
204
+ def test_should_not_set_initial_state_after_already_initialized
205
+ record = @model.new(:value => 1)
206
+ assert_equal 'parked', record.state
207
+
208
+ record.state = 'idling'
209
+ record.attributes = {}
210
+ assert_equal 'idling', record.state
211
+ end
212
+
213
+ def test_should_use_stored_values_when_loading_from_database
214
+ @machine.state :idling
215
+
216
+ record = @model.find(@model.create(:state => 'idling').id)
217
+ assert_equal 'idling', record.state
218
+ end
219
+
220
+ def test_should_use_stored_values_when_loading_from_database_with_nil_state
221
+ @machine.state nil
222
+
223
+ record = @model.find(@model.create(:state => nil).id)
224
+ assert_nil record.state
225
+ end
226
+ end
227
+
228
+ class MachineWithDynamicInitialStateTest < BaseTestCase
229
+ def setup
230
+ @model = new_model do
231
+ attr_accessor :value
232
+ end
233
+ @machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked})
234
+ @machine.state :parked
235
+ end
236
+
237
+ def test_should_set_initial_state_on_created_object
238
+ record = @model.new
239
+ assert_equal 'parked', record.state
240
+ end
241
+
242
+ def test_should_still_set_attributes
243
+ record = @model.new(:value => 1)
244
+ assert_equal 1, record.value
245
+ end
246
+
247
+ def test_should_still_allow_initialize_blocks
248
+ block_args = nil
249
+ record = @model.new do |*args|
250
+ block_args = args
251
+ end
252
+
253
+ assert_equal [record], block_args
254
+ end
255
+
256
+ def test_should_set_attributes_prior_to_after_initialize_hook
257
+ state = nil
258
+ @model.class_eval {define_method(:after_initialize) {}} if ::ActiveRecord::VERSION::MAJOR <= 2
259
+ @model.after_initialize do |record|
260
+ state = record.state
261
+ end
262
+ @model.new
263
+ assert_equal 'parked', state
264
+ end
265
+
266
+ def test_should_set_initial_state_after_setting_attributes
267
+ @model.class_eval do
268
+ attr_accessor :state_during_setter
269
+
270
+ define_method(:value=) do |value|
271
+ self.state_during_setter = state || 'nil'
272
+ end
273
+ end
274
+
275
+ record = @model.new(:value => 1)
276
+ assert_equal 'nil', record.state_during_setter
277
+ end
278
+
279
+ def test_should_not_set_initial_state_after_already_initialized
280
+ record = @model.new(:value => 1)
281
+ assert_equal 'parked', record.state
282
+
283
+ record.state = 'idling'
284
+ record.attributes = {}
285
+ assert_equal 'idling', record.state
286
+ end
287
+
288
+ def test_should_use_stored_values_when_loading_from_database
289
+ @machine.state :idling
290
+
291
+ record = @model.find(@model.create(:state => 'idling').id)
292
+ assert_equal 'idling', record.state
293
+ end
294
+
295
+ def test_should_use_stored_values_when_loading_from_database_with_nil_state
296
+ @machine.state nil
297
+
298
+ record = @model.find(@model.create(:state => nil).id)
299
+ assert_nil record.state
300
+ end
301
+ end
302
+
303
+ class MachineWithEventsTest < BaseTestCase
304
+ def setup
305
+ @model = new_model
306
+ @machine = StateMachine::Machine.new(@model)
307
+ @machine.event :shift_up
308
+ end
309
+
310
+ def test_should_humanize_name
311
+ assert_equal 'shift up', @machine.event(:shift_up).human_name
312
+ end
313
+ end
314
+
315
+ class MachineWithColumnDefaultTest < BaseTestCase
316
+ def setup
317
+ @model = new_model do
318
+ connection.add_column :foo, :status, :string, :default => 'idling'
319
+ end
320
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
321
+ @record = @model.new
322
+ end
323
+
324
+ def test_should_use_machine_default
325
+ assert_equal 'parked', @record.status
326
+ end
327
+ end
328
+
329
+ class MachineWithConflictingPredicateTest < BaseTestCase
330
+ def setup
331
+ @model = new_model do
332
+ def state?(*args)
333
+ true
334
+ end
335
+ end
336
+
337
+ @machine = StateMachine::Machine.new(@model)
338
+ @record = @model.new
339
+ end
340
+
341
+ def test_should_not_define_attribute_predicate
342
+ assert @record.state?
343
+ end
344
+ end
345
+
346
+ class MachineWithColumnStateAttributeTest < BaseTestCase
347
+ def setup
348
+ @model = new_model
349
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
350
+ @machine.other_states(:idling)
351
+
352
+ @record = @model.new
353
+ end
354
+
355
+ def test_should_not_override_the_column_reader
356
+ @record[:state] = 'parked'
357
+ assert_equal 'parked', @record.state
358
+ end
359
+
360
+ def test_should_not_override_the_column_writer
361
+ @record.state = 'parked'
362
+ assert_equal 'parked', @record[:state]
363
+ end
364
+
365
+ def test_should_have_an_attribute_predicate
366
+ assert @record.respond_to?(:state?)
367
+ end
368
+
369
+ def test_should_test_for_existence_on_predicate_without_parameters
370
+ assert @record.state?
371
+
372
+ @record.state = nil
373
+ assert !@record.state?
374
+ end
375
+
376
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
377
+ assert !@record.state?(:idling)
378
+ end
379
+
380
+ def test_should_return_true_for_predicate_if_matches_current_value
381
+ assert @record.state?(:parked)
382
+ end
383
+
384
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
385
+ assert_raise(IndexError) { @record.state?(:invalid) }
386
+ end
387
+ end
388
+
389
+ class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
390
+ def setup
391
+ @model = new_model do
392
+ def initialize
393
+ # Skip attribute initialization
394
+ @initialized_state_machines = true
395
+ super
396
+ end
397
+ end
398
+
399
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
400
+ @machine.other_states(:idling)
401
+ @record = @model.new
402
+ end
403
+
404
+ def test_should_not_define_a_reader_attribute_for_the_attribute
405
+ assert !@record.respond_to?(:status)
406
+ end
407
+
408
+ def test_should_not_define_a_writer_attribute_for_the_attribute
409
+ assert !@record.respond_to?(:status=)
410
+ end
411
+
412
+ def test_should_define_an_attribute_predicate
413
+ assert @record.respond_to?(:status?)
414
+ end
415
+
416
+ def test_should_raise_exception_on_predicate_without_parameters
417
+ old_verbose, $VERBOSE = $VERBOSE, nil
418
+ assert_raise(NoMethodError) { @record.status? }
419
+ ensure
420
+ $VERBOSE = old_verbose
421
+ end
422
+ end
423
+
424
+ class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
425
+ def setup
426
+ @model = new_model do
427
+ attr_accessor :status
428
+ end
429
+
430
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
431
+ @machine.other_states(:idling)
432
+ @record = @model.new
433
+ end
434
+
435
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
436
+ assert !@record.status?(:idling)
437
+ end
438
+
439
+ def test_should_return_true_for_predicate_if_matches_current_value
440
+ assert @record.status?(:parked)
441
+ end
442
+
443
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
444
+ assert_raise(IndexError) { @record.status?(:invalid) }
445
+ end
446
+
447
+ def test_should_set_initial_state_on_created_object
448
+ assert_equal 'parked', @record.status
449
+ end
450
+ end
451
+
452
+ class MachineWithAliasedAttributeTest < BaseTestCase
453
+ def setup
454
+ @model = new_model do
455
+ alias_attribute :vehicle_status, :state
456
+ end
457
+
458
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :vehicle_status)
459
+ @machine.state :parked
460
+
461
+ @record = @model.new
462
+ end
463
+
464
+ def test_should_check_custom_attribute_for_predicate
465
+ @record.vehicle_status = nil
466
+ assert !@record.status?(:parked)
467
+
468
+ @record.vehicle_status = 'parked'
469
+ assert @record.status?(:parked)
470
+ end
471
+ end
472
+
473
+ class MachineWithInitializedStateTest < BaseTestCase
474
+ def setup
475
+ @model = new_model
476
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
477
+ @machine.state nil, :idling
478
+ end
479
+
480
+ def test_should_allow_nil_initial_state_when_static
481
+ record = @model.new(:state => nil)
482
+ assert_nil record.state
483
+ end
484
+
485
+ def test_should_allow_nil_initial_state_when_dynamic
486
+ @machine.initial_state = lambda {:parked}
487
+ record = @model.new(:state => nil)
488
+ assert_nil record.state
489
+ end
490
+
491
+ def test_should_allow_different_initial_state_when_static
492
+ record = @model.new(:state => 'idling')
493
+ assert_equal 'idling', record.state
494
+ end
495
+
496
+ def test_should_allow_different_initial_state_when_dynamic
497
+ @machine.initial_state = lambda {:parked}
498
+ record = @model.new(:state => 'idling')
499
+ assert_equal 'idling', record.state
500
+ end
501
+
502
+ def test_should_use_default_state_if_protected
503
+ @model.class_eval do
504
+ attr_protected :state
505
+ end
506
+
507
+ record = @model.new(:state => 'idling')
508
+ assert_equal 'parked', record.state
509
+ end
510
+ end
511
+
512
+ class MachineWithLoopbackTest < BaseTestCase
513
+ def setup
514
+ @model = new_model do
515
+ connection.add_column :foo, :updated_at, :datetime
516
+ end
517
+
518
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
519
+ @machine.event :park
520
+
521
+ @record = @model.create(:updated_at => Time.now - 1)
522
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
523
+
524
+ @timestamp = @record.updated_at
525
+ @transition.perform
526
+ end
527
+
528
+ def test_should_update_record
529
+ assert_not_equal @timestamp, @record.updated_at
530
+ end
531
+ end
532
+
533
+ if ActiveRecord.const_defined?(:Dirty) || ActiveRecord::AttributeMethods.const_defined?(:Dirty)
534
+ class MachineWithDirtyAttributesTest < BaseTestCase
535
+ def setup
536
+ @model = new_model
537
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
538
+ @machine.event :ignite
539
+ @machine.state :idling
540
+
541
+ @record = @model.create
542
+
543
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
544
+ @transition.perform(false)
545
+ end
546
+
547
+ def test_should_include_state_in_changed_attributes
548
+ assert_equal %w(state), @record.changed
549
+ end
550
+
551
+ def test_should_track_attribute_change
552
+ assert_equal %w(parked idling), @record.changes['state']
553
+ end
554
+
555
+ def test_should_not_reset_changes_on_multiple_transitions
556
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
557
+ transition.perform(false)
558
+
559
+ assert_equal %w(parked idling), @record.changes['state']
560
+ end
561
+
562
+ def test_should_not_have_changes_when_loaded_from_database
563
+ record = @model.find(@record.id)
564
+ assert !record.changed?
565
+ end
566
+ end
567
+
568
+ class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
569
+ def setup
570
+ @model = new_model
571
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
572
+ @machine.event :park
573
+
574
+ @record = @model.create
575
+
576
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
577
+ @transition.perform(false)
578
+ end
579
+
580
+ def test_should_include_state_in_changed_attributes
581
+ assert_equal %w(state), @record.changed
582
+ end
583
+
584
+ def test_should_track_attribute_changes
585
+ assert_equal %w(parked parked), @record.changes['state']
586
+ end
587
+ end
588
+
589
+ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
590
+ def setup
591
+ @model = new_model do
592
+ connection.add_column :foo, :status, :string, :default => 'idling'
593
+ end
594
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
595
+ @machine.event :ignite
596
+ @machine.state :idling
597
+
598
+ @record = @model.create
599
+
600
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
601
+ @transition.perform(false)
602
+ end
603
+
604
+ def test_should_include_state_in_changed_attributes
605
+ assert_equal %w(status), @record.changed
606
+ end
607
+
608
+ def test_should_track_attribute_change
609
+ assert_equal %w(parked idling), @record.changes['status']
610
+ end
611
+
612
+ def test_should_not_reset_changes_on_multiple_transitions
613
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
614
+ transition.perform(false)
615
+
616
+ assert_equal %w(parked idling), @record.changes['status']
617
+ end
618
+ end
619
+
620
+ class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
621
+ def setup
622
+ @model = new_model do
623
+ connection.add_column :foo, :status, :string, :default => 'idling'
624
+ end
625
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
626
+ @machine.event :park
627
+
628
+ @record = @model.create
629
+
630
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
631
+ @transition.perform(false)
632
+ end
633
+
634
+ def test_should_include_state_in_changed_attributes
635
+ assert_equal %w(status), @record.changed
636
+ end
637
+
638
+ def test_should_track_attribute_changes
639
+ assert_equal %w(parked parked), @record.changes['status']
640
+ end
641
+ end
642
+ else
643
+ $stderr.puts 'Skipping ActiveRecord Dirty tests. `gem install active_record` >= v2.1.0 and try again.'
644
+ end
645
+
646
+ class MachineWithoutTransactionsTest < BaseTestCase
647
+ def setup
648
+ @model = new_model
649
+ @machine = StateMachine::Machine.new(@model, :use_transactions => false)
650
+ end
651
+
652
+ def test_should_not_rollback_transaction_if_false
653
+ @machine.within_transaction(@model.new) do
654
+ @model.create
655
+ false
656
+ end
657
+
658
+ assert_equal 1, @model.count
659
+ end
660
+
661
+ def test_should_not_rollback_transaction_if_true
662
+ @machine.within_transaction(@model.new) do
663
+ @model.create
664
+ true
665
+ end
666
+
667
+ assert_equal 1, @model.count
668
+ end
669
+ end
670
+
671
+ class MachineWithTransactionsTest < BaseTestCase
672
+ def setup
673
+ @model = new_model
674
+ @machine = StateMachine::Machine.new(@model, :use_transactions => true)
675
+ end
676
+
677
+ def test_should_rollback_transaction_if_false
678
+ @machine.within_transaction(@model.new) do
679
+ @model.create
680
+ false
681
+ end
682
+
683
+ assert_equal 0, @model.count
684
+ end
685
+
686
+ def test_should_not_rollback_transaction_if_true
687
+ @machine.within_transaction(@model.new) do
688
+ @model.create
689
+ true
690
+ end
691
+
692
+ assert_equal 1, @model.count
693
+ end
694
+ end
695
+
696
+ class MachineWithCallbacksTest < BaseTestCase
697
+ def setup
698
+ @model = new_model
699
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
700
+ @machine.other_states :idling
701
+ @machine.event :ignite
702
+
703
+ @record = @model.new(:state => 'parked')
704
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
705
+ end
706
+
707
+ def test_should_run_before_callbacks
708
+ called = false
709
+ @machine.before_transition {called = true}
710
+
711
+ @transition.perform
712
+ assert called
713
+ end
714
+
715
+ def test_should_pass_record_to_before_callbacks_with_one_argument
716
+ record = nil
717
+ @machine.before_transition {|arg| record = arg}
718
+
719
+ @transition.perform
720
+ assert_equal @record, record
721
+ end
722
+
723
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
724
+ callback_args = nil
725
+ @machine.before_transition {|*args| callback_args = args}
726
+
727
+ @transition.perform
728
+ assert_equal [@record, @transition], callback_args
729
+ end
730
+
731
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
732
+ context = nil
733
+ @machine.before_transition {context = self}
734
+
735
+ @transition.perform
736
+ assert_equal self, context
737
+ end
738
+
739
+ def test_should_run_after_callbacks
740
+ called = false
741
+ @machine.after_transition {called = true}
742
+
743
+ @transition.perform
744
+ assert called
745
+ end
746
+
747
+ def test_should_pass_record_to_after_callbacks_with_one_argument
748
+ record = nil
749
+ @machine.after_transition {|arg| record = arg}
750
+
751
+ @transition.perform
752
+ assert_equal @record, record
753
+ end
754
+
755
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
756
+ callback_args = nil
757
+ @machine.after_transition {|*args| callback_args = args}
758
+
759
+ @transition.perform
760
+ assert_equal [@record, @transition], callback_args
761
+ end
762
+
763
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
764
+ context = nil
765
+ @machine.after_transition {context = self}
766
+
767
+ @transition.perform
768
+ assert_equal self, context
769
+ end
770
+
771
+ def test_should_run_around_callbacks
772
+ before_called = false
773
+ after_called = false
774
+ @machine.around_transition {|block| before_called = true; block.call; after_called = true}
775
+
776
+ @transition.perform
777
+ assert before_called
778
+ assert after_called
779
+ end
780
+
781
+ def test_should_include_transition_states_in_known_states
782
+ @machine.before_transition :to => :first_gear, :do => lambda {}
783
+
784
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
785
+ end
786
+
787
+ def test_should_allow_symbolic_callbacks
788
+ callback_args = nil
789
+
790
+ klass = class << @record; self; end
791
+ klass.send(:define_method, :after_ignite) do |*args|
792
+ callback_args = args
793
+ end
794
+
795
+ @machine.before_transition(:after_ignite)
796
+
797
+ @transition.perform
798
+ assert_equal [@transition], callback_args
799
+ end
800
+
801
+ def test_should_allow_string_callbacks
802
+ class << @record
803
+ attr_reader :callback_result
804
+ end
805
+
806
+ @machine.before_transition('@callback_result = [1, 2, 3]')
807
+ @transition.perform
808
+
809
+ assert_equal [1, 2, 3], @record.callback_result
810
+ end
811
+ end
812
+
813
+ class MachineWithFailedBeforeCallbacksTest < BaseTestCase
814
+ def setup
815
+ @callbacks = []
816
+
817
+ @model = new_model
818
+ @machine = StateMachine::Machine.new(@model)
819
+ @machine.state :parked, :idling
820
+ @machine.event :ignite
821
+ @machine.before_transition {@callbacks << :before_1; false}
822
+ @machine.before_transition {@callbacks << :before_2}
823
+ @machine.after_transition {@callbacks << :after}
824
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
825
+
826
+ @record = @model.new(:state => 'parked')
827
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
828
+ @result = @transition.perform
829
+ end
830
+
831
+ def test_should_not_be_successful
832
+ assert !@result
833
+ end
834
+
835
+ def test_should_not_change_current_state
836
+ assert_equal 'parked', @record.state
837
+ end
838
+
839
+ def test_should_not_run_action
840
+ assert @record.new_record?
841
+ end
842
+
843
+ def test_should_not_run_further_callbacks
844
+ assert_equal [:before_1], @callbacks
845
+ end
846
+ end
847
+
848
+ class MachineWithFailedActionTest < BaseTestCase
849
+ def setup
850
+ @model = new_model do
851
+ validates_inclusion_of :state, :in => %w(first_gear)
852
+ end
853
+
854
+ @machine = StateMachine::Machine.new(@model)
855
+ @machine.state :parked, :idling
856
+ @machine.event :ignite
857
+
858
+ @callbacks = []
859
+ @machine.before_transition {@callbacks << :before}
860
+ @machine.after_transition {@callbacks << :after}
861
+ @machine.after_transition(:include_failures => true) {@callbacks << :after_failure}
862
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
863
+ @machine.around_transition(:include_failures => true) do |block|
864
+ @callbacks << :around_before_failure
865
+ block.call
866
+ @callbacks << :around_after_failure
867
+ end
868
+
869
+ @record = @model.new(:state => 'parked')
870
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
871
+ @result = @transition.perform
872
+ end
873
+
874
+ def test_should_not_be_successful
875
+ assert !@result
876
+ end
877
+
878
+ def test_should_not_change_current_state
879
+ assert_equal 'parked', @record.state
880
+ end
881
+
882
+ def test_should_not_save_record
883
+ assert @record.new_record?
884
+ end
885
+
886
+ def test_should_run_before_callbacks_and_after_callbacks_with_failures
887
+ assert_equal [:before, :around_before, :around_before_failure, :around_after_failure, :after_failure], @callbacks
888
+ end
889
+ end
890
+
891
+ class MachineWithFailedAfterCallbacksTest < BaseTestCase
892
+ def setup
893
+ @callbacks = []
894
+
895
+ @model = new_model
896
+ @machine = StateMachine::Machine.new(@model)
897
+ @machine.state :parked, :idling
898
+ @machine.event :ignite
899
+ @machine.after_transition {@callbacks << :after_1; false}
900
+ @machine.after_transition {@callbacks << :after_2}
901
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
902
+
903
+ @record = @model.new(:state => 'parked')
904
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
905
+ @result = @transition.perform
906
+ end
907
+
908
+ def test_should_be_successful
909
+ assert @result
910
+ end
911
+
912
+ def test_should_change_current_state
913
+ assert_equal 'idling', @record.state
914
+ end
915
+
916
+ def test_should_save_record
917
+ assert !@record.new_record?
918
+ end
919
+
920
+ def test_should_not_run_further_after_callbacks
921
+ assert_equal [:around_before, :around_after, :after_1], @callbacks
922
+ end
923
+ end
924
+
925
+ class MachineWithValidationsTest < BaseTestCase
926
+ def setup
927
+ @model = new_model
928
+ @machine = StateMachine::Machine.new(@model)
929
+ @machine.state :parked
930
+
931
+ @record = @model.new
932
+ end
933
+
934
+ def test_should_invalidate_using_errors
935
+ I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
936
+ @record.state = 'parked'
937
+
938
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, 'park']])
939
+ assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
940
+ end
941
+
942
+ def test_should_auto_prefix_custom_attributes_on_invalidation
943
+ @machine.invalidate(@record, :event, :invalid)
944
+
945
+ assert_equal ['State event is invalid'], @record.errors.full_messages
946
+ end
947
+
948
+ def test_should_clear_errors_on_reset
949
+ @record.state = 'parked'
950
+ @record.errors.add(:state, 'is invalid')
951
+
952
+ @machine.reset(@record)
953
+ assert_equal [], @record.errors.full_messages
954
+ end
955
+
956
+ def test_should_be_valid_if_state_is_known
957
+ @record.state = 'parked'
958
+
959
+ assert @record.valid?
960
+ end
961
+
962
+ def test_should_not_be_valid_if_state_is_unknown
963
+ @record.state = 'invalid'
964
+
965
+ assert !@record.valid?
966
+ assert_equal ['State is invalid'], @record.errors.full_messages
967
+ end
968
+ end
969
+
970
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
971
+ def setup
972
+ @model = new_model do
973
+ alias_attribute :status, :state
974
+ end
975
+
976
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
977
+ @machine.state :parked
978
+
979
+ @record = @model.new
980
+ end
981
+
982
+ def test_should_add_validation_errors_to_custom_attribute
983
+ @record.state = 'invalid'
984
+
985
+ assert !@record.valid?
986
+ assert_equal ['State is invalid'], @record.errors.full_messages
987
+
988
+ @record.state = 'parked'
989
+ assert @record.valid?
990
+ end
991
+ end
992
+
993
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
994
+ def setup
995
+ @model = new_model do
996
+ attr_accessor :seatbelt
997
+ end
998
+
999
+ @machine = StateMachine::Machine.new(@model)
1000
+ @machine.state :first_gear, :second_gear do
1001
+ validates_presence_of :seatbelt
1002
+ end
1003
+ @machine.other_states :parked
1004
+ end
1005
+
1006
+ def test_should_be_valid_if_validation_fails_outside_state_scope
1007
+ record = @model.new(:state => 'parked', :seatbelt => nil)
1008
+ assert record.valid?
1009
+ end
1010
+
1011
+ def test_should_be_invalid_if_validation_fails_within_state_scope
1012
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
1013
+ assert !record.valid?
1014
+ end
1015
+
1016
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
1017
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
1018
+ assert record.valid?
1019
+ end
1020
+ end
1021
+
1022
+ class MachineWithEventAttributesOnValidationTest < BaseTestCase
1023
+ def setup
1024
+ @model = new_model
1025
+ @machine = StateMachine::Machine.new(@model)
1026
+ @machine.event :ignite do
1027
+ transition :parked => :idling
1028
+ end
1029
+
1030
+ @record = @model.new
1031
+ @record.state = 'parked'
1032
+ @record.state_event = 'ignite'
1033
+ end
1034
+
1035
+ def test_should_fail_if_event_is_invalid
1036
+ @record.state_event = 'invalid'
1037
+ assert !@record.valid?
1038
+ assert_equal ['State event is invalid'], @record.errors.full_messages
1039
+ end
1040
+
1041
+ def test_should_fail_if_event_has_no_transition
1042
+ @record.state = 'idling'
1043
+ assert !@record.valid?
1044
+ assert_equal ['State event cannot transition when idling'], @record.errors.full_messages
1045
+ end
1046
+
1047
+ def test_should_be_successful_if_event_has_transition
1048
+ assert @record.valid?
1049
+ end
1050
+
1051
+ def test_should_run_before_callbacks
1052
+ ran_callback = false
1053
+ @machine.before_transition { ran_callback = true }
1054
+
1055
+ @record.valid?
1056
+ assert ran_callback
1057
+ end
1058
+
1059
+ def test_should_run_around_callbacks_before_yield
1060
+ ran_callback = false
1061
+ @machine.around_transition {|block| ran_callback = true; block.call }
1062
+
1063
+ @record.valid?
1064
+ assert ran_callback
1065
+ end
1066
+
1067
+ def test_should_persist_new_state
1068
+ @record.valid?
1069
+ assert_equal 'idling', @record.state
1070
+ end
1071
+
1072
+ def test_should_not_run_after_callbacks
1073
+ ran_callback = false
1074
+ @machine.after_transition { ran_callback = true }
1075
+
1076
+ @record.valid?
1077
+ assert !ran_callback
1078
+ end
1079
+
1080
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
1081
+ @model.class_eval do
1082
+ attr_accessor :seatbelt
1083
+ validates_presence_of :seatbelt
1084
+ end
1085
+
1086
+ ran_callback = false
1087
+ @machine.after_transition { ran_callback = true }
1088
+
1089
+ @record.valid?
1090
+ assert !ran_callback
1091
+ end
1092
+
1093
+ def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
1094
+ @model.class_eval do
1095
+ attr_accessor :seatbelt
1096
+ validates_presence_of :seatbelt
1097
+ end
1098
+
1099
+ ran_callback = false
1100
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
1101
+
1102
+ @record.valid?
1103
+ assert ran_callback
1104
+ end
1105
+
1106
+ def test_should_not_run_around_callbacks_after_yield
1107
+ ran_callback = false
1108
+ @machine.around_transition {|block| block.call; ran_callback = true }
1109
+
1110
+ @record.valid?
1111
+ assert !ran_callback
1112
+ end
1113
+
1114
+ def test_should_not_run_around_callbacks_after_yield_with_failures_disabled_if_validation_fails
1115
+ @model.class_eval do
1116
+ attr_accessor :seatbelt
1117
+ validates_presence_of :seatbelt
1118
+ end
1119
+
1120
+ ran_callback = false
1121
+ @machine.around_transition {|block| block.call; ran_callback = true }
1122
+
1123
+ @record.valid?
1124
+ assert !ran_callback
1125
+ end
1126
+
1127
+ def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_validation_fails
1128
+ @model.class_eval do
1129
+ attr_accessor :seatbelt
1130
+ validates_presence_of :seatbelt
1131
+ end
1132
+
1133
+ ran_callback = false
1134
+ @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
1135
+
1136
+ @record.valid?
1137
+ assert ran_callback
1138
+ end
1139
+
1140
+ def test_should_not_run_before_transitions_within_transaction
1141
+ @machine.before_transition { @model.create; raise ActiveRecord::Rollback }
1142
+
1143
+ begin
1144
+ @record.valid?
1145
+ rescue Exception
1146
+ end
1147
+
1148
+ assert_equal 1, @model.count
1149
+ end
1150
+ end
1151
+
1152
+ class MachineWithEventAttributesOnSaveTest < BaseTestCase
1153
+ def setup
1154
+ @model = new_model
1155
+ @machine = StateMachine::Machine.new(@model)
1156
+ @machine.event :ignite do
1157
+ transition :parked => :idling
1158
+ end
1159
+
1160
+ @record = @model.new
1161
+ @record.state = 'parked'
1162
+ @record.state_event = 'ignite'
1163
+ end
1164
+
1165
+ def test_should_fail_if_event_is_invalid
1166
+ @record.state_event = 'invalid'
1167
+ assert_equal false, @record.save
1168
+ end
1169
+
1170
+ def test_should_fail_if_event_has_no_transition
1171
+ @record.state = 'idling'
1172
+ assert_equal false, @record.save
1173
+ end
1174
+
1175
+ def test_should_run_before_callbacks
1176
+ ran_callback = false
1177
+ @machine.before_transition { ran_callback = true }
1178
+
1179
+ @record.save
1180
+ assert ran_callback
1181
+ end
1182
+
1183
+ def test_should_run_before_callbacks_once
1184
+ before_count = 0
1185
+ @machine.before_transition { before_count += 1 }
1186
+
1187
+ @record.save
1188
+ assert_equal 1, before_count
1189
+ end
1190
+
1191
+ def test_should_run_around_callbacks_before_yield
1192
+ ran_callback = false
1193
+ @machine.around_transition {|block| ran_callback = true; block.call }
1194
+
1195
+ @record.save
1196
+ assert ran_callback
1197
+ end
1198
+
1199
+ def test_should_run_around_callbacks_before_yield_once
1200
+ around_before_count = 0
1201
+ @machine.around_transition {|block| around_before_count += 1; block.call }
1202
+
1203
+ @record.save
1204
+ assert_equal 1, around_before_count
1205
+ end
1206
+
1207
+ def test_should_persist_new_state
1208
+ @record.save
1209
+ assert_equal 'idling', @record.state
1210
+ end
1211
+
1212
+ def test_should_run_after_callbacks
1213
+ ran_callback = false
1214
+ @machine.after_transition { ran_callback = true }
1215
+
1216
+ @record.save
1217
+ assert ran_callback
1218
+ end
1219
+
1220
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
1221
+ @model.before_create {|record| false}
1222
+
1223
+ ran_callback = false
1224
+ @machine.after_transition { ran_callback = true }
1225
+
1226
+ begin; @record.save; rescue; end
1227
+ assert !ran_callback
1228
+ end
1229
+
1230
+ def test_should_run_after_callbacks_with_failures_enabled_if_fails
1231
+ @model.before_create {|record| false}
1232
+
1233
+ ran_callback = false
1234
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
1235
+
1236
+ begin; @record.save; rescue; end
1237
+ assert ran_callback
1238
+ end
1239
+
1240
+ def test_should_not_run_around_callbacks_with_failures_disabled_if_fails
1241
+ @model.before_create {|record| false}
1242
+
1243
+ ran_callback = false
1244
+ @machine.around_transition {|block| block.call; ran_callback = true }
1245
+
1246
+ begin; @record.save; rescue; end
1247
+ assert !ran_callback
1248
+ end
1249
+
1250
+ def test_should_run_around_callbacks_after_yield
1251
+ ran_callback = false
1252
+ @machine.around_transition {|block| block.call; ran_callback = true }
1253
+
1254
+ @record.save
1255
+ assert ran_callback
1256
+ end
1257
+
1258
+ def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_fails
1259
+ @model.before_create {|record| false}
1260
+
1261
+ ran_callback = false
1262
+ @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
1263
+
1264
+ begin; @record.save; rescue; end
1265
+ assert ran_callback
1266
+ end
1267
+
1268
+ def test_should_run_before_transitions_within_transaction
1269
+ @machine.before_transition { @model.create; raise ActiveRecord::Rollback }
1270
+
1271
+ begin
1272
+ @record.save
1273
+ rescue Exception
1274
+ end
1275
+
1276
+ assert_equal 0, @model.count
1277
+ end
1278
+
1279
+ def test_should_run_after_transitions_within_transaction
1280
+ @machine.after_transition { @model.create; raise ActiveRecord::Rollback }
1281
+
1282
+ begin
1283
+ @record.save
1284
+ rescue Exception
1285
+ end
1286
+
1287
+ assert_equal 0, @model.count
1288
+ end
1289
+
1290
+ def test_should_run_around_transition_within_transaction
1291
+ @machine.around_transition { @model.create; raise ActiveRecord::Rollback }
1292
+
1293
+ begin
1294
+ @record.save
1295
+ rescue Exception
1296
+ end
1297
+
1298
+ assert_equal 0, @model.count
1299
+ end
1300
+ end
1301
+
1302
+ class MachineWithEventAttributesOnSaveBangTest < BaseTestCase
1303
+ def setup
1304
+ @model = new_model
1305
+ @machine = StateMachine::Machine.new(@model)
1306
+ @machine.event :ignite do
1307
+ transition :parked => :idling
1308
+ end
1309
+
1310
+ @record = @model.new
1311
+ @record.state = 'parked'
1312
+ @record.state_event = 'ignite'
1313
+ end
1314
+
1315
+ def test_should_fail_if_event_is_invalid
1316
+ @record.state_event = 'invalid'
1317
+ assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
1318
+ end
1319
+
1320
+ def test_should_fail_if_event_has_no_transition
1321
+ @record.state = 'idling'
1322
+ assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
1323
+ end
1324
+
1325
+ def test_should_be_successful_if_event_has_transition
1326
+ assert_equal true, @record.save!
1327
+ end
1328
+
1329
+ def test_should_run_before_callbacks
1330
+ ran_callback = false
1331
+ @machine.before_transition { ran_callback = true }
1332
+
1333
+ @record.save!
1334
+ assert ran_callback
1335
+ end
1336
+
1337
+ def test_should_run_before_callbacks_once
1338
+ before_count = 0
1339
+ @machine.before_transition { before_count += 1 }
1340
+
1341
+ @record.save!
1342
+ assert_equal 1, before_count
1343
+ end
1344
+
1345
+ def test_should_run_around_callbacks_before_yield
1346
+ ran_callback = false
1347
+ @machine.around_transition {|block| ran_callback = true; block.call }
1348
+
1349
+ @record.save!
1350
+ assert ran_callback
1351
+ end
1352
+
1353
+ def test_should_run_around_callbacks_before_yield_once
1354
+ around_before_count = 0
1355
+ @machine.around_transition {|block| around_before_count += 1; block.call }
1356
+
1357
+ @record.save!
1358
+ assert_equal 1, around_before_count
1359
+ end
1360
+
1361
+ def test_should_persist_new_state
1362
+ @record.save!
1363
+ assert_equal 'idling', @record.state
1364
+ end
1365
+
1366
+ def test_should_run_after_callbacks
1367
+ ran_callback = false
1368
+ @machine.after_transition { ran_callback = true }
1369
+
1370
+ @record.save!
1371
+ assert ran_callback
1372
+ end
1373
+
1374
+ def test_should_run_around_callbacks_after_yield
1375
+ ran_callback = false
1376
+ @machine.around_transition {|block| block.call; ran_callback = true }
1377
+
1378
+ @record.save!
1379
+ assert ran_callback
1380
+ end
1381
+ end
1382
+
1383
+ class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
1384
+ def setup
1385
+ @superclass = new_model do
1386
+ def persist
1387
+ create_or_update
1388
+ end
1389
+ end
1390
+ @model = Class.new(@superclass)
1391
+ @machine = StateMachine::Machine.new(@model, :action => :persist)
1392
+ @machine.event :ignite do
1393
+ transition :parked => :idling
1394
+ end
1395
+
1396
+ @record = @model.new
1397
+ @record.state = 'parked'
1398
+ @record.state_event = 'ignite'
1399
+ end
1400
+
1401
+ def test_should_not_transition_on_valid?
1402
+ @record.valid?
1403
+ assert_equal 'parked', @record.state
1404
+ end
1405
+
1406
+ def test_should_not_transition_on_save
1407
+ @record.save
1408
+ assert_equal 'parked', @record.state
1409
+ end
1410
+
1411
+ def test_should_not_transition_on_save!
1412
+ @record.save!
1413
+ assert_equal 'parked', @record.state
1414
+ end
1415
+
1416
+ def test_should_transition_on_custom_action
1417
+ @record.persist
1418
+ assert_equal 'idling', @record.state
1419
+ end
1420
+ end
1421
+
1422
+ class MachineWithObserversTest < BaseTestCase
1423
+ def setup
1424
+ @model = new_model
1425
+ @machine = StateMachine::Machine.new(@model)
1426
+ @machine.state :parked, :idling
1427
+ @machine.event :ignite
1428
+ @record = @model.new(:state => 'parked')
1429
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
1430
+ end
1431
+
1432
+ def test_should_call_all_transition_callback_permutations
1433
+ callbacks = [
1434
+ :before_ignite_from_parked_to_idling,
1435
+ :before_ignite_from_parked,
1436
+ :before_ignite_to_idling,
1437
+ :before_ignite,
1438
+ :before_transition_state_from_parked_to_idling,
1439
+ :before_transition_state_from_parked,
1440
+ :before_transition_state_to_idling,
1441
+ :before_transition_state,
1442
+ :before_transition
1443
+ ]
1444
+
1445
+ notified = false
1446
+ observer = new_observer(@model) do
1447
+ callbacks.each do |callback|
1448
+ define_method(callback) do |*args|
1449
+ notifications << callback
1450
+ end
1451
+ end
1452
+ end
1453
+
1454
+ instance = observer.instance
1455
+
1456
+ @transition.perform
1457
+ assert_equal callbacks, instance.notifications
1458
+ end
1459
+
1460
+ def test_should_pass_record_and_transition_to_before_callbacks
1461
+ observer = new_observer(@model) do
1462
+ def before_transition(*args)
1463
+ notifications << args
1464
+ end
1465
+ end
1466
+ instance = observer.instance
1467
+
1468
+ @transition.perform
1469
+ assert_equal [[@record, @transition]], instance.notifications
1470
+ end
1471
+
1472
+ def test_should_pass_record_and_transition_to_after_callbacks
1473
+ observer = new_observer(@model) do
1474
+ def after_transition(*args)
1475
+ notifications << args
1476
+ end
1477
+ end
1478
+ instance = observer.instance
1479
+
1480
+ @transition.perform
1481
+ assert_equal [[@record, @transition]], instance.notifications
1482
+ end
1483
+
1484
+ def test_should_call_methods_outside_the_context_of_the_record
1485
+ observer = new_observer(@model) do
1486
+ def before_ignite(*args)
1487
+ notifications << self
1488
+ end
1489
+ end
1490
+ instance = observer.instance
1491
+
1492
+ @transition.perform
1493
+ assert_equal [instance], instance.notifications
1494
+ end
1495
+
1496
+ def test_should_use_original_observer_behavior_to_handle_non_state_machine_callbacks
1497
+ observer = new_observer(@model) do
1498
+ def before_save(object)
1499
+ end
1500
+
1501
+ def before_ignite(*args)
1502
+ end
1503
+
1504
+ def update_without_multiple_args(observed_method, object)
1505
+ notifications << [observed_method, object] if [:before_save, :before_ignite].include?(observed_method)
1506
+ super
1507
+ end
1508
+ end
1509
+
1510
+ instance = observer.instance
1511
+
1512
+ @transition.perform
1513
+ assert_equal [[:before_save, @record]], instance.notifications
1514
+ end
1515
+ end
1516
+
1517
+ class MachineWithNamespacedObserversTest < BaseTestCase
1518
+ def setup
1519
+ @model = new_model
1520
+ @machine = StateMachine::Machine.new(@model, :state, :namespace => 'alarm')
1521
+ @machine.state :active, :off
1522
+ @machine.event :enable
1523
+ @record = @model.new(:state => 'off')
1524
+ @transition = StateMachine::Transition.new(@record, @machine, :enable, :off, :active)
1525
+ end
1526
+
1527
+ def test_should_call_namespaced_before_event_method
1528
+ observer = new_observer(@model) do
1529
+ def before_enable_alarm(*args)
1530
+ notifications << args
1531
+ end
1532
+ end
1533
+ instance = observer.instance
1534
+
1535
+ @transition.perform
1536
+ assert_equal [[@record, @transition]], instance.notifications
1537
+ end
1538
+
1539
+ def test_should_call_namespaced_after_event_method
1540
+ observer = new_observer(@model) do
1541
+ def after_enable_alarm(*args)
1542
+ notifications << args
1543
+ end
1544
+ end
1545
+ instance = observer.instance
1546
+
1547
+ @transition.perform
1548
+ assert_equal [[@record, @transition]], instance.notifications
1549
+ end
1550
+ end
1551
+
1552
+ class MachineWithMixedCallbacksTest < BaseTestCase
1553
+ def setup
1554
+ @model = new_model
1555
+ @machine = StateMachine::Machine.new(@model)
1556
+ @machine.state :parked, :idling
1557
+ @machine.event :ignite
1558
+ @record = @model.new(:state => 'parked')
1559
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
1560
+
1561
+ @notifications = []
1562
+
1563
+ # Create callbacks
1564
+ @machine.before_transition {@notifications << :callback_before_transition}
1565
+ @machine.after_transition {@notifications << :callback_after_transition}
1566
+ @machine.around_transition do |block|
1567
+ @notifications << :callback_around_before_transition
1568
+ block.call
1569
+ @notifications << :callback_arond_after_transition
1570
+ end
1571
+
1572
+ # Create observer callbacks
1573
+ observer = new_observer(@model) do
1574
+ def before_ignite(*args)
1575
+ notifications << :observer_before_ignite
1576
+ end
1577
+
1578
+ def before_transition(*args)
1579
+ notifications << :observer_before_transition
1580
+ end
1581
+
1582
+ def after_ignite(*args)
1583
+ notifications << :observer_after_ignite
1584
+ end
1585
+
1586
+ def after_transition(*args)
1587
+ notifications << :observer_after_transition
1588
+ end
1589
+ end
1590
+ instance = observer.instance
1591
+ instance.notifications = @notifications
1592
+
1593
+ @transition.perform
1594
+ end
1595
+
1596
+ def test_should_invoke_callbacks_in_specific_order
1597
+ expected = [
1598
+ :callback_before_transition,
1599
+ :callback_around_before_transition,
1600
+ :observer_before_ignite,
1601
+ :observer_before_transition,
1602
+ :callback_arond_after_transition,
1603
+ :callback_after_transition,
1604
+ :observer_after_ignite,
1605
+ :observer_after_transition
1606
+ ]
1607
+
1608
+ assert_equal expected, @notifications
1609
+ end
1610
+ end
1611
+
1612
+ if ActiveRecord.const_defined?(:NamedScope)
1613
+ class MachineWithScopesTest < BaseTestCase
1614
+ def setup
1615
+ @model = new_model
1616
+ @machine = StateMachine::Machine.new(@model)
1617
+ @machine.state :parked, :first_gear
1618
+ @machine.state :idling, :value => lambda {'idling'}
1619
+ end
1620
+
1621
+ def test_should_create_singular_with_scope
1622
+ assert @model.respond_to?(:with_state)
1623
+ end
1624
+
1625
+ def test_should_only_include_records_with_state_in_singular_with_scope
1626
+ parked = @model.create :state => 'parked'
1627
+ idling = @model.create :state => 'idling'
1628
+
1629
+ assert_equal [parked], @model.with_state(:parked).find(:all)
1630
+ end
1631
+
1632
+ def test_should_create_plural_with_scope
1633
+ assert @model.respond_to?(:with_states)
1634
+ end
1635
+
1636
+ def test_should_only_include_records_with_states_in_plural_with_scope
1637
+ parked = @model.create :state => 'parked'
1638
+ idling = @model.create :state => 'idling'
1639
+
1640
+ assert_equal [parked, idling], @model.with_states(:parked, :idling).find(:all)
1641
+ end
1642
+
1643
+ def test_should_create_singular_without_scope
1644
+ assert @model.respond_to?(:without_state)
1645
+ end
1646
+
1647
+ def test_should_only_include_records_without_state_in_singular_without_scope
1648
+ parked = @model.create :state => 'parked'
1649
+ idling = @model.create :state => 'idling'
1650
+
1651
+ assert_equal [parked], @model.without_state(:idling).find(:all)
1652
+ end
1653
+
1654
+ def test_should_create_plural_without_scope
1655
+ assert @model.respond_to?(:without_states)
1656
+ end
1657
+
1658
+ def test_should_only_include_records_without_states_in_plural_without_scope
1659
+ parked = @model.create :state => 'parked'
1660
+ idling = @model.create :state => 'idling'
1661
+ first_gear = @model.create :state => 'first_gear'
1662
+
1663
+ assert_equal [parked, idling], @model.without_states(:first_gear).find(:all)
1664
+ end
1665
+
1666
+ def test_should_allow_chaining_scopes
1667
+ parked = @model.create :state => 'parked'
1668
+ idling = @model.create :state => 'idling'
1669
+
1670
+ assert_equal [idling], @model.without_state(:parked).with_state(:idling).find(:all)
1671
+ end
1672
+ end
1673
+
1674
+ class MachineWithScopesAndOwnerSubclassTest < BaseTestCase
1675
+ def setup
1676
+ @model = new_model
1677
+ @machine = StateMachine::Machine.new(@model, :state)
1678
+
1679
+ @subclass = Class.new(@model)
1680
+ @subclass_machine = @subclass.state_machine(:state) {}
1681
+ @subclass_machine.state :parked, :idling, :first_gear
1682
+ end
1683
+
1684
+ def test_should_only_include_records_with_subclass_states_in_with_scope
1685
+ parked = @subclass.create :state => 'parked'
1686
+ idling = @subclass.create :state => 'idling'
1687
+
1688
+ assert_equal [parked, idling], @subclass.with_states(:parked, :idling).find(:all)
1689
+ end
1690
+
1691
+ def test_should_only_include_records_without_subclass_states_in_without_scope
1692
+ parked = @subclass.create :state => 'parked'
1693
+ idling = @subclass.create :state => 'idling'
1694
+ first_gear = @subclass.create :state => 'first_gear'
1695
+
1696
+ assert_equal [parked, idling], @subclass.without_states(:first_gear).find(:all)
1697
+ end
1698
+ end
1699
+
1700
+ class MachineWithComplexPluralizationScopesTest < BaseTestCase
1701
+ def setup
1702
+ @model = new_model
1703
+ @machine = StateMachine::Machine.new(@model, :status)
1704
+ end
1705
+
1706
+ def test_should_create_singular_with_scope
1707
+ assert @model.respond_to?(:with_status)
1708
+ end
1709
+
1710
+ def test_should_create_plural_with_scope
1711
+ assert @model.respond_to?(:with_statuses)
1712
+ end
1713
+ end
1714
+
1715
+ class MachineWithScopesAndJoinsTest < BaseTestCase
1716
+ def setup
1717
+ @company = new_model(:company)
1718
+ ActiveRecordTest.const_set('Company', @company)
1719
+
1720
+ @vehicle = new_model(:vehicle) do
1721
+ connection.add_column :vehicle, :company_id, :integer
1722
+ belongs_to :company, :class_name => 'ActiveRecordTest::Company'
1723
+ end
1724
+ ActiveRecordTest.const_set('Vehicle', @vehicle)
1725
+
1726
+ @company_machine = StateMachine::Machine.new(@company, :initial => :active)
1727
+ @vehicle_machine = StateMachine::Machine.new(@vehicle, :initial => :parked)
1728
+ @vehicle_machine.state :idling
1729
+
1730
+ @ford = @company.create
1731
+ @mustang = @vehicle.create(:company => @ford)
1732
+ end
1733
+
1734
+ def test_should_find_records_in_with_scope
1735
+ assert_equal [@mustang], @vehicle.with_states(:parked).find(:all, :include => :company, :conditions => 'company.state = "active"')
1736
+ end
1737
+
1738
+ def test_should_find_records_in_without_scope
1739
+ assert_equal [@mustang], @vehicle.without_states(:idling).find(:all, :include => :company, :conditions => 'company.state = "active"')
1740
+ end
1741
+
1742
+ def teardown
1743
+ ActiveRecordTest.class_eval do
1744
+ remove_const('Vehicle')
1745
+ remove_const('Company')
1746
+ end
1747
+ end
1748
+ end
1749
+ else
1750
+ $stderr.puts 'Skipping ActiveRecord Scope tests. `gem install active_record` >= v2.1.0 and try again.'
1751
+ end
1752
+
1753
+ if Object.const_defined?(:I18n)
1754
+ class MachineWithInternationalizationTest < BaseTestCase
1755
+ def setup
1756
+ I18n.backend = I18n::Backend::Simple.new
1757
+
1758
+ # Initialize the backend
1759
+ I18n.backend.translate(:en, 'activerecord.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
1760
+
1761
+ @model = new_model
1762
+ end
1763
+
1764
+ def test_should_use_defaults
1765
+ I18n.backend.store_translations(:en, {
1766
+ :activerecord => {:errors => {:messages => {:invalid_transition => "cannot #{interpolation_key('event')}"}}}
1767
+ })
1768
+
1769
+ machine = StateMachine::Machine.new(@model)
1770
+ machine.state :parked, :idling
1771
+ machine.event :ignite
1772
+
1773
+ record = @model.new(:state => 'idling')
1774
+
1775
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
1776
+ assert_equal ['State cannot ignite'], record.errors.full_messages
1777
+ end
1778
+
1779
+ def test_should_allow_customized_error_key
1780
+ I18n.backend.store_translations(:en, {
1781
+ :activerecord => {:errors => {:messages => {:bad_transition => "cannot #{interpolation_key('event')}"}}}
1782
+ })
1783
+
1784
+ machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => :bad_transition})
1785
+ machine.state :parked, :idling
1786
+
1787
+ record = @model.new(:state => 'idling')
1788
+
1789
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
1790
+ assert_equal ['State cannot ignite'], record.errors.full_messages
1791
+ end
1792
+
1793
+ def test_should_allow_customized_error_string
1794
+ machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => "cannot #{interpolation_key('event')}"})
1795
+ machine.state :parked, :idling
1796
+
1797
+ record = @model.new(:state => 'idling')
1798
+
1799
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
1800
+ assert_equal ['State cannot ignite'], record.errors.full_messages
1801
+ end
1802
+
1803
+ def test_should_allow_customized_state_key_scoped_to_class_and_machine
1804
+ I18n.backend.store_translations(:en, {
1805
+ :activerecord => {:state_machines => {:'active_record_test/foo' => {:state => {:states => {:parked => 'shutdown'}}}}}
1806
+ })
1807
+
1808
+ machine = StateMachine::Machine.new(@model)
1809
+ machine.state :parked
1810
+
1811
+ assert_equal 'shutdown', machine.state(:parked).human_name
1812
+ end
1813
+
1814
+ def test_should_allow_customized_state_key_scoped_to_machine
1815
+ I18n.backend.store_translations(:en, {
1816
+ :activerecord => {:state_machines => {:state => {:states => {:parked => 'shutdown'}}}}
1817
+ })
1818
+
1819
+ machine = StateMachine::Machine.new(@model)
1820
+ machine.state :parked
1821
+
1822
+ assert_equal 'shutdown', machine.state(:parked).human_name
1823
+ end
1824
+
1825
+ def test_should_allow_customized_state_key_unscoped
1826
+ I18n.backend.store_translations(:en, {
1827
+ :activerecord => {:state_machines => {:states => {:parked => 'shutdown'}}}
1828
+ })
1829
+
1830
+ machine = StateMachine::Machine.new(@model)
1831
+ machine.state :parked
1832
+
1833
+ assert_equal 'shutdown', machine.state(:parked).human_name
1834
+ end
1835
+
1836
+ def test_should_allow_customized_event_key_scoped_to_class_and_machine
1837
+ I18n.backend.store_translations(:en, {
1838
+ :activerecord => {:state_machines => {:'active_record_test/foo' => {:state => {:events => {:park => 'stop'}}}}}
1839
+ })
1840
+
1841
+ machine = StateMachine::Machine.new(@model)
1842
+ machine.event :park
1843
+
1844
+ assert_equal 'stop', machine.event(:park).human_name
1845
+ end
1846
+
1847
+ def test_should_allow_customized_event_key_scoped_to_machine
1848
+ I18n.backend.store_translations(:en, {
1849
+ :activerecord => {:state_machines => {:state => {:events => {:park => 'stop'}}}}
1850
+ })
1851
+
1852
+ machine = StateMachine::Machine.new(@model)
1853
+ machine.event :park
1854
+
1855
+ assert_equal 'stop', machine.event(:park).human_name
1856
+ end
1857
+
1858
+ def test_should_allow_customized_event_key_unscoped
1859
+ I18n.backend.store_translations(:en, {
1860
+ :activerecord => {:state_machines => {:events => {:park => 'stop'}}}
1861
+ })
1862
+
1863
+ machine = StateMachine::Machine.new(@model)
1864
+ machine.event :park
1865
+
1866
+ assert_equal 'stop', machine.event(:park).human_name
1867
+ end
1868
+
1869
+ def test_should_only_add_locale_once_in_load_path
1870
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_record/locale\.rb$}}.length
1871
+
1872
+ # Create another ActiveRecord model that will triger the i18n feature
1873
+ new_model
1874
+
1875
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_record/locale\.rb$}}.length
1876
+ end
1877
+
1878
+ def test_should_add_locale_to_beginning_of_load_path
1879
+ @original_load_path = I18n.load_path
1880
+ I18n.backend = I18n::Backend::Simple.new
1881
+
1882
+ app_locale = File.dirname(__FILE__) + '/../../files/en.yml'
1883
+ default_locale = File.dirname(__FILE__) + '/../../../lib/state_machine/integrations/active_record/locale.rb'
1884
+ I18n.load_path = [app_locale]
1885
+
1886
+ StateMachine::Machine.new(@model)
1887
+
1888
+ assert_equal [default_locale, app_locale].map {|path| File.expand_path(path)}, I18n.load_path.map {|path| File.expand_path(path)}
1889
+ ensure
1890
+ I18n.load_path = @original_load_path
1891
+ end
1892
+
1893
+ def test_should_prefer_other_locales_first
1894
+ @original_load_path = I18n.load_path
1895
+ I18n.backend = I18n::Backend::Simple.new
1896
+ I18n.load_path = [File.dirname(__FILE__) + '/../../files/en.yml']
1897
+
1898
+ machine = StateMachine::Machine.new(@model)
1899
+ machine.state :parked, :idling
1900
+ machine.event :ignite
1901
+
1902
+ record = @model.new(:state => 'idling')
1903
+
1904
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
1905
+ assert_equal ['State cannot transition'], record.errors.full_messages
1906
+ ensure
1907
+ I18n.load_path = @original_load_path
1908
+ end
1909
+
1910
+ private
1911
+ def interpolation_key(key)
1912
+ !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0' ? "{{#{key}}}" : "%{#{key}}"
1913
+ end
1914
+ end
1915
+ else
1916
+ $stderr.puts 'Skipping ActiveRecord I18n tests. `gem install active_record` >= v2.2.0 and try again.'
1917
+ end
1918
+ end