state_machine 0.3.1 → 0.4.0

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