state_machine 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/CHANGELOG.rdoc +26 -0
  2. data/README.rdoc +254 -46
  3. data/Rakefile +29 -3
  4. data/examples/AutoShop_state.png +0 -0
  5. data/examples/Car_state.jpg +0 -0
  6. data/examples/Vehicle_state.png +0 -0
  7. data/lib/state_machine.rb +161 -116
  8. data/lib/state_machine/assertions.rb +21 -0
  9. data/lib/state_machine/callback.rb +168 -0
  10. data/lib/state_machine/eval_helpers.rb +67 -0
  11. data/lib/state_machine/event.rb +135 -101
  12. data/lib/state_machine/extensions.rb +83 -0
  13. data/lib/state_machine/guard.rb +115 -0
  14. data/lib/state_machine/integrations/active_record.rb +242 -0
  15. data/lib/state_machine/integrations/data_mapper.rb +198 -0
  16. data/lib/state_machine/integrations/data_mapper/observer.rb +153 -0
  17. data/lib/state_machine/integrations/sequel.rb +169 -0
  18. data/lib/state_machine/machine.rb +746 -352
  19. data/lib/state_machine/transition.rb +104 -212
  20. data/test/active_record.log +34865 -0
  21. data/test/classes/switch.rb +11 -0
  22. data/test/data_mapper.log +14015 -0
  23. data/test/functional/state_machine_test.rb +249 -15
  24. data/test/sequel.log +3835 -0
  25. data/test/test_helper.rb +3 -12
  26. data/test/unit/assertions_test.rb +13 -0
  27. data/test/unit/callback_test.rb +189 -0
  28. data/test/unit/eval_helpers_test.rb +92 -0
  29. data/test/unit/event_test.rb +247 -113
  30. data/test/unit/guard_test.rb +420 -0
  31. data/test/unit/integrations/active_record_test.rb +515 -0
  32. data/test/unit/integrations/data_mapper_test.rb +407 -0
  33. data/test/unit/integrations/sequel_test.rb +244 -0
  34. data/test/unit/invalid_transition_test.rb +1 -1
  35. data/test/unit/machine_test.rb +1056 -98
  36. data/test/unit/state_machine_test.rb +14 -113
  37. data/test/unit/transition_test.rb +269 -495
  38. metadata +44 -30
  39. data/test/app_root/app/models/auto_shop.rb +0 -34
  40. data/test/app_root/app/models/car.rb +0 -19
  41. data/test/app_root/app/models/highway.rb +0 -3
  42. data/test/app_root/app/models/motorcycle.rb +0 -3
  43. data/test/app_root/app/models/switch.rb +0 -23
  44. data/test/app_root/app/models/switch_observer.rb +0 -20
  45. data/test/app_root/app/models/toggle_switch.rb +0 -2
  46. data/test/app_root/app/models/vehicle.rb +0 -78
  47. data/test/app_root/config/environment.rb +0 -7
  48. data/test/app_root/db/migrate/001_create_switches.rb +0 -12
  49. data/test/app_root/db/migrate/002_create_auto_shops.rb +0 -13
  50. data/test/app_root/db/migrate/003_create_highways.rb +0 -11
  51. data/test/app_root/db/migrate/004_create_vehicles.rb +0 -16
  52. data/test/factory.rb +0 -77
@@ -0,0 +1,407 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ begin
4
+ # Load library
5
+ require 'rubygems'
6
+ require 'dm-core'
7
+ require 'dm-observer'
8
+ require 'dm-aggregates'
9
+
10
+ # Establish database connection
11
+ DataMapper.setup(:default, 'sqlite3::memory:')
12
+ DataObjects::Sqlite3.logger = DataObjects::Logger.new("#{File.dirname(__FILE__)}/../../data_mapper.log", 0)
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, Integer, :serial => true
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
+ end
68
+
69
+ class MachineTest < BaseTestCase
70
+ def setup
71
+ @resource = new_resource
72
+ @machine = StateMachine::Machine.new(@resource)
73
+ end
74
+
75
+ def test_should_create_singular_with_scope
76
+ assert @resource.respond_to?(:with_state)
77
+ end
78
+
79
+ def test_should_only_include_records_with_state_in_singular_with_scope
80
+ off = @resource.create :state => 'off'
81
+ on = @resource.create :state => 'on'
82
+
83
+ assert_equal [off], @resource.with_state('off')
84
+ end
85
+
86
+ def test_should_create_plural_with_scope
87
+ assert @resource.respond_to?(:with_states)
88
+ end
89
+
90
+ def test_should_only_include_records_with_states_in_plural_with_scope
91
+ off = @resource.create :state => 'off'
92
+ on = @resource.create :state => 'on'
93
+
94
+ assert_equal [off, on], @resource.with_states('off', 'on')
95
+ end
96
+
97
+ def test_should_create_singular_without_scope
98
+ assert @resource.respond_to?(:without_state)
99
+ end
100
+
101
+ def test_should_only_include_records_without_state_in_singular_without_scope
102
+ off = @resource.create :state => 'off'
103
+ on = @resource.create :state => 'on'
104
+
105
+ assert_equal [off], @resource.without_state('on')
106
+ end
107
+
108
+ def test_should_create_plural_without_scope
109
+ assert @resource.respond_to?(:without_states)
110
+ end
111
+
112
+ def test_should_only_include_records_without_states_in_plural_without_scope
113
+ off = @resource.create :state => 'off'
114
+ on = @resource.create :state => 'on'
115
+ error = @resource.create :state => 'error'
116
+
117
+ assert_equal [off, on], @resource.without_states('error')
118
+ end
119
+
120
+ def test_should_rollback_transaction_if_false
121
+ @machine.within_transaction(@resource.new) do
122
+ @resource.create
123
+ false
124
+ end
125
+
126
+ assert_equal 0, @resource.count
127
+ end
128
+
129
+ def test_should_not_rollback_transaction_if_true
130
+ @machine.within_transaction(@resource.new) do
131
+ @resource.create
132
+ true
133
+ end
134
+
135
+ assert_equal 1, @resource.count
136
+ end
137
+
138
+ def test_should_not_override_the_column_reader
139
+ record = @resource.new
140
+ record.attribute_set(:state, 'off')
141
+ assert_equal 'off', record.state
142
+ end
143
+
144
+ def test_should_not_override_the_column_writer
145
+ record = @resource.new
146
+ record.state = 'off'
147
+ assert_equal 'off', record.attribute_get(:state)
148
+ end
149
+ end
150
+
151
+ class MachineUnmigratedTest < BaseTestCase
152
+ def setup
153
+ @resource = new_resource(false)
154
+ end
155
+
156
+ def test_should_allow_machine_creation
157
+ assert_nothing_raised { StateMachine::Machine.new(@resource) }
158
+ end
159
+ end
160
+
161
+ class MachineWithInitialStateTest < BaseTestCase
162
+ def setup
163
+ @resource = new_resource
164
+ @machine = StateMachine::Machine.new(@resource, :initial => 'off')
165
+ @record = @resource.new
166
+ end
167
+
168
+ def test_should_set_initial_state_on_created_object
169
+ assert_equal 'off', @record.state
170
+ end
171
+ end
172
+
173
+ class MachineWithNonColumnStateAttributeTest < BaseTestCase
174
+ def setup
175
+ @resource = new_resource
176
+ @machine = StateMachine::Machine.new(@resource, :status, :initial => 'off')
177
+ @record = @resource.new
178
+ end
179
+
180
+ def test_should_define_a_reader_attribute_for_the_attribute
181
+ assert @record.respond_to?(:status)
182
+ end
183
+
184
+ def test_should_define_a_writer_attribute_for_the_attribute
185
+ assert @record.respond_to?(:status=)
186
+ end
187
+
188
+ def test_should_set_initial_state_on_created_object
189
+ assert_equal 'off', @record.status
190
+ end
191
+ end
192
+
193
+ class MachineWithCallbacksTest < BaseTestCase
194
+ def setup
195
+ @resource = new_resource
196
+ @machine = StateMachine::Machine.new(@resource)
197
+ @record = @resource.new(:state => 'off')
198
+ @transition = StateMachine::Transition.new(@record, @machine, 'turn_on', 'off', 'on')
199
+ end
200
+
201
+ def test_should_run_before_callbacks
202
+ called = false
203
+ @machine.before_transition(lambda {called = true})
204
+
205
+ @transition.perform
206
+ assert called
207
+ end
208
+
209
+ def test_should_pass_transition_into_before_callbacks_with_one_argument
210
+ transition = nil
211
+ @machine.before_transition(lambda {|arg| transition = arg})
212
+
213
+ @transition.perform
214
+ assert_equal @transition, transition
215
+ end
216
+
217
+ def test_should_pass_transition_into_before_callbacks_with_multiple_arguments
218
+ callback_args = nil
219
+ @machine.before_transition(lambda {|*args| callback_args = args})
220
+
221
+ @transition.perform
222
+ assert_equal [@transition], callback_args
223
+ end
224
+
225
+ def test_should_run_before_callbacks_within_the_context_of_the_record
226
+ context = nil
227
+ @machine.before_transition(lambda {context = self})
228
+
229
+ @transition.perform
230
+ assert_equal @record, context
231
+ end
232
+
233
+ def test_should_run_after_callbacks
234
+ called = false
235
+ @machine.after_transition(lambda {called = true})
236
+
237
+ @transition.perform
238
+ assert called
239
+ end
240
+
241
+ def test_should_pass_transition_and_result_into_after_callbacks_with_multiple_arguments
242
+ callback_args = nil
243
+ @machine.after_transition(lambda {|*args| callback_args = args})
244
+
245
+ @transition.perform
246
+ assert_equal [@transition, true], callback_args
247
+ end
248
+
249
+ def test_should_run_after_callbacks_with_the_context_of_the_record
250
+ context = nil
251
+ @machine.after_transition(lambda {context = self})
252
+
253
+ @transition.perform
254
+ assert_equal @record, context
255
+ end
256
+ end
257
+
258
+ class MachineWithObserversTest < BaseTestCase
259
+ def setup
260
+ @resource = new_resource
261
+ @machine = StateMachine::Machine.new(@resource)
262
+ @record = @resource.new(:state => 'off')
263
+ @transition = StateMachine::Transition.new(@record, @machine, 'turn_on', 'off', 'on')
264
+ end
265
+
266
+ def test_should_call_before_transition_callback_if_requirements_match
267
+ called = false
268
+
269
+ observer = new_observer(@resource) do
270
+ before_transition :from => 'off' do
271
+ called = true
272
+ end
273
+ end
274
+
275
+ @transition.perform
276
+ assert called
277
+ end
278
+
279
+ def test_should_not_call_before_transition_callback_if_requirements_do_not_match
280
+ called = false
281
+
282
+ observer = new_observer(@resource) do
283
+ before_transition :from => 'on' do
284
+ called = true
285
+ end
286
+ end
287
+
288
+ @transition.perform
289
+ assert !called
290
+ end
291
+
292
+ def test_should_allow_targeting_specific_machine
293
+ @second_machine = StateMachine::Machine.new(@resource, :status)
294
+
295
+ called_state = false
296
+ called_status = false
297
+
298
+ observer = new_observer(@resource) do
299
+ before_transition :state, :from => 'off' do
300
+ called_state = true
301
+ end
302
+
303
+ before_transition :status, :from => 'off' do
304
+ called_status = true
305
+ end
306
+ end
307
+
308
+ @transition.perform
309
+
310
+ assert called_state
311
+ assert !called_status
312
+ end
313
+
314
+ def test_should_pass_transition_to_before_callbacks
315
+ callback_args = nil
316
+
317
+ observer = new_observer(@resource) do
318
+ before_transition do |*args|
319
+ callback_args = args
320
+ end
321
+ end
322
+
323
+ @transition.perform
324
+ assert_equal [@transition], callback_args
325
+ end
326
+
327
+ def test_should_call_after_transition_callback_if_requirements_match
328
+ called = false
329
+
330
+ observer = new_observer(@resource) do
331
+ after_transition :from => 'off' do
332
+ called = true
333
+ end
334
+ end
335
+
336
+ @transition.perform
337
+ assert called
338
+ end
339
+
340
+ def test_should_not_call_after_transition_callback_if_requirements_do_not_match
341
+ called = false
342
+
343
+ observer = new_observer(@resource) do
344
+ after_transition :from => 'on' do
345
+ called = true
346
+ end
347
+ end
348
+
349
+ @transition.perform
350
+ assert !called
351
+ end
352
+
353
+ def test_should_pass_transition_and_result_to_before_callbacks
354
+ callback_args = nil
355
+
356
+ observer = new_observer(@resource) do
357
+ after_transition do |*args|
358
+ callback_args = args
359
+ end
360
+ end
361
+
362
+ @transition.perform
363
+ assert_equal [@transition, true], callback_args
364
+ end
365
+ end
366
+
367
+ class MachineWithMixedCallbacksTest < BaseTestCase
368
+ def setup
369
+ @resource = new_resource
370
+ @machine = StateMachine::Machine.new(@resource)
371
+ @record = @resource.new(:state => 'off')
372
+ @transition = StateMachine::Transition.new(@record, @machine, 'turn_on', 'off', 'on')
373
+
374
+ @notifications = notifications = []
375
+
376
+ # Create callbacks
377
+ @machine.before_transition(lambda {notifications << :callback_before_transition})
378
+ @machine.after_transition(lambda {notifications << :callback_after_transition})
379
+
380
+ observer = new_observer(@resource) do
381
+ before_transition do
382
+ notifications << :observer_before_transition
383
+ end
384
+
385
+ after_transition do
386
+ notifications << :observer_after_transition
387
+ end
388
+ end
389
+
390
+ @transition.perform
391
+ end
392
+
393
+ def test_should_invoke_callbacks_in_specific_order
394
+ expected = [
395
+ :callback_before_transition,
396
+ :observer_before_transition,
397
+ :callback_after_transition,
398
+ :observer_after_transition
399
+ ]
400
+
401
+ assert_equal expected, @notifications
402
+ end
403
+ end
404
+ end
405
+ rescue LoadError
406
+ $stderr.puts 'Skipping DataMapper tests. `gem install dm-core dm-observer dm-aggregates` and try again.'
407
+ end
@@ -0,0 +1,244 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ begin
4
+ # Load library
5
+ require 'rubygems'
6
+ require 'sequel'
7
+ require 'logger'
8
+
9
+ # Establish database connection
10
+ DB = Sequel.connect('sqlite:///', :loggers => [Logger.new("#{File.dirname(__FILE__)}/../../sequel.log")])
11
+
12
+ module SequelTest
13
+ class BaseTestCase < Test::Unit::TestCase
14
+ def default_test
15
+ end
16
+
17
+ protected
18
+ # Creates a new Sequel model (and the associated table)
19
+ def new_model(auto_migrate = true, &block)
20
+ DB.create_table! :foo do
21
+ primary_key :id
22
+ column :state, :string
23
+ end if auto_migrate
24
+ model = Class.new(Sequel::Model(:foo)) do
25
+ def self.name; 'SequelTest::Foo'; end
26
+ end
27
+ model.class_eval(&block) if block_given?
28
+ model
29
+ end
30
+ end
31
+
32
+ class IntegrationTest < BaseTestCase
33
+ def test_should_match_if_class_inherits_from_sequel
34
+ assert StateMachine::Integrations::Sequel.matches?(new_model)
35
+ end
36
+
37
+ def test_should_not_match_if_class_does_not_inherit_from_sequel
38
+ assert !StateMachine::Integrations::Sequel.matches?(Class.new)
39
+ end
40
+ end
41
+
42
+ class MachineByDefaultTest < BaseTestCase
43
+ def setup
44
+ @model = new_model
45
+ @machine = StateMachine::Machine.new(@model)
46
+ end
47
+
48
+ def test_should_use_save_as_action
49
+ assert_equal :save, @machine.action
50
+ end
51
+ end
52
+
53
+ class MachineTest < BaseTestCase
54
+ def setup
55
+ @model = new_model
56
+ @machine = StateMachine::Machine.new(@model)
57
+ end
58
+
59
+ def test_should_create_singular_with_scope
60
+ assert @model.respond_to?(:with_state)
61
+ end
62
+
63
+ def test_should_only_include_records_with_state_in_singular_with_scope
64
+ off = @model.create :state => 'off'
65
+ on = @model.create :state => 'on'
66
+
67
+ assert_equal [off], @model.with_state('off').all
68
+ end
69
+
70
+ def test_should_create_plural_with_scope
71
+ assert @model.respond_to?(:with_states)
72
+ end
73
+
74
+ def test_should_only_include_records_with_states_in_plural_with_scope
75
+ off = @model.create :state => 'off'
76
+ on = @model.create :state => 'on'
77
+
78
+ assert_equal [off, on], @model.with_states('off', 'on').all
79
+ end
80
+
81
+ def test_should_create_singular_without_scope
82
+ assert @model.respond_to?(:without_state)
83
+ end
84
+
85
+ def test_should_only_include_records_without_state_in_singular_without_scope
86
+ off = @model.create :state => 'off'
87
+ on = @model.create :state => 'on'
88
+
89
+ assert_equal [off], @model.without_state('on').all
90
+ end
91
+
92
+ def test_should_create_plural_without_scope
93
+ assert @model.respond_to?(:without_states)
94
+ end
95
+
96
+ def test_should_only_include_records_without_states_in_plural_without_scope
97
+ off = @model.create :state => 'off'
98
+ on = @model.create :state => 'on'
99
+ error = @model.create :state => 'error'
100
+
101
+ assert_equal [off, on], @model.without_states('error').all
102
+ end
103
+
104
+ def test_should_rollback_transaction_if_false
105
+ @machine.within_transaction(@model.new) do
106
+ @model.create
107
+ false
108
+ end
109
+
110
+ assert_equal 0, @model.count
111
+ end
112
+
113
+ def test_should_not_rollback_transaction_if_true
114
+ @machine.within_transaction(@model.new) do
115
+ @model.create
116
+ true
117
+ end
118
+
119
+ assert_equal 1, @model.count
120
+ end
121
+
122
+ def test_should_not_override_the_column_reader
123
+ record = @model.new
124
+ record[:state] = 'off'
125
+ assert_equal 'off', record.state
126
+ end
127
+
128
+ def test_should_not_override_the_column_writer
129
+ record = @model.new
130
+ record.state = 'off'
131
+ assert_equal 'off', record[:state]
132
+ end
133
+ end
134
+
135
+ class MachineUnmigratedTest < BaseTestCase
136
+ def setup
137
+ @model = new_model(false)
138
+ end
139
+
140
+ def test_should_allow_machine_creation
141
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
142
+ end
143
+ end
144
+
145
+ class MachineWithInitialStateTest < BaseTestCase
146
+ def setup
147
+ @model = new_model
148
+ @machine = StateMachine::Machine.new(@model, :initial => 'off')
149
+ @record = @model.new
150
+ end
151
+
152
+ def test_should_set_initial_state_on_created_object
153
+ assert_equal 'off', @record.state
154
+ end
155
+ end
156
+
157
+ class MachineWithNonColumnStateAttributeTest < BaseTestCase
158
+ def setup
159
+ @model = new_model
160
+ @machine = StateMachine::Machine.new(@model, :status, :initial => 'off')
161
+ @record = @model.new
162
+ end
163
+
164
+ def test_should_define_a_reader_attribute_for_the_attribute
165
+ assert @record.respond_to?(:status)
166
+ end
167
+
168
+ def test_should_define_a_writer_attribute_for_the_attribute
169
+ assert @record.respond_to?(:status=)
170
+ end
171
+
172
+ def test_should_set_initial_state_on_created_object
173
+ assert_equal 'off', @record.status
174
+ end
175
+ end
176
+
177
+ class MachineWithCallbacksTest < BaseTestCase
178
+ def setup
179
+ @model = new_model
180
+ @machine = StateMachine::Machine.new(@model)
181
+ @record = @model.new(:state => 'off')
182
+ @transition = StateMachine::Transition.new(@record, @machine, 'turn_on', 'off', 'on')
183
+ end
184
+
185
+ def test_should_run_before_callbacks
186
+ called = false
187
+ @machine.before_transition(lambda {called = true})
188
+
189
+ @transition.perform
190
+ assert called
191
+ end
192
+
193
+ def test_should_pass_transition_into_before_callbacks_with_one_argument
194
+ transition = nil
195
+ @machine.before_transition(lambda {|arg| transition = arg})
196
+
197
+ @transition.perform
198
+ assert_equal @transition, transition
199
+ end
200
+
201
+ def test_should_pass_transition_into_before_callbacks_with_multiple_arguments
202
+ callback_args = nil
203
+ @machine.before_transition(lambda {|*args| callback_args = args})
204
+
205
+ @transition.perform
206
+ assert_equal [@transition], callback_args
207
+ end
208
+
209
+ def test_should_run_before_callbacks_within_the_context_of_the_record
210
+ context = nil
211
+ @machine.before_transition(lambda {context = self})
212
+
213
+ @transition.perform
214
+ assert_equal @record, context
215
+ end
216
+
217
+ def test_should_run_after_callbacks
218
+ called = false
219
+ @machine.after_transition(lambda {called = true})
220
+
221
+ @transition.perform
222
+ assert called
223
+ end
224
+
225
+ def test_should_pass_transition_and_result_into_after_callbacks_with_multiple_arguments
226
+ callback_args = nil
227
+ @machine.after_transition(lambda {|*args| callback_args = args})
228
+
229
+ @transition.perform
230
+ assert_equal [@transition, @record], callback_args
231
+ end
232
+
233
+ def test_should_run_after_callbacks_with_the_context_of_the_record
234
+ context = nil
235
+ @machine.after_transition(lambda {context = self})
236
+
237
+ @transition.perform
238
+ assert_equal @record, context
239
+ end
240
+ end
241
+ end
242
+ rescue LoadError
243
+ $stderr.puts 'Skipping Sequel tests. `gem install sequel` and try again.'
244
+ end