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,486 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ begin
4
+ # Load library
5
+ require 'rubygems'
6
+
7
+ gem 'sequel', ENV['SEQUEL_VERSION'] ? "=#{ENV['SEQUEL_VERSION']}" : '>=2.8.0'
8
+ require 'sequel'
9
+ require 'logger'
10
+
11
+ # Establish database connection
12
+ DB = Sequel.connect('sqlite:///', :loggers => [Logger.new("#{File.dirname(__FILE__)}/../../sequel.log")])
13
+
14
+ module SequelTest
15
+ class BaseTestCase < Test::Unit::TestCase
16
+ def default_test
17
+ end
18
+
19
+ protected
20
+ # Creates a new Sequel model (and the associated table)
21
+ def new_model(auto_migrate = true, &block)
22
+ DB.create_table! :foo do
23
+ primary_key :id
24
+ column :state, :string
25
+ end if auto_migrate
26
+ model = Class.new(Sequel::Model(:foo)) do
27
+ self.raise_on_save_failure = false
28
+ plugin :validation_class_methods if respond_to?(:plugin)
29
+
30
+ def self.name; 'SequelTest::Foo'; end
31
+ end
32
+ model.class_eval(&block) if block_given?
33
+ model
34
+ end
35
+ end
36
+
37
+ class IntegrationTest < BaseTestCase
38
+ def test_should_match_if_class_inherits_from_sequel
39
+ assert StateMachine::Integrations::Sequel.matches?(new_model)
40
+ end
41
+
42
+ def test_should_not_match_if_class_does_not_inherit_from_sequel
43
+ assert !StateMachine::Integrations::Sequel.matches?(Class.new)
44
+ end
45
+ end
46
+
47
+ class MachineByDefaultTest < BaseTestCase
48
+ def setup
49
+ @model = new_model
50
+ @machine = StateMachine::Machine.new(@model)
51
+ end
52
+
53
+ def test_should_use_save_as_action
54
+ assert_equal :save, @machine.action
55
+ end
56
+
57
+ def test_should_use_transactions
58
+ assert_equal true, @machine.use_transactions
59
+ end
60
+ end
61
+
62
+ class MachineTest < BaseTestCase
63
+ def setup
64
+ @model = new_model
65
+ @machine = StateMachine::Machine.new(@model)
66
+ @machine.state :parked, :first_gear
67
+ @machine.state :idling, :value => lambda {'idling'}
68
+ end
69
+
70
+ def test_should_create_singular_with_scope
71
+ assert @model.respond_to?(:with_state)
72
+ end
73
+
74
+ def test_should_only_include_records_with_state_in_singular_with_scope
75
+ parked = @model.create :state => 'parked'
76
+ idling = @model.create :state => 'idling'
77
+
78
+ assert_equal [parked], @model.with_state(:parked).all
79
+ end
80
+
81
+ def test_should_create_plural_with_scope
82
+ assert @model.respond_to?(:with_states)
83
+ end
84
+
85
+ def test_should_only_include_records_with_states_in_plural_with_scope
86
+ parked = @model.create :state => 'parked'
87
+ idling = @model.create :state => 'idling'
88
+
89
+ assert_equal [parked, idling], @model.with_states(:parked, :idling).all
90
+ end
91
+
92
+ def test_should_create_singular_without_scope
93
+ assert @model.respond_to?(:without_state)
94
+ end
95
+
96
+ def test_should_only_include_records_without_state_in_singular_without_scope
97
+ parked = @model.create :state => 'parked'
98
+ idling = @model.create :state => 'idling'
99
+
100
+ assert_equal [parked], @model.without_state(:idling).all
101
+ end
102
+
103
+ def test_should_create_plural_without_scope
104
+ assert @model.respond_to?(:without_states)
105
+ end
106
+
107
+ def test_should_only_include_records_without_states_in_plural_without_scope
108
+ parked = @model.create :state => 'parked'
109
+ idling = @model.create :state => 'idling'
110
+ first_gear = @model.create :state => 'first_gear'
111
+
112
+ assert_equal [parked, idling], @model.without_states(:first_gear).all
113
+ end
114
+
115
+ def test_should_allow_chaining_scopes_and_fitlers
116
+ parked = @model.create :state => 'parked'
117
+ idling = @model.create :state => 'idling'
118
+
119
+ assert_equal [idling], @model.without_state(:parked).filter(:state => 'idling').all
120
+ end
121
+
122
+ def test_should_rollback_transaction_if_false
123
+ @machine.within_transaction(@model.new) do
124
+ @model.create
125
+ false
126
+ end
127
+
128
+ assert_equal 0, @model.count
129
+ end
130
+
131
+ def test_should_not_rollback_transaction_if_true
132
+ @machine.within_transaction(@model.new) do
133
+ @model.create
134
+ true
135
+ end
136
+
137
+ assert_equal 1, @model.count
138
+ end
139
+
140
+ def test_should_invalidate_using_errors
141
+ record = @model.new
142
+ record.state = 'parked'
143
+
144
+ @machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
145
+
146
+ assert_equal ['cannot transition via "park"'], record.errors.on(:state)
147
+ end
148
+
149
+ def test_should_auto_prefix_custom_attributes_on_invalidation
150
+ record = @model.new
151
+ @machine.invalidate(record, :event, :invalid)
152
+
153
+ assert_equal ['is invalid'], record.errors.on(:state_event)
154
+ end
155
+
156
+ def test_should_clear_errors_on_reset
157
+ record = @model.new
158
+ record.state = 'parked'
159
+ record.errors.add(:state, 'is invalid')
160
+
161
+ @machine.reset(record)
162
+ assert_nil record.errors.on(:id)
163
+ end
164
+
165
+ def test_should_not_override_the_column_reader
166
+ record = @model.new
167
+ record[:state] = 'parked'
168
+ assert_equal 'parked', record.state
169
+ end
170
+
171
+ def test_should_not_override_the_column_writer
172
+ record = @model.new
173
+ record.state = 'parked'
174
+ assert_equal 'parked', record[:state]
175
+ end
176
+ end
177
+
178
+ class MachineUnmigratedTest < BaseTestCase
179
+ def setup
180
+ @model = new_model(false)
181
+ end
182
+
183
+ def test_should_allow_machine_creation
184
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
185
+ end
186
+ end
187
+
188
+ class MachineWithInitialStateTest < BaseTestCase
189
+ def setup
190
+ @model = new_model
191
+ @machine = StateMachine::Machine.new(@model, :initial => 'parked')
192
+ @record = @model.new
193
+ end
194
+
195
+ def test_should_set_initial_state_on_created_object
196
+ assert_equal 'parked', @record.state
197
+ end
198
+ end
199
+
200
+ class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
201
+ def setup
202
+ @model = new_model do
203
+ def initialize
204
+ # Skip attribute initialization
205
+ end
206
+ end
207
+
208
+ @machine = StateMachine::Machine.new(@model, :status, :initial => 'parked')
209
+ @record = @model.new
210
+ end
211
+
212
+ def test_should_not_define_a_reader_attribute_for_the_attribute
213
+ assert !@record.respond_to?(:status)
214
+ end
215
+
216
+ def test_should_not_define_a_writer_attribute_for_the_attribute
217
+ assert !@record.respond_to?(:status=)
218
+ end
219
+
220
+ def test_should_define_an_attribute_predicate
221
+ assert @record.respond_to?(:status?)
222
+ end
223
+ end
224
+
225
+ class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
226
+ def setup
227
+ @model = new_model do
228
+ attr_accessor :status
229
+ end
230
+
231
+ @machine = StateMachine::Machine.new(@model, :status, :initial => 'parked')
232
+ @record = @model.new
233
+ end
234
+
235
+ def test_should_set_initial_state_on_created_object
236
+ assert_equal 'parked', @record.status
237
+ end
238
+ end
239
+
240
+ class MachineWithComplexPluralizationTest < BaseTestCase
241
+ def setup
242
+ @model = new_model
243
+ @machine = StateMachine::Machine.new(@model, :status)
244
+ end
245
+
246
+ def test_should_create_singular_with_scope
247
+ assert @model.respond_to?(:with_status)
248
+ end
249
+
250
+ def test_should_create_plural_with_scope
251
+ assert @model.respond_to?(:with_statuses)
252
+ end
253
+ end
254
+
255
+ class MachineWithCallbacksTest < BaseTestCase
256
+ def setup
257
+ @model = new_model
258
+ @machine = StateMachine::Machine.new(@model)
259
+ @machine.state :parked, :idling
260
+ @machine.event :ignite
261
+ @record = @model.new(:state => 'parked')
262
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
263
+ end
264
+
265
+ def test_should_run_before_callbacks
266
+ called = false
267
+ @machine.before_transition(lambda {called = true})
268
+
269
+ @transition.perform
270
+ assert called
271
+ end
272
+
273
+ def test_should_pass_transition_to_before_callbacks_with_one_argument
274
+ transition = nil
275
+ @machine.before_transition(lambda {|arg| transition = arg})
276
+
277
+ @transition.perform
278
+ assert_equal @transition, transition
279
+ end
280
+
281
+ def test_should_pass_transition_to_before_callbacks_with_multiple_arguments
282
+ callback_args = nil
283
+ @machine.before_transition(lambda {|*args| callback_args = args})
284
+
285
+ @transition.perform
286
+ assert_equal [@transition], callback_args
287
+ end
288
+
289
+ def test_should_run_before_callbacks_within_the_context_of_the_record
290
+ context = nil
291
+ @machine.before_transition(lambda {context = self})
292
+
293
+ @transition.perform
294
+ assert_equal @record, context
295
+ end
296
+
297
+ def test_should_run_after_callbacks
298
+ called = false
299
+ @machine.after_transition(lambda {called = true})
300
+
301
+ @transition.perform
302
+ assert called
303
+ end
304
+
305
+ def test_should_pass_transition_to_after_callbacks_with_multiple_arguments
306
+ callback_args = nil
307
+ @machine.after_transition(lambda {|*args| callback_args = args})
308
+
309
+ @transition.perform
310
+ assert_equal [@transition], callback_args
311
+ end
312
+
313
+ def test_should_run_after_callbacks_with_the_context_of_the_record
314
+ context = nil
315
+ @machine.after_transition(lambda {context = self})
316
+
317
+ @transition.perform
318
+ assert_equal @record, context
319
+ end
320
+
321
+ def test_should_allow_symbolic_callbacks
322
+ callback_args = nil
323
+
324
+ klass = class << @record; self; end
325
+ klass.send(:define_method, :after_ignite) do |*args|
326
+ callback_args = args
327
+ end
328
+
329
+ @machine.before_transition(:after_ignite)
330
+
331
+ @transition.perform
332
+ assert_equal [@transition], callback_args
333
+ end
334
+
335
+ def test_should_allow_string_callbacks
336
+ class << @record
337
+ attr_reader :callback_result
338
+ end
339
+
340
+ @machine.before_transition('@callback_result = [1, 2, 3]')
341
+ @transition.perform
342
+
343
+ assert_equal [1, 2, 3], @record.callback_result
344
+ end
345
+ end
346
+
347
+ class MachineWithValidationsTest < BaseTestCase
348
+ def setup
349
+ @model = new_model
350
+ @machine = StateMachine::Machine.new(@model)
351
+ @machine.state :parked
352
+
353
+ @record = @model.new
354
+ end
355
+
356
+ def test_should_be_valid_if_state_is_known
357
+ @record.state = 'parked'
358
+
359
+ assert @record.valid?
360
+ end
361
+
362
+ def test_should_not_be_valid_if_state_is_unknown
363
+ @record.state = 'invalid'
364
+
365
+ assert !@record.valid?
366
+ assert_equal ['state is invalid'], @record.errors.full_messages
367
+ end
368
+ end
369
+
370
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
371
+ def setup
372
+ @model = new_model do
373
+ attr_accessor :seatbelt
374
+ end
375
+
376
+ @machine = StateMachine::Machine.new(@model)
377
+ @machine.state :first_gear do
378
+ validates_presence_of :seatbelt
379
+ end
380
+ @machine.other_states :parked
381
+ end
382
+
383
+ def test_should_be_valid_if_validation_fails_outside_state_scope
384
+ record = @model.new(:state => 'parked', :seatbelt => nil)
385
+ assert record.valid?
386
+ end
387
+
388
+ def test_should_be_invalid_if_validation_fails_within_state_scope
389
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
390
+ assert !record.valid?
391
+ end
392
+
393
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
394
+ record = @model.new(:state => 'first_gear', :seatbelt => true)
395
+ assert record.valid?
396
+ end
397
+ end
398
+
399
+ class MachineWithEventAttributesOnValidationTest < BaseTestCase
400
+ def setup
401
+ @model = new_model
402
+ @machine = StateMachine::Machine.new(@model)
403
+ @machine.event :ignite do
404
+ transition :parked => :idling
405
+ end
406
+
407
+ @record = @model.new
408
+ @record.state = 'parked'
409
+ @record.state_event = 'ignite'
410
+ end
411
+
412
+ def test_should_fail_if_event_is_invalid
413
+ @record.state_event = 'invalid'
414
+ assert !@record.valid?
415
+ assert_equal ['state_event is invalid'], @record.errors.full_messages
416
+ end
417
+
418
+ def test_should_fail_if_event_has_no_transition
419
+ @record.state = 'idling'
420
+ assert !@record.valid?
421
+ assert_equal ['state_event cannot transition when idling'], @record.errors.full_messages
422
+ end
423
+
424
+ def test_should_be_successful_if_event_has_transition
425
+ assert @record.valid?
426
+ end
427
+
428
+ def test_should_run_before_callbacks
429
+ ran_callback = false
430
+ @machine.before_transition { ran_callback = true }
431
+
432
+ @record.valid?
433
+ assert ran_callback
434
+ end
435
+
436
+ def test_should_persist_new_state
437
+ @record.valid?
438
+ assert_equal 'idling', @record.state
439
+ end
440
+
441
+ def test_should_not_run_after_callbacks
442
+ ran_callback = false
443
+ @machine.after_transition { ran_callback = true }
444
+
445
+ @record.valid?
446
+ assert !ran_callback
447
+ end
448
+ end
449
+
450
+ class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
451
+ def setup
452
+ @superclass = new_model do
453
+ def persist
454
+ save
455
+ end
456
+ end
457
+ @model = Class.new(@superclass)
458
+ @machine = StateMachine::Machine.new(@model, :action => :persist)
459
+ @machine.event :ignite do
460
+ transition :parked => :idling
461
+ end
462
+
463
+ @record = @model.new
464
+ @record.state = 'parked'
465
+ @record.state_event = 'ignite'
466
+ end
467
+
468
+ def test_should_not_transition_on_valid?
469
+ @record.valid?
470
+ assert_equal 'parked', @record.state
471
+ end
472
+
473
+ def test_should_not_transition_on_save
474
+ @record.save
475
+ assert_equal 'parked', @record.state
476
+ end
477
+
478
+ def test_should_transition_on_custom_action
479
+ @record.persist
480
+ assert_equal 'idling', @record.state
481
+ end
482
+ end
483
+ end
484
+ rescue LoadError
485
+ $stderr.puts "Skipping Sequel tests. `gem install sequel#{" -v #{ENV['SEQUEL_VERSION']}" if ENV['SEQUEL_VERSION']}` and try again."
486
+ end