state_machine 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/CHANGELOG.rdoc +31 -1
  2. data/README.rdoc +33 -21
  3. data/Rakefile +2 -2
  4. data/examples/merb-rest/controller.rb +51 -0
  5. data/examples/merb-rest/model.rb +28 -0
  6. data/examples/merb-rest/view_edit.html.erb +24 -0
  7. data/examples/merb-rest/view_index.html.erb +23 -0
  8. data/examples/merb-rest/view_new.html.erb +13 -0
  9. data/examples/merb-rest/view_show.html.erb +17 -0
  10. data/examples/rails-rest/controller.rb +43 -0
  11. data/examples/rails-rest/migration.rb +11 -0
  12. data/examples/rails-rest/model.rb +23 -0
  13. data/examples/rails-rest/view_edit.html.erb +25 -0
  14. data/examples/rails-rest/view_index.html.erb +23 -0
  15. data/examples/rails-rest/view_new.html.erb +14 -0
  16. data/examples/rails-rest/view_show.html.erb +17 -0
  17. data/lib/state_machine/assertions.rb +2 -2
  18. data/lib/state_machine/callback.rb +14 -8
  19. data/lib/state_machine/condition_proxy.rb +3 -3
  20. data/lib/state_machine/event.rb +19 -21
  21. data/lib/state_machine/event_collection.rb +114 -0
  22. data/lib/state_machine/extensions.rb +127 -11
  23. data/lib/state_machine/guard.rb +1 -1
  24. data/lib/state_machine/integrations/active_record/locale.rb +2 -1
  25. data/lib/state_machine/integrations/active_record.rb +117 -39
  26. data/lib/state_machine/integrations/data_mapper/observer.rb +20 -64
  27. data/lib/state_machine/integrations/data_mapper.rb +71 -26
  28. data/lib/state_machine/integrations/sequel.rb +69 -21
  29. data/lib/state_machine/machine.rb +267 -139
  30. data/lib/state_machine/machine_collection.rb +145 -0
  31. data/lib/state_machine/matcher.rb +2 -2
  32. data/lib/state_machine/node_collection.rb +9 -4
  33. data/lib/state_machine/state.rb +22 -32
  34. data/lib/state_machine/state_collection.rb +66 -17
  35. data/lib/state_machine/transition.rb +259 -28
  36. data/lib/state_machine.rb +121 -56
  37. data/tasks/state_machine.rake +1 -0
  38. data/tasks/state_machine.rb +26 -0
  39. data/test/active_record.log +116877 -0
  40. data/test/functional/state_machine_test.rb +118 -12
  41. data/test/sequel.log +28542 -0
  42. data/test/unit/callback_test.rb +46 -1
  43. data/test/unit/condition_proxy_test.rb +55 -28
  44. data/test/unit/event_collection_test.rb +228 -0
  45. data/test/unit/event_test.rb +51 -46
  46. data/test/unit/integrations/active_record_test.rb +128 -70
  47. data/test/unit/integrations/data_mapper_test.rb +150 -58
  48. data/test/unit/integrations/sequel_test.rb +63 -6
  49. data/test/unit/invalid_event_test.rb +7 -0
  50. data/test/unit/machine_collection_test.rb +678 -0
  51. data/test/unit/machine_test.rb +198 -91
  52. data/test/unit/node_collection_test.rb +33 -30
  53. data/test/unit/state_collection_test.rb +112 -5
  54. data/test/unit/state_test.rb +23 -3
  55. data/test/unit/transition_test.rb +750 -89
  56. metadata +28 -3
@@ -3,7 +3,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
3
3
  class CallbackTest < Test::Unit::TestCase
4
4
  def test_should_raise_exception_if_do_option_not_specified
5
5
  exception = assert_raise(ArgumentError) { StateMachine::Callback.new }
6
- assert_match ':do callback must be specified', exception.message
6
+ assert_equal ':do callback must be specified', exception.message
7
7
  end
8
8
 
9
9
  def test_should_not_raise_exception_if_do_option_specified
@@ -17,6 +17,10 @@ class CallbackTest < Test::Unit::TestCase
17
17
  def test_should_not_bind_to_objects
18
18
  assert !StateMachine::Callback.bind_to_object
19
19
  end
20
+
21
+ def test_should_not_have_a_terminator
22
+ assert_nil StateMachine::Callback.terminator
23
+ end
20
24
  end
21
25
 
22
26
  class CallbackByDefaultTest < Test::Unit::TestCase
@@ -236,3 +240,44 @@ class CallbackWithBoundObjectTest < Test::Unit::TestCase
236
240
  assert_equal [1, 2, 3], @callback.call(@object)
237
241
  end
238
242
  end
243
+
244
+ class CallbackWithApplicationBoundObjectTest < Test::Unit::TestCase
245
+ def setup
246
+ @original_bind_to_object = StateMachine::Callback.bind_to_object
247
+ StateMachine::Callback.bind_to_object = true
248
+
249
+ @object = Object.new
250
+ @callback = StateMachine::Callback.new(:do => lambda {|*args| self})
251
+ end
252
+
253
+ def test_should_call_method_within_the_context_of_the_object
254
+ assert_equal @object, @callback.call(@object)
255
+ end
256
+
257
+ def teardown
258
+ StateMachine::Callback.bind_to_object = @original_bind_to_object
259
+ end
260
+ end
261
+
262
+ class CallbackWithApplicationTerminatorTest < Test::Unit::TestCase
263
+ def setup
264
+ @original_terminator = StateMachine::Callback.bind_to_object
265
+ StateMachine::Callback.terminator = lambda {|result| result == false}
266
+
267
+ @object = Object.new
268
+ end
269
+
270
+ def test_should_not_halt_if_terminator_does_not_match
271
+ callback = StateMachine::Callback.new(:do => lambda {true})
272
+ assert_nothing_thrown { callback.call(@object) }
273
+ end
274
+
275
+ def test_should_halt_if_terminator_matches
276
+ callback = StateMachine::Callback.new(:do => lambda {false})
277
+ assert_throws(:halt) { callback.call(@object) }
278
+ end
279
+
280
+ def teardown
281
+ StateMachine::Callback.bind_to_object = @original_bind_to_object
282
+ end
283
+ end
@@ -31,10 +31,10 @@ class ConditionProxyTest < Test::Unit::TestCase
31
31
  def test_should_pass_object_into_proxy_condition
32
32
  condition_args = []
33
33
  condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {|*args| condition_args = args})
34
- validation = condition_proxy.validate(:name)
34
+ options = condition_proxy.validate[0]
35
35
 
36
36
  object = Validateable.new
37
- validation.last[:if].call(object)
37
+ options[:if].call(object)
38
38
 
39
39
  assert_equal [object], condition_args
40
40
  end
@@ -49,10 +49,10 @@ class ConditionProxyTest < Test::Unit::TestCase
49
49
  end
50
50
 
51
51
  condition_proxy = StateMachine::ConditionProxy.new(klass, :callback)
52
- validation = condition_proxy.validate(:name)
52
+ options = condition_proxy.validate[0]
53
53
 
54
54
  object = klass.new
55
- validation.last[:if].call(object)
55
+ options[:if].call(object)
56
56
 
57
57
  assert object.callback_called
58
58
  end
@@ -63,10 +63,10 @@ class ConditionProxyTest < Test::Unit::TestCase
63
63
  end
64
64
 
65
65
  condition_proxy = StateMachine::ConditionProxy.new(klass, '@callback_called = true')
66
- validation = condition_proxy.validate(:name)
66
+ options = condition_proxy.validate[0]
67
67
 
68
68
  object = klass.new
69
- validation.last[:if].call(object)
69
+ options[:if].call(object)
70
70
 
71
71
  assert object.callback_called
72
72
  end
@@ -78,8 +78,7 @@ class ConditionProxyWithoutConditionsTest < Test::Unit::TestCase
78
78
  condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {@proxy_result})
79
79
 
80
80
  @object = Validateable.new
81
- @validation = condition_proxy.validate(:name)
82
- @options = @validation.last
81
+ @options = condition_proxy.validate[0]
83
82
  end
84
83
 
85
84
  def test_should_have_options_configuration
@@ -114,8 +113,7 @@ class ConditionProxyWithIfConditionTest < Test::Unit::TestCase
114
113
  @object = Validateable.new
115
114
 
116
115
  @condition_result = nil
117
- @validation = condition_proxy.validate(:name, :if => lambda {@condition_result})
118
- @options = @validation.pop
116
+ @options = condition_proxy.validate(:if => lambda {@condition_result})[0]
119
117
  end
120
118
 
121
119
  def test_should_have_if_option
@@ -144,8 +142,7 @@ class ConditionProxyWithIfConditionTest < Test::Unit::TestCase
144
142
  end
145
143
 
146
144
  condition_proxy = StateMachine::ConditionProxy.new(klass, lambda {true})
147
- validation = condition_proxy.validate(:name, :if => :callback)
148
- options = validation.last
145
+ options = condition_proxy.validate(:if => :callback)[0]
149
146
 
150
147
  object = klass.new
151
148
  object.callback = false
@@ -161,8 +158,7 @@ class ConditionProxyWithIfConditionTest < Test::Unit::TestCase
161
158
  end
162
159
 
163
160
  condition_proxy = StateMachine::ConditionProxy.new(klass, lambda {true})
164
- validation = condition_proxy.validate(:name, :if => '@callback')
165
- options = validation.last
161
+ options = condition_proxy.validate(:if => '@callback')[0]
166
162
 
167
163
  object = klass.new
168
164
  object.callback = false
@@ -175,15 +171,13 @@ end
175
171
 
176
172
  class ConditionProxyWithMultipleIfConditionsTest < Test::Unit::TestCase
177
173
  def setup
178
- @proxy_result = true
179
- condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {@proxy_result})
174
+ condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {true})
180
175
 
181
176
  @object = Validateable.new
182
177
 
183
178
  @first_condition_result = nil
184
179
  @second_condition_result = nil
185
- @validation = condition_proxy.validate(:name, :if => [lambda {@first_condition_result}, lambda {@second_condition_result}])
186
- @options = @validation.pop
180
+ @options = condition_proxy.validate(:if => [lambda {@first_condition_result}, lambda {@second_condition_result}])[0]
187
181
  end
188
182
 
189
183
  def test_should_be_true_if_all_conditions_are_true
@@ -211,8 +205,7 @@ class ConditionProxyWithUnlessConditionTest < Test::Unit::TestCase
211
205
  @object = Validateable.new
212
206
 
213
207
  @condition_result = nil
214
- @validation = condition_proxy.validate(:name, :unless => lambda {@condition_result})
215
- @options = @validation.pop
208
+ @options = condition_proxy.validate(:unless => lambda {@condition_result})[0]
216
209
  end
217
210
 
218
211
  def test_should_have_if_option
@@ -241,8 +234,7 @@ class ConditionProxyWithUnlessConditionTest < Test::Unit::TestCase
241
234
  end
242
235
 
243
236
  condition_proxy = StateMachine::ConditionProxy.new(klass, lambda {true})
244
- validation = condition_proxy.validate(:name, :unless => :callback)
245
- options = validation.last
237
+ options = condition_proxy.validate(:unless => :callback)[0]
246
238
 
247
239
  object = klass.new
248
240
  object.callback = true
@@ -258,8 +250,7 @@ class ConditionProxyWithUnlessConditionTest < Test::Unit::TestCase
258
250
  end
259
251
 
260
252
  condition_proxy = StateMachine::ConditionProxy.new(klass, lambda {true})
261
- validation = condition_proxy.validate(:name, :unless => '@callback')
262
- options = validation.last
253
+ options = condition_proxy.validate(:unless => '@callback')[0]
263
254
 
264
255
  object = klass.new
265
256
  object.callback = true
@@ -272,15 +263,13 @@ end
272
263
 
273
264
  class ConditionProxyWithMultipleUnlessConditionsTest < Test::Unit::TestCase
274
265
  def setup
275
- @proxy_result = true
276
- condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {@proxy_result})
266
+ condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {true})
277
267
 
278
268
  @object = Validateable.new
279
269
 
280
270
  @first_condition_result = nil
281
271
  @second_condition_result = nil
282
- @validation = condition_proxy.validate(:name, :unless => [lambda {@first_condition_result}, lambda {@second_condition_result}])
283
- @options = @validation.pop
272
+ @options = condition_proxy.validate(:unless => [lambda {@first_condition_result}, lambda {@second_condition_result}])[0]
284
273
  end
285
274
 
286
275
  def test_should_be_true_if_all_conditions_are_false
@@ -299,3 +288,41 @@ class ConditionProxyWithMultipleUnlessConditionsTest < Test::Unit::TestCase
299
288
  assert !@options[:if].call(@object)
300
289
  end
301
290
  end
291
+
292
+ class ConditionProxyWithIfAndUnlessConditionsTest < Test::Unit::TestCase
293
+ def setup
294
+ condition_proxy = StateMachine::ConditionProxy.new(Validateable, lambda {true})
295
+
296
+ @object = Validateable.new
297
+
298
+ @if_condition_result = nil
299
+ @unless_condition_result = nil
300
+ @options = condition_proxy.validate(:if => lambda {@if_condition_result}, :unless => lambda {@unless_condition_result})[0]
301
+ end
302
+
303
+ def test_should_be_false_if_if_condition_is_false
304
+ @if_condition_result = false
305
+ @unless_condition_result = false
306
+ assert !@options[:if].call(@object)
307
+
308
+ @if_condition_result = false
309
+ @unless_condition_result = true
310
+ assert !@options[:if].call(@object)
311
+ end
312
+
313
+ def test_should_be_false_if_unless_condition_is_true
314
+ @if_condition_result = false
315
+ @unless_condition_result = true
316
+ assert !@options[:if].call(@object)
317
+
318
+ @if_condition_result = true
319
+ @unless_condition_result = true
320
+ assert !@options[:if].call(@object)
321
+ end
322
+
323
+ def test_should_be_true_if_if_condition_is_true_and_unless_condition_is_false
324
+ @if_condition_result = true
325
+ @unless_condition_result = false
326
+ assert @options[:if].call(@object)
327
+ end
328
+ end
@@ -0,0 +1,228 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ class EventCollectionByDefaultTest < Test::Unit::TestCase
4
+ def setup
5
+ @machine = StateMachine::Machine.new(Class.new)
6
+ @events = StateMachine::EventCollection.new(@machine)
7
+ end
8
+
9
+ def test_should_not_have_any_nodes
10
+ assert_equal 0, @events.length
11
+ end
12
+
13
+ def test_should_have_a_machine
14
+ assert_equal @machine, @events.machine
15
+ end
16
+
17
+ def test_should_not_have_any_valid_events_for_an_object
18
+ assert @events.valid_for(@object).empty?
19
+ end
20
+
21
+ def test_should_not_have_any_transitions_for_an_object
22
+ assert @events.transitions_for(@object).empty?
23
+ end
24
+ end
25
+
26
+ class EventCollectionTest < Test::Unit::TestCase
27
+ def setup
28
+ machine = StateMachine::Machine.new(Class.new, :namespace => 'alarm')
29
+ @events = StateMachine::EventCollection.new(machine)
30
+
31
+ @events << @open = StateMachine::Event.new(machine, :enable)
32
+ end
33
+
34
+ def test_should_index_by_name
35
+ assert_equal @open, @events[:enable, :name]
36
+ end
37
+
38
+ def test_should_index_by_name_by_default
39
+ assert_equal @open, @events[:enable]
40
+ end
41
+
42
+ def test_should_index_by_qualified_name
43
+ assert_equal @open, @events[:enable_alarm, :qualified_name]
44
+ end
45
+ end
46
+
47
+ class EventCollectionWithEventsWithTransitionsTest < Test::Unit::TestCase
48
+ def setup
49
+ @klass = Class.new
50
+ @machine = StateMachine::Machine.new(@klass, :initial => :parked)
51
+ @events = StateMachine::EventCollection.new(@machine)
52
+
53
+ @machine.state :idling, :stalled
54
+ @machine.event :ignite
55
+
56
+ @events << @ignite = StateMachine::Event.new(@machine, :ignite)
57
+ @ignite.transition :parked => :idling
58
+ @ignite.transition :stalled => :idling
59
+ end
60
+
61
+ def test_should_only_include_valid_events_for_an_object
62
+ object = @klass.new
63
+ object.state = 'parked'
64
+ assert_equal [@ignite], @events.valid_for(object)
65
+
66
+ object.state = 'stalled'
67
+ assert_equal [@ignite], @events.valid_for(object)
68
+
69
+ object.state = 'idling'
70
+ assert_equal [], @events.valid_for(object)
71
+ end
72
+
73
+ def test_should_only_include_valid_transitions_for_an_object
74
+ object = @klass.new
75
+ object.state = 'parked'
76
+ assert_equal [{:object => object, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}], @events.transitions_for(object).map {|transition| transition.attributes}
77
+
78
+ object.state = 'stalled'
79
+ assert_equal [{:object => object, :attribute => :state, :event => :ignite, :from => 'stalled', :to => 'idling'}], @events.transitions_for(object).map {|transition| transition.attributes}
80
+
81
+ object.state = 'idling'
82
+ assert_equal [], @events.transitions_for(object)
83
+ end
84
+ end
85
+
86
+ class EventCollectionWithMultipleEventsTest < Test::Unit::TestCase
87
+ def setup
88
+ @klass = Class.new
89
+ @machine = StateMachine::Machine.new(@klass, :initial => :parked)
90
+ @events = StateMachine::EventCollection.new(@machine)
91
+
92
+ @machine.state :first_gear
93
+ @machine.event :park, :shift_down
94
+
95
+ @events << @park = StateMachine::Event.new(@machine, :park)
96
+ @park.transition :first_gear => :parked
97
+
98
+ @events << @shift_down = StateMachine::Event.new(@machine, :shift_down)
99
+ @shift_down.transition :first_gear => :parked
100
+ end
101
+
102
+ def test_should_only_include_all_valid_events_for_an_object
103
+ object = @klass.new
104
+ object.state = 'first_gear'
105
+ assert_equal [@park, @shift_down], @events.valid_for(object)
106
+ end
107
+ end
108
+
109
+ class EventCollectionWithoutMachineActionTest < Test::Unit::TestCase
110
+ def setup
111
+ @klass = Class.new
112
+ @machine = StateMachine::Machine.new(@klass, :initial => :parked)
113
+ @events = StateMachine::EventCollection.new(@machine)
114
+
115
+ @machine.event :ignite
116
+ @events << StateMachine::Event.new(@machine, :ignite)
117
+
118
+ @object = @klass.new
119
+ end
120
+
121
+ def test_should_not_have_an_attribute_transition
122
+ assert_nil @events.attribute_transition_for(@object)
123
+ end
124
+ end
125
+
126
+ class EventCollectionAttributeWithMachineActionTest < Test::Unit::TestCase
127
+ def setup
128
+ @klass = Class.new do
129
+ def save
130
+ end
131
+ end
132
+
133
+ @machine = StateMachine::Machine.new(@klass, :initial => :parked, :action => :save)
134
+ @events = StateMachine::EventCollection.new(@machine)
135
+
136
+ @machine.event :ignite
137
+ @machine.state :parked, :idling
138
+ @events << @ignite = StateMachine::Event.new(@machine, :ignite)
139
+
140
+ @object = @klass.new
141
+ end
142
+
143
+ def test_should_not_have_transition_if_nil
144
+ @object.state_event = nil
145
+ assert_nil @events.attribute_transition_for(@object)
146
+ end
147
+
148
+ def test_should_not_have_transition_if_empty
149
+ @object.state_event = ''
150
+ assert_nil @events.attribute_transition_for(@object)
151
+ end
152
+
153
+ def test_should_have_invalid_transition_if_invalid_event_specified
154
+ @object.state_event = 'invalid'
155
+ assert_equal false, @events.attribute_transition_for(@object)
156
+ end
157
+
158
+ def test_should_have_invalid_transition_if_event_cannot_be_fired
159
+ @object.state_event = 'ignite'
160
+ assert_equal false, @events.attribute_transition_for(@object)
161
+ end
162
+
163
+ def test_should_have_valid_transition_if_event_can_be_fired
164
+ @ignite.transition :parked => :idling
165
+ @object.state_event = 'ignite'
166
+
167
+ assert_instance_of StateMachine::Transition, @events.attribute_transition_for(@object)
168
+ end
169
+ end
170
+
171
+ class EventCollectionWithValidationsTest < Test::Unit::TestCase
172
+ def setup
173
+ StateMachine::Integrations.const_set('Custom', Module.new do
174
+ def invalidate(object, attribute, message, values = [])
175
+ (object.errors ||= []) << generate_message(message, values)
176
+ end
177
+
178
+ def reset(object)
179
+ object.errors = []
180
+ end
181
+ end)
182
+
183
+ @klass = Class.new do
184
+ attr_accessor :errors
185
+
186
+ def initialize
187
+ @errors = []
188
+ super
189
+ end
190
+ end
191
+
192
+ @machine = StateMachine::Machine.new(@klass, :initial => :parked, :action => :save, :integration => :custom)
193
+ @events = StateMachine::EventCollection.new(@machine)
194
+
195
+ @machine.event :ignite
196
+ @machine.state :parked, :idling
197
+ @events << @ignite = StateMachine::Event.new(@machine, :ignite)
198
+
199
+ @object = @klass.new
200
+ end
201
+
202
+ def test_should_invalidate_if_invalid_event_specified
203
+ @object.state_event = 'invalid'
204
+ @events.attribute_transition_for(@object, true)
205
+
206
+ assert_equal ['is invalid'], @object.errors
207
+ end
208
+
209
+ def test_should_invalidate_if_event_cannot_be_fired
210
+ @object.state = 'idling'
211
+ @object.state_event = 'ignite'
212
+ @events.attribute_transition_for(@object, true)
213
+
214
+ assert_equal ['cannot transition when idling'], @object.errors
215
+ end
216
+
217
+ def test_should_not_invalidate_event_can_be_fired
218
+ @ignite.transition :parked => :idling
219
+ @object.state_event = 'ignite'
220
+ @events.attribute_transition_for(@object, true)
221
+
222
+ assert_equal [], @object.errors
223
+ end
224
+
225
+ def teardown
226
+ StateMachine::Integrations.send(:remove_const, 'Custom')
227
+ end
228
+ end