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,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