pluginaweek-state_machine 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/CHANGELOG.rdoc +273 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +429 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +251 -0
  33. data/lib/state_machine/event_collection.rb +113 -0
  34. data/lib/state_machine/extensions.rb +158 -0
  35. data/lib/state_machine/guard.rb +219 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +444 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +10 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +325 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +292 -0
  43. data/lib/state_machine/machine.rb +1431 -0
  44. data/lib/state_machine/machine_collection.rb +146 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +367 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +129 -0
  60. data/test/unit/event_collection_test.rb +293 -0
  61. data/test/unit/event_test.rb +605 -0
  62. data/test/unit/guard_test.rb +862 -0
  63. data/test/unit/integrations/active_record_test.rb +1001 -0
  64. data/test/unit/integrations/data_mapper_test.rb +694 -0
  65. data/test/unit/integrations/sequel_test.rb +486 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +710 -0
  70. data/test/unit/machine_test.rb +1910 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1113 -0
  78. metadata +161 -0
@@ -0,0 +1,1001 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ begin
4
+ # Load library
5
+ require 'rubygems'
6
+
7
+ gem 'activerecord', ENV['AR_VERSION'] ? "=#{ENV['AR_VERSION']}" : '>=2.1.0'
8
+ require 'active_record'
9
+
10
+ FIXTURES_ROOT = File.dirname(__FILE__) + '/../../fixtures/'
11
+
12
+ # Load TestCase helpers
13
+ require 'active_support/test_case'
14
+ require 'active_record/fixtures'
15
+ require 'active_record/test_case'
16
+
17
+ # Establish database connection
18
+ ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3', 'database' => ':memory:'})
19
+ ActiveRecord::Base.logger = Logger.new("#{File.dirname(__FILE__)}/../../active_record.log")
20
+
21
+ # Add model/observer creation helpers
22
+ ActiveRecord::TestCase.class_eval do
23
+ # Creates a new ActiveRecord model (and the associated table)
24
+ def new_model(create_table = true, &block)
25
+ model = Class.new(ActiveRecord::Base) do
26
+ connection.create_table(:foo, :force => true) {|t| t.string(:state)} if create_table
27
+ set_table_name('foo')
28
+
29
+ def self.name; 'ActiveRecordTest::Foo'; end
30
+ end
31
+ model.class_eval(&block) if block_given?
32
+ model
33
+ end
34
+
35
+ # Creates a new ActiveRecord observer
36
+ def new_observer(model, &block)
37
+ observer = Class.new(ActiveRecord::Observer) do
38
+ attr_accessor :notifications
39
+
40
+ def initialize
41
+ super
42
+ @notifications = []
43
+ end
44
+ end
45
+ observer.observe(model)
46
+ observer.class_eval(&block) if block_given?
47
+ observer
48
+ end
49
+ end
50
+
51
+ module ActiveRecordTest
52
+ class IntegrationTest < ActiveRecord::TestCase
53
+ def test_should_match_if_class_inherits_from_active_record
54
+ assert StateMachine::Integrations::ActiveRecord.matches?(new_model)
55
+ end
56
+
57
+ def test_should_not_match_if_class_does_not_inherit_from_active_record
58
+ assert !StateMachine::Integrations::ActiveRecord.matches?(Class.new)
59
+ end
60
+ end
61
+
62
+ class MachineByDefaultTest < ActiveRecord::TestCase
63
+ def setup
64
+ @model = new_model
65
+ @machine = StateMachine::Machine.new(@model)
66
+ end
67
+
68
+ def test_should_use_save_as_action
69
+ assert_equal :save, @machine.action
70
+ end
71
+
72
+ def test_should_use_transactions
73
+ assert_equal true, @machine.use_transactions
74
+ end
75
+
76
+ def test_should_create_notifier_before_callback
77
+ assert_equal 1, @machine.callbacks[:before].size
78
+ end
79
+
80
+ def test_should_create_notifier_after_callback
81
+ assert_equal 1, @machine.callbacks[:after].size
82
+ end
83
+ end
84
+
85
+ class MachineTest < ActiveRecord::TestCase
86
+ def setup
87
+ @model = new_model
88
+ @machine = StateMachine::Machine.new(@model)
89
+ @machine.state :parked, :first_gear
90
+ @machine.state :idling, :value => lambda {'idling'}
91
+ end
92
+
93
+ def test_should_create_singular_with_scope
94
+ assert @model.respond_to?(:with_state)
95
+ end
96
+
97
+ def test_should_only_include_records_with_state_in_singular_with_scope
98
+ parked = @model.create :state => 'parked'
99
+ idling = @model.create :state => 'idling'
100
+
101
+ assert_equal [parked], @model.with_state(:parked)
102
+ end
103
+
104
+ def test_should_create_plural_with_scope
105
+ assert @model.respond_to?(:with_states)
106
+ end
107
+
108
+ def test_should_only_include_records_with_states_in_plural_with_scope
109
+ parked = @model.create :state => 'parked'
110
+ idling = @model.create :state => 'idling'
111
+
112
+ assert_equal [parked, idling], @model.with_states(:parked, :idling)
113
+ end
114
+
115
+ def test_should_create_singular_without_scope
116
+ assert @model.respond_to?(:without_state)
117
+ end
118
+
119
+ def test_should_only_include_records_without_state_in_singular_without_scope
120
+ parked = @model.create :state => 'parked'
121
+ idling = @model.create :state => 'idling'
122
+
123
+ assert_equal [parked], @model.without_state(:idling)
124
+ end
125
+
126
+ def test_should_create_plural_without_scope
127
+ assert @model.respond_to?(:without_states)
128
+ end
129
+
130
+ def test_should_only_include_records_without_states_in_plural_without_scope
131
+ parked = @model.create :state => 'parked'
132
+ idling = @model.create :state => 'idling'
133
+ first_gear = @model.create :state => 'first_gear'
134
+
135
+ assert_equal [parked, idling], @model.without_states(:first_gear)
136
+ end
137
+
138
+ def test_should_allow_chaining_scopes
139
+ parked = @model.create :state => 'parked'
140
+ idling = @model.create :state => 'idling'
141
+
142
+ assert_equal [idling], @model.without_state(:parked).with_state(:idling)
143
+ end
144
+
145
+ def test_should_rollback_transaction_if_false
146
+ @machine.within_transaction(@model.new) do
147
+ @model.create
148
+ false
149
+ end
150
+
151
+ assert_equal 0, @model.count
152
+ end
153
+
154
+ def test_should_not_rollback_transaction_if_true
155
+ @machine.within_transaction(@model.new) do
156
+ @model.create
157
+ true
158
+ end
159
+
160
+ assert_equal 1, @model.count
161
+ end
162
+
163
+ def test_should_invalidate_using_errors
164
+ I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
165
+
166
+ record = @model.new
167
+ record.state = 'parked'
168
+
169
+ @machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
170
+
171
+ assert record.errors.invalid?(:state)
172
+ assert_equal 'cannot transition via "park"', record.errors.on(:state)
173
+ end
174
+
175
+ def test_should_auto_prefix_custom_attributes_on_invalidation
176
+ record = @model.new
177
+ @machine.invalidate(record, :event, :invalid)
178
+
179
+ assert_equal 'is invalid', record.errors.on(:state_event)
180
+ end
181
+
182
+ def test_should_clear_errors_on_reset
183
+ record = @model.new
184
+ record.state = 'parked'
185
+ record.errors.add(:state, 'is invalid')
186
+
187
+ @machine.reset(record)
188
+ assert_nil record.errors.on(:id)
189
+ end
190
+
191
+ def test_should_not_override_the_column_reader
192
+ record = @model.new
193
+ record[:state] = 'parked'
194
+ assert_equal 'parked', record.state
195
+ end
196
+
197
+ def test_should_not_override_the_column_writer
198
+ record = @model.new
199
+ record.state = 'parked'
200
+ assert_equal 'parked', record[:state]
201
+ end
202
+ end
203
+
204
+ class MachineWithoutDatabaseTest < ActiveRecord::TestCase
205
+ def setup
206
+ @model = new_model(false) do
207
+ # Simulate the database not being available entirely
208
+ def self.connection
209
+ raise ActiveRecord::ConnectionNotEstablished
210
+ end
211
+ end
212
+ end
213
+
214
+ def test_should_allow_machine_creation
215
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
216
+ end
217
+ end
218
+
219
+ class MachineUnmigratedTest < ActiveRecord::TestCase
220
+ def setup
221
+ @model = new_model(false)
222
+
223
+ # Drop the table so that it definitely doesn't exist
224
+ @model.connection.drop_table(:foo) if @model.connection.table_exists?(:foo)
225
+ end
226
+
227
+ def test_should_allow_machine_creation
228
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
229
+ end
230
+ end
231
+
232
+ class MachineWithInitialStateTest < ActiveRecord::TestCase
233
+ def setup
234
+ @model = new_model
235
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
236
+ @record = @model.new
237
+ end
238
+
239
+ def test_should_set_initial_state_on_created_object
240
+ assert_equal 'parked', @record.state
241
+ end
242
+ end
243
+
244
+ class MachineWithConflictingPredicateTest < ActiveRecord::TestCase
245
+ def setup
246
+ @model = new_model do
247
+ def state?(*args)
248
+ true
249
+ end
250
+ end
251
+
252
+ @machine = StateMachine::Machine.new(@model)
253
+ @record = @model.new
254
+ end
255
+
256
+ def test_should_not_define_attribute_predicate
257
+ assert @record.state?
258
+ end
259
+ end
260
+
261
+ class MachineWithColumnStateAttributeTest < ActiveRecord::TestCase
262
+ def setup
263
+ @model = new_model
264
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
265
+ @machine.other_states(:idling)
266
+
267
+ @record = @model.new
268
+ end
269
+
270
+ def test_should_have_an_attribute_predicate
271
+ assert @record.respond_to?(:state?)
272
+ end
273
+
274
+ def test_should_test_for_existence_on_predicate_without_parameters
275
+ assert @record.state?
276
+
277
+ @record.state = nil
278
+ assert !@record.state?
279
+ end
280
+
281
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
282
+ assert !@record.state?(:idling)
283
+ end
284
+
285
+ def test_should_return_true_for_predicate_if_matches_current_value
286
+ assert @record.state?(:parked)
287
+ end
288
+
289
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
290
+ assert_raise(IndexError) { @record.state?(:invalid) }
291
+ end
292
+ end
293
+
294
+ class MachineWithNonColumnStateAttributeUndefinedTest < ActiveRecord::TestCase
295
+ def setup
296
+ @model = new_model do
297
+ def initialize
298
+ # Skip attribute initialization
299
+ end
300
+ end
301
+
302
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
303
+ @machine.other_states(:idling)
304
+ @record = @model.new
305
+ end
306
+
307
+ def test_should_not_define_a_reader_attribute_for_the_attribute
308
+ assert !@record.respond_to?(:status)
309
+ end
310
+
311
+ def test_should_not_define_a_writer_attribute_for_the_attribute
312
+ assert !@record.respond_to?(:status=)
313
+ end
314
+
315
+ def test_should_define_an_attribute_predicate
316
+ assert @record.respond_to?(:status?)
317
+ end
318
+
319
+ def test_should_raise_exception_on_predicate_without_parameters
320
+ old_verbose, $VERBOSE = $VERBOSE, nil
321
+ assert_raise(NoMethodError) { @record.status? }
322
+ ensure
323
+ $VERBOSE = old_verbose
324
+ end
325
+ end
326
+
327
+ class MachineWithNonColumnStateAttributeDefinedTest < ActiveRecord::TestCase
328
+ def setup
329
+ @model = new_model do
330
+ attr_accessor :status
331
+ end
332
+
333
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
334
+ @machine.other_states(:idling)
335
+ @record = @model.new
336
+ end
337
+
338
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
339
+ assert !@record.status?(:idling)
340
+ end
341
+
342
+ def test_should_return_true_for_predicate_if_matches_current_value
343
+ assert @record.status?(:parked)
344
+ end
345
+
346
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
347
+ assert_raise(IndexError) { @record.status?(:invalid) }
348
+ end
349
+
350
+ def test_should_set_initial_state_on_created_object
351
+ assert_equal 'parked', @record.status
352
+ end
353
+ end
354
+
355
+ class MachineWithComplexPluralizationTest < ActiveRecord::TestCase
356
+ def setup
357
+ @model = new_model
358
+ @machine = StateMachine::Machine.new(@model, :status)
359
+ end
360
+
361
+ def test_should_create_singular_with_scope
362
+ assert @model.respond_to?(:with_status)
363
+ end
364
+
365
+ def test_should_create_plural_with_scope
366
+ assert @model.respond_to?(:with_statuses)
367
+ end
368
+ end
369
+
370
+ class MachineWithCallbacksTest < ActiveRecord::TestCase
371
+ def setup
372
+ @model = new_model
373
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
374
+ @machine.other_states :idling
375
+ @machine.event :ignite
376
+
377
+ @record = @model.new(:state => 'parked')
378
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
379
+ end
380
+
381
+ def test_should_run_before_callbacks
382
+ called = false
383
+ @machine.before_transition(lambda {called = true})
384
+
385
+ @transition.perform
386
+ assert called
387
+ end
388
+
389
+ def test_should_pass_record_to_before_callbacks_with_one_argument
390
+ record = nil
391
+ @machine.before_transition(lambda {|arg| record = arg})
392
+
393
+ @transition.perform
394
+ assert_equal @record, record
395
+ end
396
+
397
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
398
+ callback_args = nil
399
+ @machine.before_transition(lambda {|*args| callback_args = args})
400
+
401
+ @transition.perform
402
+ assert_equal [@record, @transition], callback_args
403
+ end
404
+
405
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
406
+ context = nil
407
+ @machine.before_transition(lambda {context = self})
408
+
409
+ @transition.perform
410
+ assert_equal self, context
411
+ end
412
+
413
+ def test_should_run_after_callbacks
414
+ called = false
415
+ @machine.after_transition(lambda {called = true})
416
+
417
+ @transition.perform
418
+ assert called
419
+ end
420
+
421
+ def test_should_pass_record_to_after_callbacks_with_one_argument
422
+ record = nil
423
+ @machine.after_transition(lambda {|arg| record = arg})
424
+
425
+ @transition.perform
426
+ assert_equal @record, record
427
+ end
428
+
429
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
430
+ callback_args = nil
431
+ @machine.after_transition(lambda {|*args| callback_args = args})
432
+
433
+ @transition.perform
434
+ assert_equal [@record, @transition], callback_args
435
+ end
436
+
437
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
438
+ context = nil
439
+ @machine.after_transition(lambda {context = self})
440
+
441
+ @transition.perform
442
+ assert_equal self, context
443
+ end
444
+
445
+ def test_should_include_transition_states_in_known_states
446
+ @machine.before_transition :to => :first_gear, :do => lambda {}
447
+
448
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
449
+ end
450
+
451
+ def test_should_allow_symbolic_callbacks
452
+ callback_args = nil
453
+
454
+ klass = class << @record; self; end
455
+ klass.send(:define_method, :after_ignite) do |*args|
456
+ callback_args = args
457
+ end
458
+
459
+ @machine.before_transition(:after_ignite)
460
+
461
+ @transition.perform
462
+ assert_equal [@transition], callback_args
463
+ end
464
+
465
+ def test_should_allow_string_callbacks
466
+ class << @record
467
+ attr_reader :callback_result
468
+ end
469
+
470
+ @machine.before_transition('@callback_result = [1, 2, 3]')
471
+ @transition.perform
472
+
473
+ assert_equal [1, 2, 3], @record.callback_result
474
+ end
475
+ end
476
+
477
+ class MachineWithFailedBeforeCallbacksTest < ActiveRecord::TestCase
478
+ def setup
479
+ @before_count = 0
480
+
481
+ @model = new_model
482
+ @machine = StateMachine::Machine.new(@model)
483
+ @machine.state :parked, :idling
484
+ @machine.event :ignite
485
+ @machine.before_transition(lambda {@before_count += 1; false})
486
+ @machine.before_transition(lambda {@before_count += 1})
487
+
488
+ @record = @model.new(:state => 'parked')
489
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
490
+ @result = @transition.perform
491
+ end
492
+
493
+ def test_should_not_be_successful
494
+ assert !@result
495
+ end
496
+
497
+ def test_should_not_change_current_state
498
+ assert_equal 'parked', @record.state
499
+ end
500
+
501
+ def test_should_not_run_action
502
+ assert @record.new_record?
503
+ end
504
+
505
+ def test_should_not_run_further_before_callbacks
506
+ assert_equal 1, @before_count
507
+ end
508
+ end
509
+
510
+ class MachineWithFailedActionTest < ActiveRecord::TestCase
511
+ def setup
512
+ @model = new_model do
513
+ validates_inclusion_of :state, :in => %w(first_gear)
514
+ end
515
+
516
+ @machine = StateMachine::Machine.new(@model)
517
+ @machine.state :parked, :idling
518
+ @machine.event :ignite
519
+ @record = @model.new(:state => 'parked')
520
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
521
+ @result = @transition.perform
522
+ end
523
+
524
+ def test_should_not_be_successful
525
+ assert !@result
526
+ end
527
+
528
+ def test_should_not_change_current_state
529
+ assert_equal 'parked', @record.state
530
+ end
531
+
532
+ def test_should_not_save_record
533
+ assert @record.new_record?
534
+ end
535
+ end
536
+
537
+ class MachineWithValidationsTest < ActiveRecord::TestCase
538
+ def setup
539
+ @model = new_model
540
+ @machine = StateMachine::Machine.new(@model)
541
+ @machine.state :parked
542
+
543
+ @record = @model.new
544
+ end
545
+
546
+ def test_should_be_valid_if_state_is_known
547
+ @record.state = 'parked'
548
+
549
+ assert @record.valid?
550
+ end
551
+
552
+ def test_should_not_be_valid_if_state_is_unknown
553
+ @record.state = 'invalid'
554
+
555
+ assert !@record.valid?
556
+ assert_equal ['State is invalid'], @record.errors.full_messages
557
+ end
558
+ end
559
+
560
+ class MachineWithStateDrivenValidationsTest < ActiveRecord::TestCase
561
+ def setup
562
+ @model = new_model do
563
+ attr_accessor :seatbelt
564
+ end
565
+
566
+ @machine = StateMachine::Machine.new(@model)
567
+ @machine.state :first_gear, :second_gear do
568
+ validates_presence_of :seatbelt
569
+ end
570
+ @machine.other_states :parked
571
+ end
572
+
573
+ def test_should_be_valid_if_validation_fails_outside_state_scope
574
+ record = @model.new(:state => 'parked', :seatbelt => nil)
575
+ assert record.valid?
576
+ end
577
+
578
+ def test_should_be_invalid_if_validation_fails_within_state_scope
579
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
580
+ assert !record.valid?
581
+ end
582
+
583
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
584
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
585
+ assert record.valid?
586
+ end
587
+ end
588
+
589
+ class MachineWithFailedAfterCallbacksTest < ActiveRecord::TestCase
590
+ def setup
591
+ @after_count = 0
592
+
593
+ @model = new_model
594
+ @machine = StateMachine::Machine.new(@model)
595
+ @machine.state :parked, :idling
596
+ @machine.event :ignite
597
+ @machine.after_transition(lambda {@after_count += 1; false})
598
+ @machine.after_transition(lambda {@after_count += 1})
599
+
600
+ @record = @model.new(:state => 'parked')
601
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
602
+ @result = @transition.perform
603
+ end
604
+
605
+ def test_should_be_successful
606
+ assert @result
607
+ end
608
+
609
+ def test_should_change_current_state
610
+ assert_equal 'idling', @record.state
611
+ end
612
+
613
+ def test_should_save_record
614
+ assert !@record.new_record?
615
+ end
616
+
617
+ def test_should_not_run_further_after_callbacks
618
+ assert_equal 1, @after_count
619
+ end
620
+ end
621
+
622
+ class MachineWithEventAttributesOnValidationTest < ActiveRecord::TestCase
623
+ def setup
624
+ @model = new_model
625
+ @machine = StateMachine::Machine.new(@model)
626
+ @machine.event :ignite do
627
+ transition :parked => :idling
628
+ end
629
+
630
+ @record = @model.new
631
+ @record.state = 'parked'
632
+ @record.state_event = 'ignite'
633
+ end
634
+
635
+ def test_should_fail_if_event_is_invalid
636
+ @record.state_event = 'invalid'
637
+ assert !@record.valid?
638
+ assert_equal ['State event is invalid'], @record.errors.full_messages
639
+ end
640
+
641
+ def test_should_fail_if_event_has_no_transition
642
+ @record.state = 'idling'
643
+ assert !@record.valid?
644
+ assert_equal ['State event cannot transition when idling'], @record.errors.full_messages
645
+ end
646
+
647
+ def test_should_be_successful_if_event_has_transition
648
+ assert @record.valid?
649
+ end
650
+
651
+ def test_should_run_before_callbacks
652
+ ran_callback = false
653
+ @machine.before_transition { ran_callback = true }
654
+
655
+ @record.valid?
656
+ assert ran_callback
657
+ end
658
+
659
+ def test_should_persist_new_state
660
+ @record.valid?
661
+ assert_equal 'idling', @record.state
662
+ end
663
+
664
+ def test_should_not_run_after_callbacks
665
+ ran_callback = false
666
+ @machine.after_transition { ran_callback = true }
667
+
668
+ @record.valid?
669
+ assert !ran_callback
670
+ end
671
+ end
672
+
673
+ class MachineWithEventAttributesOnSaveBangTest < ActiveRecord::TestCase
674
+ def setup
675
+ @model = new_model
676
+ @machine = StateMachine::Machine.new(@model)
677
+ @machine.event :ignite do
678
+ transition :parked => :idling
679
+ end
680
+
681
+ @record = @model.new
682
+ @record.state = 'parked'
683
+ @record.state_event = 'ignite'
684
+ end
685
+
686
+ def test_should_fail_if_event_is_invalid
687
+ @record.state_event = 'invalid'
688
+ assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
689
+ end
690
+
691
+ def test_should_fail_if_event_has_no_transition
692
+ @record.state = 'idling'
693
+ assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
694
+ end
695
+
696
+ def test_should_be_successful_if_event_has_transition
697
+ assert_equal true, @record.save!
698
+ end
699
+
700
+ def test_should_run_before_callbacks
701
+ ran_callback = false
702
+ @machine.before_transition { ran_callback = true }
703
+
704
+ @record.save!
705
+ assert ran_callback
706
+ end
707
+
708
+ def test_should_persist_new_state
709
+ @record.save!
710
+ assert_equal 'idling', @record.state
711
+ end
712
+
713
+ def test_should_run_after_callbacks
714
+ ran_callback = false
715
+ @machine.after_transition { ran_callback = true }
716
+
717
+ @record.save!
718
+ assert ran_callback
719
+ end
720
+ end
721
+
722
+ class MachineWithEventAttributesOnCustomActionTest < ActiveRecord::TestCase
723
+ def setup
724
+ @superclass = new_model do
725
+ def persist
726
+ create_or_update
727
+ end
728
+ end
729
+ @model = Class.new(@superclass)
730
+ @machine = StateMachine::Machine.new(@model, :action => :persist)
731
+ @machine.event :ignite do
732
+ transition :parked => :idling
733
+ end
734
+
735
+ @record = @model.new
736
+ @record.state = 'parked'
737
+ @record.state_event = 'ignite'
738
+ end
739
+
740
+ def test_should_not_transition_on_valid?
741
+ @record.valid?
742
+ assert_equal 'parked', @record.state
743
+ end
744
+
745
+ def test_should_not_transition_on_save
746
+ @record.save
747
+ assert_equal 'parked', @record.state
748
+ end
749
+
750
+ def test_should_not_transition_on_save!
751
+ @record.save!
752
+ assert_equal 'parked', @record.state
753
+ end
754
+
755
+ def test_should_transition_on_custom_action
756
+ @record.persist
757
+ assert_equal 'idling', @record.state
758
+ end
759
+ end
760
+
761
+ class MachineWithObserversTest < ActiveRecord::TestCase
762
+ def setup
763
+ @model = new_model
764
+ @machine = StateMachine::Machine.new(@model)
765
+ @machine.state :parked, :idling
766
+ @machine.event :ignite
767
+ @record = @model.new(:state => 'parked')
768
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
769
+ end
770
+
771
+ def test_should_call_all_transition_callback_permutations
772
+ callbacks = [
773
+ :before_ignite_from_parked_to_idling,
774
+ :before_ignite_from_parked,
775
+ :before_ignite_to_idling,
776
+ :before_ignite,
777
+ :before_transition_state_from_parked_to_idling,
778
+ :before_transition_state_from_parked,
779
+ :before_transition_state_to_idling,
780
+ :before_transition_state,
781
+ :before_transition
782
+ ]
783
+
784
+ notified = false
785
+ observer = new_observer(@model) do
786
+ callbacks.each do |callback|
787
+ define_method(callback) do |*args|
788
+ notifications << callback
789
+ end
790
+ end
791
+ end
792
+
793
+ instance = observer.instance
794
+
795
+ @transition.perform
796
+ assert_equal callbacks, instance.notifications
797
+ end
798
+
799
+ def test_should_pass_record_and_transition_to_before_callbacks
800
+ observer = new_observer(@model) do
801
+ def before_transition(*args)
802
+ notifications << args
803
+ end
804
+ end
805
+ instance = observer.instance
806
+
807
+ @transition.perform
808
+ assert_equal [[@record, @transition]], instance.notifications
809
+ end
810
+
811
+ def test_should_pass_record_and_transition_to_after_callbacks
812
+ observer = new_observer(@model) do
813
+ def after_transition(*args)
814
+ notifications << args
815
+ end
816
+ end
817
+ instance = observer.instance
818
+
819
+ @transition.perform
820
+ assert_equal [[@record, @transition]], instance.notifications
821
+ end
822
+
823
+ def test_should_call_methods_outside_the_context_of_the_record
824
+ observer = new_observer(@model) do
825
+ def before_ignite(*args)
826
+ notifications << self
827
+ end
828
+ end
829
+ instance = observer.instance
830
+
831
+ @transition.perform
832
+ assert_equal [instance], instance.notifications
833
+ end
834
+ end
835
+
836
+ class MachineWithNamespacedObserversTest < ActiveRecord::TestCase
837
+ def setup
838
+ @model = new_model
839
+ @machine = StateMachine::Machine.new(@model, :state, :namespace => 'alarm')
840
+ @machine.state :active, :off
841
+ @machine.event :enable
842
+ @record = @model.new(:state => 'off')
843
+ @transition = StateMachine::Transition.new(@record, @machine, :enable, :off, :active)
844
+ end
845
+
846
+ def test_should_call_namespaced_before_event_method
847
+ observer = new_observer(@model) do
848
+ def before_enable_alarm(*args)
849
+ notifications << args
850
+ end
851
+ end
852
+ instance = observer.instance
853
+
854
+ @transition.perform
855
+ assert_equal [[@record, @transition]], instance.notifications
856
+ end
857
+
858
+ def test_should_call_namespaced_after_event_method
859
+ observer = new_observer(@model) do
860
+ def after_enable_alarm(*args)
861
+ notifications << args
862
+ end
863
+ end
864
+ instance = observer.instance
865
+
866
+ @transition.perform
867
+ assert_equal [[@record, @transition]], instance.notifications
868
+ end
869
+ end
870
+
871
+ class MachineWithMixedCallbacksTest < ActiveRecord::TestCase
872
+ def setup
873
+ @model = new_model
874
+ @machine = StateMachine::Machine.new(@model)
875
+ @machine.state :parked, :idling
876
+ @machine.event :ignite
877
+ @record = @model.new(:state => 'parked')
878
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
879
+
880
+ @notifications = []
881
+
882
+ # Create callbacks
883
+ @machine.before_transition(lambda {@notifications << :callback_before_transition})
884
+ @machine.after_transition(lambda {@notifications << :callback_after_transition})
885
+
886
+ # Create observer callbacks
887
+ observer = new_observer(@model) do
888
+ def before_ignite(*args)
889
+ notifications << :observer_before_ignite
890
+ end
891
+
892
+ def before_transition(*args)
893
+ notifications << :observer_before_transition
894
+ end
895
+
896
+ def after_ignite(*args)
897
+ notifications << :observer_after_ignite
898
+ end
899
+
900
+ def after_transition(*args)
901
+ notifications << :observer_after_transition
902
+ end
903
+ end
904
+ instance = observer.instance
905
+ instance.notifications = @notifications
906
+
907
+ @transition.perform
908
+ end
909
+
910
+ def test_should_invoke_callbacks_in_specific_order
911
+ expected = [
912
+ :callback_before_transition,
913
+ :observer_before_ignite,
914
+ :observer_before_transition,
915
+ :callback_after_transition,
916
+ :observer_after_ignite,
917
+ :observer_after_transition
918
+ ]
919
+
920
+ assert_equal expected, @notifications
921
+ end
922
+ end
923
+
924
+ if Object.const_defined?(:I18n)
925
+ class MachineWithInternationalizationTest < ActiveRecord::TestCase
926
+ def setup
927
+ I18n.backend = I18n::Backend::Simple.new
928
+
929
+ # Initialize the backend
930
+ I18n.backend.translate(:en, 'activerecord.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
931
+
932
+ @model = new_model
933
+ end
934
+
935
+ def test_should_invalidate_using_i18n_default
936
+ I18n.backend.store_translations(:en, {
937
+ :activerecord => {
938
+ :errors => {
939
+ :messages => {
940
+ :invalid_transition => 'cannot {{event}}'
941
+ }
942
+ }
943
+ }
944
+ })
945
+
946
+ machine = StateMachine::Machine.new(@model)
947
+ machine.state :parked, :idling
948
+ event = StateMachine::Event.new(machine, :ignite)
949
+
950
+ record = @model.new(:state => 'idling')
951
+
952
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
953
+ assert_equal 'cannot ignite', record.errors.on(:state)
954
+ end
955
+
956
+ def test_should_invalidate_using_customized_i18n_key_if_specified
957
+ I18n.backend.store_translations(:en, {
958
+ :activerecord => {
959
+ :errors => {
960
+ :messages => {
961
+ :bad_transition => 'cannot {{event}}'
962
+ }
963
+ }
964
+ }
965
+ })
966
+
967
+ machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => :bad_transition})
968
+ machine.state :parked, :idling
969
+
970
+ record = @model.new(:state => 'idling')
971
+
972
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
973
+ assert_equal 'cannot ignite', record.errors.on(:state)
974
+ end
975
+
976
+ def test_should_invalidate_using_customized_i18n_string_if_specified
977
+ machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => 'cannot {{event}}'})
978
+ machine.state :parked, :idling
979
+
980
+ record = @model.new(:state => 'idling')
981
+
982
+ machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
983
+ assert_equal 'cannot ignite', record.errors.on(:state)
984
+ end
985
+
986
+ def test_should_only_add_locale_once_in_load_path
987
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_record/locale\.rb$}}.length
988
+
989
+ # Create another ActiveRecord model that will triger the i18n feature
990
+ new_model
991
+
992
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_record/locale\.rb$}}.length
993
+ end
994
+ end
995
+ else
996
+ $stderr.puts 'Skipping ActiveRecord I18n tests. `gem install active_record` >= v2.2.0 and try again.'
997
+ end
998
+ end
999
+ rescue LoadError
1000
+ $stderr.puts "Skipping ActiveRecord tests. `gem install activerecord#{" -v #{ENV['AR_VERSION']}" if ENV['AR_VERSION']}` and try again."
1001
+ end