pluginaweek-state_machine 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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