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.
- data/CHANGELOG.rdoc +273 -0
- data/LICENSE +20 -0
- data/README.rdoc +466 -0
- data/Rakefile +98 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +429 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +189 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +251 -0
- data/lib/state_machine/event_collection.rb +113 -0
- data/lib/state_machine/extensions.rb +158 -0
- data/lib/state_machine/guard.rb +219 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +444 -0
- data/lib/state_machine/integrations/active_record/locale.rb +10 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +325 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +292 -0
- data/lib/state_machine/machine.rb +1431 -0
- data/lib/state_machine/machine_collection.rb +146 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +249 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +367 -0
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +30 -0
- data/test/classes/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +941 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +455 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +129 -0
- data/test/unit/event_collection_test.rb +293 -0
- data/test/unit/event_test.rb +605 -0
- data/test/unit/guard_test.rb +862 -0
- data/test/unit/integrations/active_record_test.rb +1001 -0
- data/test/unit/integrations/data_mapper_test.rb +694 -0
- data/test/unit/integrations/sequel_test.rb +486 -0
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +710 -0
- data/test/unit/machine_test.rb +1910 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +795 -0
- data/test/unit/transition_test.rb +1113 -0
- metadata +161 -0
@@ -0,0 +1,1001 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
|
2
|
+
|
3
|
+
begin
|
4
|
+
# Load library
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
gem 'activerecord', ENV['AR_VERSION'] ? "=#{ENV['AR_VERSION']}" : '>=2.1.0'
|
8
|
+
require 'active_record'
|
9
|
+
|
10
|
+
FIXTURES_ROOT = File.dirname(__FILE__) + '/../../fixtures/'
|
11
|
+
|
12
|
+
# Load TestCase helpers
|
13
|
+
require 'active_support/test_case'
|
14
|
+
require 'active_record/fixtures'
|
15
|
+
require 'active_record/test_case'
|
16
|
+
|
17
|
+
# Establish database connection
|
18
|
+
ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3', 'database' => ':memory:'})
|
19
|
+
ActiveRecord::Base.logger = Logger.new("#{File.dirname(__FILE__)}/../../active_record.log")
|
20
|
+
|
21
|
+
# Add model/observer creation helpers
|
22
|
+
ActiveRecord::TestCase.class_eval do
|
23
|
+
# Creates a new ActiveRecord model (and the associated table)
|
24
|
+
def new_model(create_table = true, &block)
|
25
|
+
model = Class.new(ActiveRecord::Base) do
|
26
|
+
connection.create_table(:foo, :force => true) {|t| t.string(:state)} if create_table
|
27
|
+
set_table_name('foo')
|
28
|
+
|
29
|
+
def self.name; 'ActiveRecordTest::Foo'; end
|
30
|
+
end
|
31
|
+
model.class_eval(&block) if block_given?
|
32
|
+
model
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a new ActiveRecord observer
|
36
|
+
def new_observer(model, &block)
|
37
|
+
observer = Class.new(ActiveRecord::Observer) do
|
38
|
+
attr_accessor :notifications
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
super
|
42
|
+
@notifications = []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
observer.observe(model)
|
46
|
+
observer.class_eval(&block) if block_given?
|
47
|
+
observer
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module ActiveRecordTest
|
52
|
+
class IntegrationTest < ActiveRecord::TestCase
|
53
|
+
def test_should_match_if_class_inherits_from_active_record
|
54
|
+
assert StateMachine::Integrations::ActiveRecord.matches?(new_model)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_should_not_match_if_class_does_not_inherit_from_active_record
|
58
|
+
assert !StateMachine::Integrations::ActiveRecord.matches?(Class.new)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class MachineByDefaultTest < ActiveRecord::TestCase
|
63
|
+
def setup
|
64
|
+
@model = new_model
|
65
|
+
@machine = StateMachine::Machine.new(@model)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_should_use_save_as_action
|
69
|
+
assert_equal :save, @machine.action
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_should_use_transactions
|
73
|
+
assert_equal true, @machine.use_transactions
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_should_create_notifier_before_callback
|
77
|
+
assert_equal 1, @machine.callbacks[:before].size
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_should_create_notifier_after_callback
|
81
|
+
assert_equal 1, @machine.callbacks[:after].size
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class MachineTest < ActiveRecord::TestCase
|
86
|
+
def setup
|
87
|
+
@model = new_model
|
88
|
+
@machine = StateMachine::Machine.new(@model)
|
89
|
+
@machine.state :parked, :first_gear
|
90
|
+
@machine.state :idling, :value => lambda {'idling'}
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_should_create_singular_with_scope
|
94
|
+
assert @model.respond_to?(:with_state)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_should_only_include_records_with_state_in_singular_with_scope
|
98
|
+
parked = @model.create :state => 'parked'
|
99
|
+
idling = @model.create :state => 'idling'
|
100
|
+
|
101
|
+
assert_equal [parked], @model.with_state(:parked)
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_should_create_plural_with_scope
|
105
|
+
assert @model.respond_to?(:with_states)
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_should_only_include_records_with_states_in_plural_with_scope
|
109
|
+
parked = @model.create :state => 'parked'
|
110
|
+
idling = @model.create :state => 'idling'
|
111
|
+
|
112
|
+
assert_equal [parked, idling], @model.with_states(:parked, :idling)
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_should_create_singular_without_scope
|
116
|
+
assert @model.respond_to?(:without_state)
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_should_only_include_records_without_state_in_singular_without_scope
|
120
|
+
parked = @model.create :state => 'parked'
|
121
|
+
idling = @model.create :state => 'idling'
|
122
|
+
|
123
|
+
assert_equal [parked], @model.without_state(:idling)
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_should_create_plural_without_scope
|
127
|
+
assert @model.respond_to?(:without_states)
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_should_only_include_records_without_states_in_plural_without_scope
|
131
|
+
parked = @model.create :state => 'parked'
|
132
|
+
idling = @model.create :state => 'idling'
|
133
|
+
first_gear = @model.create :state => 'first_gear'
|
134
|
+
|
135
|
+
assert_equal [parked, idling], @model.without_states(:first_gear)
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_should_allow_chaining_scopes
|
139
|
+
parked = @model.create :state => 'parked'
|
140
|
+
idling = @model.create :state => 'idling'
|
141
|
+
|
142
|
+
assert_equal [idling], @model.without_state(:parked).with_state(:idling)
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_should_rollback_transaction_if_false
|
146
|
+
@machine.within_transaction(@model.new) do
|
147
|
+
@model.create
|
148
|
+
false
|
149
|
+
end
|
150
|
+
|
151
|
+
assert_equal 0, @model.count
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_should_not_rollback_transaction_if_true
|
155
|
+
@machine.within_transaction(@model.new) do
|
156
|
+
@model.create
|
157
|
+
true
|
158
|
+
end
|
159
|
+
|
160
|
+
assert_equal 1, @model.count
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_should_invalidate_using_errors
|
164
|
+
I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
|
165
|
+
|
166
|
+
record = @model.new
|
167
|
+
record.state = 'parked'
|
168
|
+
|
169
|
+
@machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
|
170
|
+
|
171
|
+
assert record.errors.invalid?(:state)
|
172
|
+
assert_equal 'cannot transition via "park"', record.errors.on(:state)
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_should_auto_prefix_custom_attributes_on_invalidation
|
176
|
+
record = @model.new
|
177
|
+
@machine.invalidate(record, :event, :invalid)
|
178
|
+
|
179
|
+
assert_equal 'is invalid', record.errors.on(:state_event)
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_should_clear_errors_on_reset
|
183
|
+
record = @model.new
|
184
|
+
record.state = 'parked'
|
185
|
+
record.errors.add(:state, 'is invalid')
|
186
|
+
|
187
|
+
@machine.reset(record)
|
188
|
+
assert_nil record.errors.on(:id)
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_should_not_override_the_column_reader
|
192
|
+
record = @model.new
|
193
|
+
record[:state] = 'parked'
|
194
|
+
assert_equal 'parked', record.state
|
195
|
+
end
|
196
|
+
|
197
|
+
def test_should_not_override_the_column_writer
|
198
|
+
record = @model.new
|
199
|
+
record.state = 'parked'
|
200
|
+
assert_equal 'parked', record[:state]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class MachineWithoutDatabaseTest < ActiveRecord::TestCase
|
205
|
+
def setup
|
206
|
+
@model = new_model(false) do
|
207
|
+
# Simulate the database not being available entirely
|
208
|
+
def self.connection
|
209
|
+
raise ActiveRecord::ConnectionNotEstablished
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def test_should_allow_machine_creation
|
215
|
+
assert_nothing_raised { StateMachine::Machine.new(@model) }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
class MachineUnmigratedTest < ActiveRecord::TestCase
|
220
|
+
def setup
|
221
|
+
@model = new_model(false)
|
222
|
+
|
223
|
+
# Drop the table so that it definitely doesn't exist
|
224
|
+
@model.connection.drop_table(:foo) if @model.connection.table_exists?(:foo)
|
225
|
+
end
|
226
|
+
|
227
|
+
def test_should_allow_machine_creation
|
228
|
+
assert_nothing_raised { StateMachine::Machine.new(@model) }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class MachineWithInitialStateTest < ActiveRecord::TestCase
|
233
|
+
def setup
|
234
|
+
@model = new_model
|
235
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
236
|
+
@record = @model.new
|
237
|
+
end
|
238
|
+
|
239
|
+
def test_should_set_initial_state_on_created_object
|
240
|
+
assert_equal 'parked', @record.state
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class MachineWithConflictingPredicateTest < ActiveRecord::TestCase
|
245
|
+
def setup
|
246
|
+
@model = new_model do
|
247
|
+
def state?(*args)
|
248
|
+
true
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
@machine = StateMachine::Machine.new(@model)
|
253
|
+
@record = @model.new
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_should_not_define_attribute_predicate
|
257
|
+
assert @record.state?
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class MachineWithColumnStateAttributeTest < ActiveRecord::TestCase
|
262
|
+
def setup
|
263
|
+
@model = new_model
|
264
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
265
|
+
@machine.other_states(:idling)
|
266
|
+
|
267
|
+
@record = @model.new
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_should_have_an_attribute_predicate
|
271
|
+
assert @record.respond_to?(:state?)
|
272
|
+
end
|
273
|
+
|
274
|
+
def test_should_test_for_existence_on_predicate_without_parameters
|
275
|
+
assert @record.state?
|
276
|
+
|
277
|
+
@record.state = nil
|
278
|
+
assert !@record.state?
|
279
|
+
end
|
280
|
+
|
281
|
+
def test_should_return_false_for_predicate_if_does_not_match_current_value
|
282
|
+
assert !@record.state?(:idling)
|
283
|
+
end
|
284
|
+
|
285
|
+
def test_should_return_true_for_predicate_if_matches_current_value
|
286
|
+
assert @record.state?(:parked)
|
287
|
+
end
|
288
|
+
|
289
|
+
def test_should_raise_exception_for_predicate_if_invalid_state_specified
|
290
|
+
assert_raise(IndexError) { @record.state?(:invalid) }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class MachineWithNonColumnStateAttributeUndefinedTest < ActiveRecord::TestCase
|
295
|
+
def setup
|
296
|
+
@model = new_model do
|
297
|
+
def initialize
|
298
|
+
# Skip attribute initialization
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
@machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
|
303
|
+
@machine.other_states(:idling)
|
304
|
+
@record = @model.new
|
305
|
+
end
|
306
|
+
|
307
|
+
def test_should_not_define_a_reader_attribute_for_the_attribute
|
308
|
+
assert !@record.respond_to?(:status)
|
309
|
+
end
|
310
|
+
|
311
|
+
def test_should_not_define_a_writer_attribute_for_the_attribute
|
312
|
+
assert !@record.respond_to?(:status=)
|
313
|
+
end
|
314
|
+
|
315
|
+
def test_should_define_an_attribute_predicate
|
316
|
+
assert @record.respond_to?(:status?)
|
317
|
+
end
|
318
|
+
|
319
|
+
def test_should_raise_exception_on_predicate_without_parameters
|
320
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
321
|
+
assert_raise(NoMethodError) { @record.status? }
|
322
|
+
ensure
|
323
|
+
$VERBOSE = old_verbose
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
class MachineWithNonColumnStateAttributeDefinedTest < ActiveRecord::TestCase
|
328
|
+
def setup
|
329
|
+
@model = new_model do
|
330
|
+
attr_accessor :status
|
331
|
+
end
|
332
|
+
|
333
|
+
@machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
|
334
|
+
@machine.other_states(:idling)
|
335
|
+
@record = @model.new
|
336
|
+
end
|
337
|
+
|
338
|
+
def test_should_return_false_for_predicate_if_does_not_match_current_value
|
339
|
+
assert !@record.status?(:idling)
|
340
|
+
end
|
341
|
+
|
342
|
+
def test_should_return_true_for_predicate_if_matches_current_value
|
343
|
+
assert @record.status?(:parked)
|
344
|
+
end
|
345
|
+
|
346
|
+
def test_should_raise_exception_for_predicate_if_invalid_state_specified
|
347
|
+
assert_raise(IndexError) { @record.status?(:invalid) }
|
348
|
+
end
|
349
|
+
|
350
|
+
def test_should_set_initial_state_on_created_object
|
351
|
+
assert_equal 'parked', @record.status
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
class MachineWithComplexPluralizationTest < ActiveRecord::TestCase
|
356
|
+
def setup
|
357
|
+
@model = new_model
|
358
|
+
@machine = StateMachine::Machine.new(@model, :status)
|
359
|
+
end
|
360
|
+
|
361
|
+
def test_should_create_singular_with_scope
|
362
|
+
assert @model.respond_to?(:with_status)
|
363
|
+
end
|
364
|
+
|
365
|
+
def test_should_create_plural_with_scope
|
366
|
+
assert @model.respond_to?(:with_statuses)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class MachineWithCallbacksTest < ActiveRecord::TestCase
|
371
|
+
def setup
|
372
|
+
@model = new_model
|
373
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
374
|
+
@machine.other_states :idling
|
375
|
+
@machine.event :ignite
|
376
|
+
|
377
|
+
@record = @model.new(:state => 'parked')
|
378
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
379
|
+
end
|
380
|
+
|
381
|
+
def test_should_run_before_callbacks
|
382
|
+
called = false
|
383
|
+
@machine.before_transition(lambda {called = true})
|
384
|
+
|
385
|
+
@transition.perform
|
386
|
+
assert called
|
387
|
+
end
|
388
|
+
|
389
|
+
def test_should_pass_record_to_before_callbacks_with_one_argument
|
390
|
+
record = nil
|
391
|
+
@machine.before_transition(lambda {|arg| record = arg})
|
392
|
+
|
393
|
+
@transition.perform
|
394
|
+
assert_equal @record, record
|
395
|
+
end
|
396
|
+
|
397
|
+
def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
|
398
|
+
callback_args = nil
|
399
|
+
@machine.before_transition(lambda {|*args| callback_args = args})
|
400
|
+
|
401
|
+
@transition.perform
|
402
|
+
assert_equal [@record, @transition], callback_args
|
403
|
+
end
|
404
|
+
|
405
|
+
def test_should_run_before_callbacks_outside_the_context_of_the_record
|
406
|
+
context = nil
|
407
|
+
@machine.before_transition(lambda {context = self})
|
408
|
+
|
409
|
+
@transition.perform
|
410
|
+
assert_equal self, context
|
411
|
+
end
|
412
|
+
|
413
|
+
def test_should_run_after_callbacks
|
414
|
+
called = false
|
415
|
+
@machine.after_transition(lambda {called = true})
|
416
|
+
|
417
|
+
@transition.perform
|
418
|
+
assert called
|
419
|
+
end
|
420
|
+
|
421
|
+
def test_should_pass_record_to_after_callbacks_with_one_argument
|
422
|
+
record = nil
|
423
|
+
@machine.after_transition(lambda {|arg| record = arg})
|
424
|
+
|
425
|
+
@transition.perform
|
426
|
+
assert_equal @record, record
|
427
|
+
end
|
428
|
+
|
429
|
+
def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
|
430
|
+
callback_args = nil
|
431
|
+
@machine.after_transition(lambda {|*args| callback_args = args})
|
432
|
+
|
433
|
+
@transition.perform
|
434
|
+
assert_equal [@record, @transition], callback_args
|
435
|
+
end
|
436
|
+
|
437
|
+
def test_should_run_after_callbacks_outside_the_context_of_the_record
|
438
|
+
context = nil
|
439
|
+
@machine.after_transition(lambda {context = self})
|
440
|
+
|
441
|
+
@transition.perform
|
442
|
+
assert_equal self, context
|
443
|
+
end
|
444
|
+
|
445
|
+
def test_should_include_transition_states_in_known_states
|
446
|
+
@machine.before_transition :to => :first_gear, :do => lambda {}
|
447
|
+
|
448
|
+
assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
|
449
|
+
end
|
450
|
+
|
451
|
+
def test_should_allow_symbolic_callbacks
|
452
|
+
callback_args = nil
|
453
|
+
|
454
|
+
klass = class << @record; self; end
|
455
|
+
klass.send(:define_method, :after_ignite) do |*args|
|
456
|
+
callback_args = args
|
457
|
+
end
|
458
|
+
|
459
|
+
@machine.before_transition(:after_ignite)
|
460
|
+
|
461
|
+
@transition.perform
|
462
|
+
assert_equal [@transition], callback_args
|
463
|
+
end
|
464
|
+
|
465
|
+
def test_should_allow_string_callbacks
|
466
|
+
class << @record
|
467
|
+
attr_reader :callback_result
|
468
|
+
end
|
469
|
+
|
470
|
+
@machine.before_transition('@callback_result = [1, 2, 3]')
|
471
|
+
@transition.perform
|
472
|
+
|
473
|
+
assert_equal [1, 2, 3], @record.callback_result
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
class MachineWithFailedBeforeCallbacksTest < ActiveRecord::TestCase
|
478
|
+
def setup
|
479
|
+
@before_count = 0
|
480
|
+
|
481
|
+
@model = new_model
|
482
|
+
@machine = StateMachine::Machine.new(@model)
|
483
|
+
@machine.state :parked, :idling
|
484
|
+
@machine.event :ignite
|
485
|
+
@machine.before_transition(lambda {@before_count += 1; false})
|
486
|
+
@machine.before_transition(lambda {@before_count += 1})
|
487
|
+
|
488
|
+
@record = @model.new(:state => 'parked')
|
489
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
490
|
+
@result = @transition.perform
|
491
|
+
end
|
492
|
+
|
493
|
+
def test_should_not_be_successful
|
494
|
+
assert !@result
|
495
|
+
end
|
496
|
+
|
497
|
+
def test_should_not_change_current_state
|
498
|
+
assert_equal 'parked', @record.state
|
499
|
+
end
|
500
|
+
|
501
|
+
def test_should_not_run_action
|
502
|
+
assert @record.new_record?
|
503
|
+
end
|
504
|
+
|
505
|
+
def test_should_not_run_further_before_callbacks
|
506
|
+
assert_equal 1, @before_count
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
class MachineWithFailedActionTest < ActiveRecord::TestCase
|
511
|
+
def setup
|
512
|
+
@model = new_model do
|
513
|
+
validates_inclusion_of :state, :in => %w(first_gear)
|
514
|
+
end
|
515
|
+
|
516
|
+
@machine = StateMachine::Machine.new(@model)
|
517
|
+
@machine.state :parked, :idling
|
518
|
+
@machine.event :ignite
|
519
|
+
@record = @model.new(:state => 'parked')
|
520
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
521
|
+
@result = @transition.perform
|
522
|
+
end
|
523
|
+
|
524
|
+
def test_should_not_be_successful
|
525
|
+
assert !@result
|
526
|
+
end
|
527
|
+
|
528
|
+
def test_should_not_change_current_state
|
529
|
+
assert_equal 'parked', @record.state
|
530
|
+
end
|
531
|
+
|
532
|
+
def test_should_not_save_record
|
533
|
+
assert @record.new_record?
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
class MachineWithValidationsTest < ActiveRecord::TestCase
|
538
|
+
def setup
|
539
|
+
@model = new_model
|
540
|
+
@machine = StateMachine::Machine.new(@model)
|
541
|
+
@machine.state :parked
|
542
|
+
|
543
|
+
@record = @model.new
|
544
|
+
end
|
545
|
+
|
546
|
+
def test_should_be_valid_if_state_is_known
|
547
|
+
@record.state = 'parked'
|
548
|
+
|
549
|
+
assert @record.valid?
|
550
|
+
end
|
551
|
+
|
552
|
+
def test_should_not_be_valid_if_state_is_unknown
|
553
|
+
@record.state = 'invalid'
|
554
|
+
|
555
|
+
assert !@record.valid?
|
556
|
+
assert_equal ['State is invalid'], @record.errors.full_messages
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
class MachineWithStateDrivenValidationsTest < ActiveRecord::TestCase
|
561
|
+
def setup
|
562
|
+
@model = new_model do
|
563
|
+
attr_accessor :seatbelt
|
564
|
+
end
|
565
|
+
|
566
|
+
@machine = StateMachine::Machine.new(@model)
|
567
|
+
@machine.state :first_gear, :second_gear do
|
568
|
+
validates_presence_of :seatbelt
|
569
|
+
end
|
570
|
+
@machine.other_states :parked
|
571
|
+
end
|
572
|
+
|
573
|
+
def test_should_be_valid_if_validation_fails_outside_state_scope
|
574
|
+
record = @model.new(:state => 'parked', :seatbelt => nil)
|
575
|
+
assert record.valid?
|
576
|
+
end
|
577
|
+
|
578
|
+
def test_should_be_invalid_if_validation_fails_within_state_scope
|
579
|
+
record = @model.new(:state => 'first_gear', :seatbelt => nil)
|
580
|
+
assert !record.valid?
|
581
|
+
end
|
582
|
+
|
583
|
+
def test_should_be_valid_if_validation_succeeds_within_state_scope
|
584
|
+
record = @model.new(:state => 'second_gear', :seatbelt => true)
|
585
|
+
assert record.valid?
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
class MachineWithFailedAfterCallbacksTest < ActiveRecord::TestCase
|
590
|
+
def setup
|
591
|
+
@after_count = 0
|
592
|
+
|
593
|
+
@model = new_model
|
594
|
+
@machine = StateMachine::Machine.new(@model)
|
595
|
+
@machine.state :parked, :idling
|
596
|
+
@machine.event :ignite
|
597
|
+
@machine.after_transition(lambda {@after_count += 1; false})
|
598
|
+
@machine.after_transition(lambda {@after_count += 1})
|
599
|
+
|
600
|
+
@record = @model.new(:state => 'parked')
|
601
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
602
|
+
@result = @transition.perform
|
603
|
+
end
|
604
|
+
|
605
|
+
def test_should_be_successful
|
606
|
+
assert @result
|
607
|
+
end
|
608
|
+
|
609
|
+
def test_should_change_current_state
|
610
|
+
assert_equal 'idling', @record.state
|
611
|
+
end
|
612
|
+
|
613
|
+
def test_should_save_record
|
614
|
+
assert !@record.new_record?
|
615
|
+
end
|
616
|
+
|
617
|
+
def test_should_not_run_further_after_callbacks
|
618
|
+
assert_equal 1, @after_count
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
class MachineWithEventAttributesOnValidationTest < ActiveRecord::TestCase
|
623
|
+
def setup
|
624
|
+
@model = new_model
|
625
|
+
@machine = StateMachine::Machine.new(@model)
|
626
|
+
@machine.event :ignite do
|
627
|
+
transition :parked => :idling
|
628
|
+
end
|
629
|
+
|
630
|
+
@record = @model.new
|
631
|
+
@record.state = 'parked'
|
632
|
+
@record.state_event = 'ignite'
|
633
|
+
end
|
634
|
+
|
635
|
+
def test_should_fail_if_event_is_invalid
|
636
|
+
@record.state_event = 'invalid'
|
637
|
+
assert !@record.valid?
|
638
|
+
assert_equal ['State event is invalid'], @record.errors.full_messages
|
639
|
+
end
|
640
|
+
|
641
|
+
def test_should_fail_if_event_has_no_transition
|
642
|
+
@record.state = 'idling'
|
643
|
+
assert !@record.valid?
|
644
|
+
assert_equal ['State event cannot transition when idling'], @record.errors.full_messages
|
645
|
+
end
|
646
|
+
|
647
|
+
def test_should_be_successful_if_event_has_transition
|
648
|
+
assert @record.valid?
|
649
|
+
end
|
650
|
+
|
651
|
+
def test_should_run_before_callbacks
|
652
|
+
ran_callback = false
|
653
|
+
@machine.before_transition { ran_callback = true }
|
654
|
+
|
655
|
+
@record.valid?
|
656
|
+
assert ran_callback
|
657
|
+
end
|
658
|
+
|
659
|
+
def test_should_persist_new_state
|
660
|
+
@record.valid?
|
661
|
+
assert_equal 'idling', @record.state
|
662
|
+
end
|
663
|
+
|
664
|
+
def test_should_not_run_after_callbacks
|
665
|
+
ran_callback = false
|
666
|
+
@machine.after_transition { ran_callback = true }
|
667
|
+
|
668
|
+
@record.valid?
|
669
|
+
assert !ran_callback
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
class MachineWithEventAttributesOnSaveBangTest < ActiveRecord::TestCase
|
674
|
+
def setup
|
675
|
+
@model = new_model
|
676
|
+
@machine = StateMachine::Machine.new(@model)
|
677
|
+
@machine.event :ignite do
|
678
|
+
transition :parked => :idling
|
679
|
+
end
|
680
|
+
|
681
|
+
@record = @model.new
|
682
|
+
@record.state = 'parked'
|
683
|
+
@record.state_event = 'ignite'
|
684
|
+
end
|
685
|
+
|
686
|
+
def test_should_fail_if_event_is_invalid
|
687
|
+
@record.state_event = 'invalid'
|
688
|
+
assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
|
689
|
+
end
|
690
|
+
|
691
|
+
def test_should_fail_if_event_has_no_transition
|
692
|
+
@record.state = 'idling'
|
693
|
+
assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
|
694
|
+
end
|
695
|
+
|
696
|
+
def test_should_be_successful_if_event_has_transition
|
697
|
+
assert_equal true, @record.save!
|
698
|
+
end
|
699
|
+
|
700
|
+
def test_should_run_before_callbacks
|
701
|
+
ran_callback = false
|
702
|
+
@machine.before_transition { ran_callback = true }
|
703
|
+
|
704
|
+
@record.save!
|
705
|
+
assert ran_callback
|
706
|
+
end
|
707
|
+
|
708
|
+
def test_should_persist_new_state
|
709
|
+
@record.save!
|
710
|
+
assert_equal 'idling', @record.state
|
711
|
+
end
|
712
|
+
|
713
|
+
def test_should_run_after_callbacks
|
714
|
+
ran_callback = false
|
715
|
+
@machine.after_transition { ran_callback = true }
|
716
|
+
|
717
|
+
@record.save!
|
718
|
+
assert ran_callback
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
class MachineWithEventAttributesOnCustomActionTest < ActiveRecord::TestCase
|
723
|
+
def setup
|
724
|
+
@superclass = new_model do
|
725
|
+
def persist
|
726
|
+
create_or_update
|
727
|
+
end
|
728
|
+
end
|
729
|
+
@model = Class.new(@superclass)
|
730
|
+
@machine = StateMachine::Machine.new(@model, :action => :persist)
|
731
|
+
@machine.event :ignite do
|
732
|
+
transition :parked => :idling
|
733
|
+
end
|
734
|
+
|
735
|
+
@record = @model.new
|
736
|
+
@record.state = 'parked'
|
737
|
+
@record.state_event = 'ignite'
|
738
|
+
end
|
739
|
+
|
740
|
+
def test_should_not_transition_on_valid?
|
741
|
+
@record.valid?
|
742
|
+
assert_equal 'parked', @record.state
|
743
|
+
end
|
744
|
+
|
745
|
+
def test_should_not_transition_on_save
|
746
|
+
@record.save
|
747
|
+
assert_equal 'parked', @record.state
|
748
|
+
end
|
749
|
+
|
750
|
+
def test_should_not_transition_on_save!
|
751
|
+
@record.save!
|
752
|
+
assert_equal 'parked', @record.state
|
753
|
+
end
|
754
|
+
|
755
|
+
def test_should_transition_on_custom_action
|
756
|
+
@record.persist
|
757
|
+
assert_equal 'idling', @record.state
|
758
|
+
end
|
759
|
+
end
|
760
|
+
|
761
|
+
class MachineWithObserversTest < ActiveRecord::TestCase
|
762
|
+
def setup
|
763
|
+
@model = new_model
|
764
|
+
@machine = StateMachine::Machine.new(@model)
|
765
|
+
@machine.state :parked, :idling
|
766
|
+
@machine.event :ignite
|
767
|
+
@record = @model.new(:state => 'parked')
|
768
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
769
|
+
end
|
770
|
+
|
771
|
+
def test_should_call_all_transition_callback_permutations
|
772
|
+
callbacks = [
|
773
|
+
:before_ignite_from_parked_to_idling,
|
774
|
+
:before_ignite_from_parked,
|
775
|
+
:before_ignite_to_idling,
|
776
|
+
:before_ignite,
|
777
|
+
:before_transition_state_from_parked_to_idling,
|
778
|
+
:before_transition_state_from_parked,
|
779
|
+
:before_transition_state_to_idling,
|
780
|
+
:before_transition_state,
|
781
|
+
:before_transition
|
782
|
+
]
|
783
|
+
|
784
|
+
notified = false
|
785
|
+
observer = new_observer(@model) do
|
786
|
+
callbacks.each do |callback|
|
787
|
+
define_method(callback) do |*args|
|
788
|
+
notifications << callback
|
789
|
+
end
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
instance = observer.instance
|
794
|
+
|
795
|
+
@transition.perform
|
796
|
+
assert_equal callbacks, instance.notifications
|
797
|
+
end
|
798
|
+
|
799
|
+
def test_should_pass_record_and_transition_to_before_callbacks
|
800
|
+
observer = new_observer(@model) do
|
801
|
+
def before_transition(*args)
|
802
|
+
notifications << args
|
803
|
+
end
|
804
|
+
end
|
805
|
+
instance = observer.instance
|
806
|
+
|
807
|
+
@transition.perform
|
808
|
+
assert_equal [[@record, @transition]], instance.notifications
|
809
|
+
end
|
810
|
+
|
811
|
+
def test_should_pass_record_and_transition_to_after_callbacks
|
812
|
+
observer = new_observer(@model) do
|
813
|
+
def after_transition(*args)
|
814
|
+
notifications << args
|
815
|
+
end
|
816
|
+
end
|
817
|
+
instance = observer.instance
|
818
|
+
|
819
|
+
@transition.perform
|
820
|
+
assert_equal [[@record, @transition]], instance.notifications
|
821
|
+
end
|
822
|
+
|
823
|
+
def test_should_call_methods_outside_the_context_of_the_record
|
824
|
+
observer = new_observer(@model) do
|
825
|
+
def before_ignite(*args)
|
826
|
+
notifications << self
|
827
|
+
end
|
828
|
+
end
|
829
|
+
instance = observer.instance
|
830
|
+
|
831
|
+
@transition.perform
|
832
|
+
assert_equal [instance], instance.notifications
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
class MachineWithNamespacedObserversTest < ActiveRecord::TestCase
|
837
|
+
def setup
|
838
|
+
@model = new_model
|
839
|
+
@machine = StateMachine::Machine.new(@model, :state, :namespace => 'alarm')
|
840
|
+
@machine.state :active, :off
|
841
|
+
@machine.event :enable
|
842
|
+
@record = @model.new(:state => 'off')
|
843
|
+
@transition = StateMachine::Transition.new(@record, @machine, :enable, :off, :active)
|
844
|
+
end
|
845
|
+
|
846
|
+
def test_should_call_namespaced_before_event_method
|
847
|
+
observer = new_observer(@model) do
|
848
|
+
def before_enable_alarm(*args)
|
849
|
+
notifications << args
|
850
|
+
end
|
851
|
+
end
|
852
|
+
instance = observer.instance
|
853
|
+
|
854
|
+
@transition.perform
|
855
|
+
assert_equal [[@record, @transition]], instance.notifications
|
856
|
+
end
|
857
|
+
|
858
|
+
def test_should_call_namespaced_after_event_method
|
859
|
+
observer = new_observer(@model) do
|
860
|
+
def after_enable_alarm(*args)
|
861
|
+
notifications << args
|
862
|
+
end
|
863
|
+
end
|
864
|
+
instance = observer.instance
|
865
|
+
|
866
|
+
@transition.perform
|
867
|
+
assert_equal [[@record, @transition]], instance.notifications
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
class MachineWithMixedCallbacksTest < ActiveRecord::TestCase
|
872
|
+
def setup
|
873
|
+
@model = new_model
|
874
|
+
@machine = StateMachine::Machine.new(@model)
|
875
|
+
@machine.state :parked, :idling
|
876
|
+
@machine.event :ignite
|
877
|
+
@record = @model.new(:state => 'parked')
|
878
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
879
|
+
|
880
|
+
@notifications = []
|
881
|
+
|
882
|
+
# Create callbacks
|
883
|
+
@machine.before_transition(lambda {@notifications << :callback_before_transition})
|
884
|
+
@machine.after_transition(lambda {@notifications << :callback_after_transition})
|
885
|
+
|
886
|
+
# Create observer callbacks
|
887
|
+
observer = new_observer(@model) do
|
888
|
+
def before_ignite(*args)
|
889
|
+
notifications << :observer_before_ignite
|
890
|
+
end
|
891
|
+
|
892
|
+
def before_transition(*args)
|
893
|
+
notifications << :observer_before_transition
|
894
|
+
end
|
895
|
+
|
896
|
+
def after_ignite(*args)
|
897
|
+
notifications << :observer_after_ignite
|
898
|
+
end
|
899
|
+
|
900
|
+
def after_transition(*args)
|
901
|
+
notifications << :observer_after_transition
|
902
|
+
end
|
903
|
+
end
|
904
|
+
instance = observer.instance
|
905
|
+
instance.notifications = @notifications
|
906
|
+
|
907
|
+
@transition.perform
|
908
|
+
end
|
909
|
+
|
910
|
+
def test_should_invoke_callbacks_in_specific_order
|
911
|
+
expected = [
|
912
|
+
:callback_before_transition,
|
913
|
+
:observer_before_ignite,
|
914
|
+
:observer_before_transition,
|
915
|
+
:callback_after_transition,
|
916
|
+
:observer_after_ignite,
|
917
|
+
:observer_after_transition
|
918
|
+
]
|
919
|
+
|
920
|
+
assert_equal expected, @notifications
|
921
|
+
end
|
922
|
+
end
|
923
|
+
|
924
|
+
if Object.const_defined?(:I18n)
|
925
|
+
class MachineWithInternationalizationTest < ActiveRecord::TestCase
|
926
|
+
def setup
|
927
|
+
I18n.backend = I18n::Backend::Simple.new
|
928
|
+
|
929
|
+
# Initialize the backend
|
930
|
+
I18n.backend.translate(:en, 'activerecord.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
|
931
|
+
|
932
|
+
@model = new_model
|
933
|
+
end
|
934
|
+
|
935
|
+
def test_should_invalidate_using_i18n_default
|
936
|
+
I18n.backend.store_translations(:en, {
|
937
|
+
:activerecord => {
|
938
|
+
:errors => {
|
939
|
+
:messages => {
|
940
|
+
:invalid_transition => 'cannot {{event}}'
|
941
|
+
}
|
942
|
+
}
|
943
|
+
}
|
944
|
+
})
|
945
|
+
|
946
|
+
machine = StateMachine::Machine.new(@model)
|
947
|
+
machine.state :parked, :idling
|
948
|
+
event = StateMachine::Event.new(machine, :ignite)
|
949
|
+
|
950
|
+
record = @model.new(:state => 'idling')
|
951
|
+
|
952
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
|
953
|
+
assert_equal 'cannot ignite', record.errors.on(:state)
|
954
|
+
end
|
955
|
+
|
956
|
+
def test_should_invalidate_using_customized_i18n_key_if_specified
|
957
|
+
I18n.backend.store_translations(:en, {
|
958
|
+
:activerecord => {
|
959
|
+
:errors => {
|
960
|
+
:messages => {
|
961
|
+
:bad_transition => 'cannot {{event}}'
|
962
|
+
}
|
963
|
+
}
|
964
|
+
}
|
965
|
+
})
|
966
|
+
|
967
|
+
machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => :bad_transition})
|
968
|
+
machine.state :parked, :idling
|
969
|
+
|
970
|
+
record = @model.new(:state => 'idling')
|
971
|
+
|
972
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
|
973
|
+
assert_equal 'cannot ignite', record.errors.on(:state)
|
974
|
+
end
|
975
|
+
|
976
|
+
def test_should_invalidate_using_customized_i18n_string_if_specified
|
977
|
+
machine = StateMachine::Machine.new(@model, :messages => {:invalid_transition => 'cannot {{event}}'})
|
978
|
+
machine.state :parked, :idling
|
979
|
+
|
980
|
+
record = @model.new(:state => 'idling')
|
981
|
+
|
982
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, :ignite]])
|
983
|
+
assert_equal 'cannot ignite', record.errors.on(:state)
|
984
|
+
end
|
985
|
+
|
986
|
+
def test_should_only_add_locale_once_in_load_path
|
987
|
+
assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_record/locale\.rb$}}.length
|
988
|
+
|
989
|
+
# Create another ActiveRecord model that will triger the i18n feature
|
990
|
+
new_model
|
991
|
+
|
992
|
+
assert_equal 1, I18n.load_path.select {|path| path =~ %r{state_machine/integrations/active_record/locale\.rb$}}.length
|
993
|
+
end
|
994
|
+
end
|
995
|
+
else
|
996
|
+
$stderr.puts 'Skipping ActiveRecord I18n tests. `gem install active_record` >= v2.2.0 and try again.'
|
997
|
+
end
|
998
|
+
end
|
999
|
+
rescue LoadError
|
1000
|
+
$stderr.puts "Skipping ActiveRecord tests. `gem install activerecord#{" -v #{ENV['AR_VERSION']}" if ENV['AR_VERSION']}` and try again."
|
1001
|
+
end
|