verborghs-state_machine 0.9.4

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