mattscilipoti-state_machine 0.8.0.1

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 +298 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +474 -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 +388 -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 +252 -0
  33. data/lib/state_machine/event_collection.rb +122 -0
  34. data/lib/state_machine/extensions.rb +149 -0
  35. data/lib/state_machine/guard.rb +230 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +492 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +351 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +322 -0
  43. data/lib/state_machine/machine.rb +1467 -0
  44. data/lib/state_machine/machine_collection.rb +155 -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/tasks.rb +30 -0
  51. data/lib/state_machine/transition.rb +394 -0
  52. data/tasks/state_machine.rake +1 -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 +120 -0
  60. data/test/unit/event_collection_test.rb +326 -0
  61. data/test/unit/event_test.rb +743 -0
  62. data/test/unit/guard_test.rb +908 -0
  63. data/test/unit/integrations/active_record_test.rb +1367 -0
  64. data/test/unit/integrations/data_mapper_test.rb +962 -0
  65. data/test/unit/integrations/sequel_test.rb +859 -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 +938 -0
  70. data/test/unit/machine_test.rb +2004 -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 +1212 -0
  78. metadata +155 -0
@@ -0,0 +1,962 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ begin
4
+ # Load library
5
+ require 'rubygems'
6
+
7
+ gem 'dm-core', ENV['DM_VERSION'] ? "=#{ENV['DM_VERSION']}" : '>=0.9.4'
8
+ require 'dm-core'
9
+
10
+ # Establish database connection
11
+ DataMapper.setup(:default, 'sqlite3::memory:')
12
+ DataObjects::Sqlite3.logger = DataObjects::Logger.new("#{File.dirname(__FILE__)}/../../data_mapper.log", :info)
13
+
14
+ module DataMapperTest
15
+ class BaseTestCase < Test::Unit::TestCase
16
+ def default_test
17
+ end
18
+
19
+ protected
20
+ # Creates a new DataMapper resource (and the associated table)
21
+ def new_resource(auto_migrate = true, &block)
22
+ resource = Class.new do
23
+ include DataMapper::Resource
24
+
25
+ storage_names[:default] = 'foo'
26
+ def self.name; 'DataMapperTest::Foo'; end
27
+
28
+ property :id, DataMapper::Types::Serial
29
+ property :state, String
30
+
31
+ auto_migrate! if auto_migrate
32
+ end
33
+ resource.class_eval(&block) if block_given?
34
+ resource
35
+ end
36
+
37
+ # Creates a new DataMapper observer
38
+ def new_observer(resource, &block)
39
+ observer = Class.new do
40
+ include DataMapper::Observer
41
+ end
42
+ observer.observe(resource)
43
+ observer.class_eval(&block) if block_given?
44
+ observer
45
+ end
46
+ end
47
+
48
+ class IntegrationTest < BaseTestCase
49
+ def test_should_match_if_class_inherits_from_active_record
50
+ assert StateMachine::Integrations::DataMapper.matches?(new_resource)
51
+ end
52
+
53
+ def test_should_not_match_if_class_does_not_inherit_from_active_record
54
+ assert !StateMachine::Integrations::DataMapper.matches?(Class.new)
55
+ end
56
+ end
57
+
58
+ class MachineByDefaultTest < BaseTestCase
59
+ def setup
60
+ @resource = new_resource
61
+ @machine = StateMachine::Machine.new(@resource)
62
+ end
63
+
64
+ def test_should_use_save_as_action
65
+ assert_equal :save, @machine.action
66
+ end
67
+
68
+ def test_should_not_use_transactions
69
+ assert_equal false, @machine.use_transactions
70
+ end
71
+ end
72
+
73
+ class MachineTest < BaseTestCase
74
+ def setup
75
+ @resource = new_resource
76
+ @machine = StateMachine::Machine.new(@resource)
77
+ @machine.state :parked, :first_gear
78
+ @machine.state :idling, :value => lambda {'idling'}
79
+ end
80
+
81
+ def test_should_create_singular_with_scope
82
+ assert @resource.respond_to?(:with_state)
83
+ end
84
+
85
+ def test_should_only_include_records_with_state_in_singular_with_scope
86
+ parked = @resource.create :state => 'parked'
87
+ idling = @resource.create :state => 'idling'
88
+
89
+ assert_equal [parked], @resource.with_state(:parked)
90
+ end
91
+
92
+ def test_should_create_plural_with_scope
93
+ assert @resource.respond_to?(:with_states)
94
+ end
95
+
96
+ def test_should_only_include_records_with_states_in_plural_with_scope
97
+ parked = @resource.create :state => 'parked'
98
+ idling = @resource.create :state => 'idling'
99
+
100
+ assert_equal [parked, idling], @resource.with_states(:parked, :idling)
101
+ end
102
+
103
+ def test_should_create_singular_without_scope
104
+ assert @resource.respond_to?(:without_state)
105
+ end
106
+
107
+ def test_should_only_include_records_without_state_in_singular_without_scope
108
+ parked = @resource.create :state => 'parked'
109
+ idling = @resource.create :state => 'idling'
110
+
111
+ assert_equal [parked], @resource.without_state(:idling)
112
+ end
113
+
114
+ def test_should_create_plural_without_scope
115
+ assert @resource.respond_to?(:without_states)
116
+ end
117
+
118
+ def test_should_only_include_records_without_states_in_plural_without_scope
119
+ parked = @resource.create :state => 'parked'
120
+ idling = @resource.create :state => 'idling'
121
+ first_gear = @resource.create :state => 'first_gear'
122
+
123
+ assert_equal [parked, idling], @resource.without_states(:first_gear)
124
+ end
125
+
126
+ def test_should_allow_chaining_scopes
127
+ parked = @resource.create :state => 'parked'
128
+ idling = @resource.create :state => 'idling'
129
+
130
+ assert_equal [idling], @resource.without_state(:parked).with_state(:idling)
131
+ end
132
+
133
+ def test_should_not_rollback_transaction_if_false
134
+ @machine.within_transaction(@resource.new) do
135
+ @resource.create
136
+ false
137
+ end
138
+
139
+ assert_equal 1, @resource.all.size
140
+ end
141
+
142
+ def test_should_not_rollback_transaction_if_true
143
+ @machine.within_transaction(@resource.new) do
144
+ @resource.create
145
+ true
146
+ end
147
+
148
+ assert_equal 1, @resource.all.size
149
+ end
150
+
151
+ def test_should_not_override_the_column_reader
152
+ record = @resource.new
153
+ record.attribute_set(:state, 'parked')
154
+ assert_equal 'parked', record.state
155
+ end
156
+
157
+ def test_should_not_override_the_column_writer
158
+ record = @resource.new
159
+ record.state = 'parked'
160
+ assert_equal 'parked', record.attribute_get(:state)
161
+ end
162
+ end
163
+
164
+ class MachineUnmigratedTest < BaseTestCase
165
+ def setup
166
+ @resource = new_resource(false)
167
+ end
168
+
169
+ def test_should_allow_machine_creation
170
+ assert_nothing_raised { StateMachine::Machine.new(@resource) }
171
+ end
172
+ end
173
+
174
+ class MachineWithInitialStateTest < BaseTestCase
175
+ def setup
176
+ @resource = new_resource
177
+ @machine = StateMachine::Machine.new(@resource, :initial => 'parked')
178
+ @record = @resource.new
179
+ end
180
+
181
+ def test_should_set_initial_state_on_created_object
182
+ assert_equal 'parked', @record.state
183
+ end
184
+ end
185
+
186
+ class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
187
+ def setup
188
+ @resource = new_resource do
189
+ def initialize
190
+ # Skip attribute initialization
191
+ @initialized_state_machines = true
192
+ super
193
+ end
194
+ end
195
+
196
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => 'parked')
197
+ @record = @resource.new
198
+ end
199
+
200
+ def test_should_define_a_new_property_for_the_attribute
201
+ assert_not_nil @resource.properties[:status]
202
+ assert @record.respond_to?(:status)
203
+ assert @record.respond_to?(:status=)
204
+ end
205
+ end
206
+
207
+ class MachineWithColumnDefaultTest < BaseTestCase
208
+ def setup
209
+ @resource = new_resource do
210
+ property :status, String, :default => 'idling'
211
+ auto_migrate!
212
+ end
213
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => :parked)
214
+ @record = @resource.new
215
+ end
216
+
217
+ def test_should_use_machine_default
218
+ assert_equal 'parked', @record.status
219
+ end
220
+ end
221
+
222
+ class MachineWithComplexPluralizationTest < BaseTestCase
223
+ def setup
224
+ @resource = new_resource
225
+ @machine = StateMachine::Machine.new(@resource, :status)
226
+ end
227
+
228
+ def test_should_create_singular_with_scope
229
+ assert @resource.respond_to?(:with_status)
230
+ end
231
+
232
+ def test_should_create_plural_with_scope
233
+ assert @resource.respond_to?(:with_statuses)
234
+ end
235
+ end
236
+
237
+ class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
238
+ def setup
239
+ @resource = new_resource do
240
+ attr_accessor :status
241
+ end
242
+
243
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => 'parked')
244
+ @record = @resource.new
245
+ end
246
+
247
+ def test_should_set_initial_state_on_created_object
248
+ assert_equal 'parked', @record.status
249
+ end
250
+ end
251
+
252
+ class MachineWithOwnerSubclassTest < BaseTestCase
253
+ def setup
254
+ @resource = new_resource
255
+ @machine = StateMachine::Machine.new(@resource, :state)
256
+
257
+ @subclass = Class.new(@resource)
258
+ @subclass_machine = @subclass.state_machine(:state) {}
259
+ @subclass_machine.state :parked, :idling, :first_gear
260
+ end
261
+
262
+ def test_should_only_include_records_with_subclass_states_in_with_scope
263
+ parked = @subclass.create :state => 'parked'
264
+ idling = @subclass.create :state => 'idling'
265
+
266
+ assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
267
+ end
268
+
269
+ def test_should_only_include_records_without_subclass_states_in_without_scope
270
+ parked = @subclass.create :state => 'parked'
271
+ idling = @subclass.create :state => 'idling'
272
+ first_gear = @subclass.create :state => 'first_gear'
273
+
274
+ assert_equal [parked, idling], @subclass.without_states(:first_gear)
275
+ end
276
+ end
277
+
278
+ class MachineWithTransactionsTest < BaseTestCase
279
+ def setup
280
+ @resource = new_resource
281
+ @machine = StateMachine::Machine.new(@resource, :use_transactions => true)
282
+ end
283
+
284
+ def test_should_rollback_transaction_if_false
285
+ @machine.within_transaction(@resource.new) do
286
+ @resource.create
287
+ false
288
+ end
289
+
290
+ assert_equal 0, @resource.all.size
291
+ end
292
+
293
+ def test_should_not_rollback_transaction_if_true
294
+ @machine.within_transaction(@resource.new) do
295
+ @resource.create
296
+ true
297
+ end
298
+
299
+ assert_equal 1, @resource.all.size
300
+ end
301
+ end
302
+
303
+ class MachineWithInitializedStateTest < BaseTestCase
304
+ def setup
305
+ @resource = new_resource
306
+ @machine = StateMachine::Machine.new(@resource, :initial => :parked)
307
+ @machine.state nil, :idling
308
+ end
309
+
310
+ def test_should_allow_nil_initial_state_when_static
311
+ record = @resource.new(:state => nil)
312
+ assert_nil record.state
313
+ end
314
+
315
+ def test_should_allow_nil_initial_state_when_dynamic
316
+ @machine.initial_state = lambda {:parked}
317
+ record = @resource.new(:state => nil)
318
+ assert_nil record.state
319
+ end
320
+
321
+ def test_should_allow_different_initial_state_when_static
322
+ record = @resource.new(:state => 'idling')
323
+ assert_equal 'idling', record.state
324
+ end
325
+
326
+ def test_should_allow_different_initial_state_when_dynamic
327
+ @machine.initial_state = lambda {:parked}
328
+ record = @resource.new(:state => 'idling')
329
+ assert_equal 'idling', record.state
330
+ end
331
+
332
+ def test_should_use_default_state_if_protected
333
+ @resource.class_eval do
334
+ protected :state=
335
+ end
336
+
337
+ assert_raise(ArgumentError) { @resource.new(:state => 'idling') }
338
+ end
339
+ end
340
+
341
+ class MachineWithCallbacksTest < BaseTestCase
342
+ def setup
343
+ @resource = new_resource
344
+ @machine = StateMachine::Machine.new(@resource)
345
+ @machine.state :parked, :idling
346
+ @machine.event :ignite
347
+ @record = @resource.new(:state => 'parked')
348
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
349
+ end
350
+
351
+ def test_should_run_before_callbacks
352
+ called = false
353
+ @machine.before_transition(lambda {called = true})
354
+
355
+ @transition.perform
356
+ assert called
357
+ end
358
+
359
+ def test_should_pass_transition_to_before_callbacks_with_one_argument
360
+ transition = nil
361
+ @machine.before_transition(lambda {|arg| transition = arg})
362
+
363
+ @transition.perform
364
+ assert_equal @transition, transition
365
+ end
366
+
367
+ def test_should_pass_transition_to_before_callbacks_with_multiple_arguments
368
+ callback_args = nil
369
+ @machine.before_transition(lambda {|*args| callback_args = args})
370
+
371
+ @transition.perform
372
+ assert_equal [@transition], callback_args
373
+ end
374
+
375
+ def test_should_run_before_callbacks_within_the_context_of_the_record
376
+ context = nil
377
+ @machine.before_transition(lambda {context = self})
378
+
379
+ @transition.perform
380
+ assert_equal @record, context
381
+ end
382
+
383
+ def test_should_run_after_callbacks
384
+ called = false
385
+ @machine.after_transition(lambda {called = true})
386
+
387
+ @transition.perform
388
+ assert called
389
+ end
390
+
391
+ def test_should_pass_transition_to_after_callbacks_with_multiple_arguments
392
+ callback_args = nil
393
+ @machine.after_transition(lambda {|*args| callback_args = args})
394
+
395
+ @transition.perform
396
+ assert_equal [@transition], callback_args
397
+ end
398
+
399
+ def test_should_run_after_callbacks_with_the_context_of_the_record
400
+ context = nil
401
+ @machine.after_transition(lambda {context = self})
402
+
403
+ @transition.perform
404
+ assert_equal @record, context
405
+ end
406
+
407
+ def test_should_allow_symbolic_callbacks
408
+ callback_args = nil
409
+
410
+ klass = class << @record; self; end
411
+ klass.send(:define_method, :after_ignite) do |*args|
412
+ callback_args = args
413
+ end
414
+
415
+ @machine.before_transition(:after_ignite)
416
+
417
+ @transition.perform
418
+ assert_equal [@transition], callback_args
419
+ end
420
+
421
+ def test_should_allow_string_callbacks
422
+ class << @record
423
+ attr_reader :callback_result
424
+ end
425
+
426
+ @machine.before_transition('@callback_result = [1, 2, 3]')
427
+ @transition.perform
428
+
429
+ assert_equal [1, 2, 3], @record.callback_result
430
+ end
431
+ end
432
+
433
+ class MachineWithLoopbackTest < BaseTestCase
434
+ def setup
435
+ dirty_attributes = nil
436
+
437
+ @resource = new_resource do
438
+ property :updated_at, DateTime
439
+ auto_migrate!
440
+
441
+ # Simulate dm-timestamps
442
+ before :update do
443
+ dirty_attributes = self.dirty_attributes.dup
444
+
445
+ return unless dirty?
446
+ self.updated_at = DateTime.now
447
+ end
448
+ end
449
+
450
+ @machine = StateMachine::Machine.new(@resource, :initial => :parked)
451
+ @machine.event :park
452
+
453
+ @record = @resource.create(:updated_at => Time.now - 1)
454
+ @timestamp = @record.updated_at
455
+
456
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
457
+ @transition.perform
458
+
459
+ @dirty_attributes = dirty_attributes
460
+ end
461
+
462
+ def test_should_include_state_in_dirty_attributes
463
+ expected = {@resource.properties[:state] => 'parked'}
464
+ assert_equal expected, @dirty_attributes
465
+ end
466
+
467
+ def test_should_update_record
468
+ assert_not_equal @timestamp, @record.updated_at
469
+ end
470
+ end
471
+
472
+ begin
473
+ gem 'dm-observer', ENV['DM_VERSION'] ? "=#{ENV['DM_VERSION']}" : '>=0.9.4'
474
+ require 'dm-observer'
475
+
476
+ class MachineWithObserversTest < BaseTestCase
477
+ def setup
478
+ @resource = new_resource
479
+ @machine = StateMachine::Machine.new(@resource)
480
+ @machine.state :parked, :idling
481
+ @machine.event :ignite
482
+ @record = @resource.new(:state => 'parked')
483
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
484
+ end
485
+
486
+ def test_should_provide_matcher_helpers
487
+ matchers = []
488
+
489
+ new_observer(@resource) do
490
+ matchers = [all, any, same]
491
+ end
492
+
493
+ assert_equal [StateMachine::AllMatcher.instance, StateMachine::AllMatcher.instance, StateMachine::LoopbackMatcher.instance], matchers
494
+ end
495
+
496
+ def test_should_call_before_transition_callback_if_requirements_match
497
+ called = false
498
+
499
+ observer = new_observer(@resource) do
500
+ before_transition :from => :parked do
501
+ called = true
502
+ end
503
+ end
504
+
505
+ @transition.perform
506
+ assert called
507
+ end
508
+
509
+ def test_should_not_call_before_transition_callback_if_requirements_do_not_match
510
+ called = false
511
+
512
+ observer = new_observer(@resource) do
513
+ before_transition :from => :idling do
514
+ called = true
515
+ end
516
+ end
517
+
518
+ @transition.perform
519
+ assert !called
520
+ end
521
+
522
+ def test_should_pass_transition_to_before_callbacks
523
+ callback_args = nil
524
+
525
+ observer = new_observer(@resource) do
526
+ before_transition do |*args|
527
+ callback_args = args
528
+ end
529
+ end
530
+
531
+ @transition.perform
532
+ assert_equal [@transition], callback_args
533
+ end
534
+
535
+ def test_should_call_after_transition_callback_if_requirements_match
536
+ called = false
537
+
538
+ observer = new_observer(@resource) do
539
+ after_transition :from => :parked do
540
+ called = true
541
+ end
542
+ end
543
+
544
+ @transition.perform
545
+ assert called
546
+ end
547
+
548
+ def test_should_not_call_after_transition_callback_if_requirements_do_not_match
549
+ called = false
550
+
551
+ observer = new_observer(@resource) do
552
+ after_transition :from => :idling do
553
+ called = true
554
+ end
555
+ end
556
+
557
+ @transition.perform
558
+ assert !called
559
+ end
560
+
561
+ def test_should_pass_transition_to_after_callbacks
562
+ callback_args = nil
563
+
564
+ observer = new_observer(@resource) do
565
+ after_transition do |*args|
566
+ callback_args = args
567
+ end
568
+ end
569
+
570
+ @transition.perform
571
+ assert_equal [@transition], callback_args
572
+ end
573
+
574
+ def test_should_raise_exception_if_targeting_invalid_machine
575
+ assert_raise(RUBY_VERSION < '1.9' ? IndexError : KeyError) do
576
+ new_observer(@resource) do
577
+ before_transition :invalid, :from => :parked do
578
+ end
579
+ end
580
+ end
581
+ end
582
+
583
+ def test_should_allow_targeting_specific_machine
584
+ @second_machine = StateMachine::Machine.new(@resource, :status)
585
+ @resource.auto_migrate!
586
+
587
+ called_state = false
588
+ called_status = false
589
+
590
+ observer = new_observer(@resource) do
591
+ before_transition :state, :from => :parked do
592
+ called_state = true
593
+ end
594
+
595
+ before_transition :status, :from => :parked do
596
+ called_status = true
597
+ end
598
+ end
599
+
600
+ @transition.perform
601
+
602
+ assert called_state
603
+ assert !called_status
604
+ end
605
+
606
+ def test_should_allow_targeting_multiple_specific_machines
607
+ @second_machine = StateMachine::Machine.new(@resource, :status)
608
+ @second_machine.state :parked, :idling
609
+ @second_machine.event :ignite
610
+ @resource.auto_migrate!
611
+
612
+ called_attribute = nil
613
+
614
+ attributes = []
615
+ observer = new_observer(@resource) do
616
+ before_transition :state, :status, :from => :parked do |transition|
617
+ called_attribute = transition.attribute
618
+ end
619
+ end
620
+
621
+ @transition.perform
622
+ assert_equal :state, called_attribute
623
+
624
+ StateMachine::Transition.new(@record, @second_machine, :ignite, :parked, :idling).perform
625
+ assert_equal :status, called_attribute
626
+ end
627
+ end
628
+
629
+ class MachineWithMixedCallbacksTest < BaseTestCase
630
+ def setup
631
+ @resource = new_resource
632
+ @machine = StateMachine::Machine.new(@resource)
633
+ @machine.state :parked, :idling
634
+ @machine.event :ignite
635
+ @record = @resource.new(:state => 'parked')
636
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
637
+
638
+ @notifications = notifications = []
639
+
640
+ # Create callbacks
641
+ @machine.before_transition(lambda {notifications << :callback_before_transition})
642
+ @machine.after_transition(lambda {notifications << :callback_after_transition})
643
+
644
+ observer = new_observer(@resource) do
645
+ before_transition do
646
+ notifications << :observer_before_transition
647
+ end
648
+
649
+ after_transition do
650
+ notifications << :observer_after_transition
651
+ end
652
+ end
653
+
654
+ @transition.perform
655
+ end
656
+
657
+ def test_should_invoke_callbacks_in_specific_order
658
+ expected = [
659
+ :callback_before_transition,
660
+ :observer_before_transition,
661
+ :callback_after_transition,
662
+ :observer_after_transition
663
+ ]
664
+
665
+ assert_equal expected, @notifications
666
+ end
667
+ end
668
+ rescue LoadError
669
+ $stderr.puts "Skipping DataMapper Observer tests. `gem install dm-observer#{" -v #{ENV['DM_VERSION']}" if ENV['DM_VERSION']}` and try again."
670
+ end
671
+
672
+ begin
673
+ gem 'dm-validations', ENV['DM_VERSION'] ? "=#{ENV['DM_VERSION']}" : '>=0.9.4'
674
+ require 'dm-validations'
675
+
676
+ class MachineWithValidationsTest < BaseTestCase
677
+ def setup
678
+ @resource = new_resource
679
+ @machine = StateMachine::Machine.new(@resource)
680
+ @machine.state :parked
681
+
682
+ @record = @resource.new
683
+ end
684
+
685
+ def test_should_invalidate_using_errors
686
+ @record.state = 'parked'
687
+
688
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, :park]])
689
+ assert_equal ['cannot transition via "park"'], @record.errors.on(:state)
690
+ end
691
+
692
+ def test_should_auto_prefix_custom_attributes_on_invalidation
693
+ @machine.invalidate(@record, :event, :invalid)
694
+
695
+ assert_equal ['is invalid'], @record.errors.on(:state_event)
696
+ end
697
+
698
+ def test_should_clear_errors_on_reset
699
+ @record.state = 'parked'
700
+ @record.errors.add(:state, 'is invalid')
701
+
702
+ @machine.reset(@record)
703
+ assert_nil @record.errors.on(:id)
704
+ end
705
+
706
+ def test_should_be_valid_if_state_is_known
707
+ @record.state = 'parked'
708
+
709
+ assert @record.valid?
710
+ end
711
+
712
+ def test_should_not_be_valid_if_state_is_unknown
713
+ @record.state = 'invalid'
714
+
715
+ assert !@record.valid?
716
+ assert_equal ['is invalid'], @record.errors.on(:state)
717
+ end
718
+ end
719
+
720
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
721
+ def setup
722
+ @resource = new_resource
723
+ @machine = StateMachine::Machine.new(@resource, :status, :attribute => :state)
724
+ @machine.state :parked
725
+
726
+ @record = @resource.new
727
+ end
728
+
729
+ def test_should_add_validation_errors_to_custom_attribute
730
+ @record.state = 'invalid'
731
+
732
+ assert !@record.valid?
733
+ assert_equal ['is invalid'], @record.errors.on(:state)
734
+
735
+ @record.state = 'parked'
736
+ assert @record.valid?
737
+ end
738
+ end
739
+
740
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
741
+ def setup
742
+ @resource = new_resource do
743
+ attr_accessor :seatbelt
744
+ end
745
+
746
+ @machine = StateMachine::Machine.new(@resource)
747
+ @machine.state :first_gear, :second_gear do
748
+ validates_present :seatbelt
749
+ end
750
+ @machine.other_states :parked
751
+ end
752
+
753
+ def test_should_be_valid_if_validation_fails_outside_state_scope
754
+ record = @resource.new(:state => 'parked', :seatbelt => nil)
755
+ assert record.valid?
756
+ end
757
+
758
+ def test_should_be_invalid_if_validation_fails_within_state_scope
759
+ record = @resource.new(:state => 'first_gear', :seatbelt => nil)
760
+ assert !record.valid?
761
+ end
762
+
763
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
764
+ record = @resource.new(:state => 'second_gear', :seatbelt => true)
765
+ assert record.valid?
766
+ end
767
+ end
768
+
769
+ class MachineWithEventAttributesOnValidationTest < BaseTestCase
770
+ def setup
771
+ @resource = new_resource
772
+ @machine = StateMachine::Machine.new(@resource)
773
+ @machine.event :ignite do
774
+ transition :parked => :idling
775
+ end
776
+
777
+ @record = @resource.new
778
+ @record.state = 'parked'
779
+ @record.state_event = 'ignite'
780
+ end
781
+
782
+ def test_should_fail_if_event_is_invalid
783
+ @record.state_event = 'invalid'
784
+ assert !@record.valid?
785
+ assert_equal ['is invalid'], @record.errors.full_messages
786
+ end
787
+
788
+ def test_should_fail_if_event_has_no_transition
789
+ @record.state = 'idling'
790
+ assert !@record.valid?
791
+ assert_equal ['cannot transition when idling'], @record.errors.full_messages
792
+ end
793
+
794
+ def test_should_be_successful_if_event_has_transition
795
+ assert @record.valid?
796
+ end
797
+
798
+ def test_should_run_before_callbacks
799
+ ran_callback = false
800
+ @machine.before_transition { ran_callback = true }
801
+
802
+ @record.valid?
803
+ assert ran_callback
804
+ end
805
+
806
+ def test_should_persist_new_state
807
+ @record.valid?
808
+ assert_equal 'idling', @record.state
809
+ end
810
+
811
+ def test_should_not_run_after_callbacks
812
+ ran_callback = false
813
+ @machine.after_transition { ran_callback = true }
814
+
815
+ @record.valid?
816
+ assert !ran_callback
817
+ end
818
+
819
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
820
+ @resource.class_eval do
821
+ attr_accessor :seatbelt
822
+ validates_present :seatbelt
823
+ end
824
+
825
+ ran_callback = false
826
+ @machine.after_transition { ran_callback = true }
827
+
828
+ @record.valid?
829
+ assert !ran_callback
830
+ end
831
+
832
+ def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
833
+ @resource.class_eval do
834
+ attr_accessor :seatbelt
835
+ validates_present :seatbelt
836
+ end
837
+
838
+ ran_callback = false
839
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
840
+
841
+ @record.valid?
842
+ assert ran_callback
843
+ end
844
+ end
845
+
846
+ class MachineWithEventAttributesOnSaveTest < BaseTestCase
847
+ def setup
848
+ @resource = new_resource
849
+ @machine = StateMachine::Machine.new(@resource)
850
+ @machine.event :ignite do
851
+ transition :parked => :idling
852
+ end
853
+
854
+ @record = @resource.new
855
+ @record.state = 'parked'
856
+ @record.state_event = 'ignite'
857
+ end
858
+
859
+ def test_should_fail_if_event_is_invalid
860
+ @record.state_event = 'invalid'
861
+ assert !@record.save
862
+ end
863
+
864
+ def test_should_fail_if_event_has_no_transition
865
+ @record.state = 'idling'
866
+ assert !@record.save
867
+ end
868
+
869
+ def test_should_be_successful_if_event_has_transition
870
+ assert_equal true, @record.save
871
+ end
872
+
873
+ def test_should_run_before_callbacks
874
+ ran_callback = false
875
+ @machine.before_transition { ran_callback = true }
876
+
877
+ @record.save
878
+ assert ran_callback
879
+ end
880
+
881
+ def test_should_run_before_callbacks_once
882
+ before_count = 0
883
+ @machine.before_transition { before_count += 1 }
884
+
885
+ @record.save
886
+ assert_equal 1, before_count
887
+ end
888
+
889
+ def test_should_persist_new_state
890
+ @record.save
891
+ assert_equal 'idling', @record.state
892
+ end
893
+
894
+ def test_should_run_after_callbacks
895
+ ran_callback = false
896
+ @machine.after_transition { ran_callback = true }
897
+
898
+ @record.save
899
+ assert ran_callback
900
+ end
901
+
902
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
903
+ @resource.before(:create) { throw :halt }
904
+
905
+ ran_callback = false
906
+ @machine.after_transition { ran_callback = true }
907
+
908
+ @record.save
909
+ assert !ran_callback
910
+ end
911
+
912
+ def test_should_run_after_callbacks_with_failures_enabled_if_fails
913
+ @resource.before(:create) { throw :halt }
914
+
915
+ ran_callback = false
916
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
917
+
918
+ @record.save
919
+ assert ran_callback
920
+ end
921
+ end
922
+
923
+ class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
924
+ def setup
925
+ @superclass = new_resource do
926
+ def persist
927
+ save
928
+ end
929
+ end
930
+ @resource = Class.new(@superclass)
931
+ @machine = StateMachine::Machine.new(@resource, :action => :persist)
932
+ @machine.event :ignite do
933
+ transition :parked => :idling
934
+ end
935
+
936
+ @record = @resource.new
937
+ @record.state = 'parked'
938
+ @record.state_event = 'ignite'
939
+ end
940
+
941
+ def test_should_not_transition_on_valid?
942
+ @record.valid?
943
+ assert_equal 'parked', @record.state
944
+ end
945
+
946
+ def test_should_not_transition_on_save
947
+ @record.save
948
+ assert_equal 'parked', @record.state
949
+ end
950
+
951
+ def test_should_transition_on_custom_action
952
+ @record.persist
953
+ assert_equal 'idling', @record.state
954
+ end
955
+ end
956
+ rescue LoadError
957
+ $stderr.puts "Skipping DataMapper Validation tests. `gem install dm-validations#{" -v #{ENV['DM_VERSION']}" if ENV['DM_VERSION']}` and try again."
958
+ end
959
+ end
960
+ rescue LoadError
961
+ $stderr.puts "Skipping DataMapper tests. `gem install dm-core#{" -v #{ENV['DM_VERSION']}" if ENV['DM_VERSION']}`, `gem install cucumber rspec hoe launchy do_sqlite3` and try again."
962
+ end