joelind-state_machine 0.8.1
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 +297 -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 +388 -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 +252 -0
- data/lib/state_machine/event_collection.rb +122 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/guard.rb +230 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +492 -0
- data/lib/state_machine/integrations/active_record/locale.rb +11 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +351 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +322 -0
- data/lib/state_machine/machine.rb +1467 -0
- data/lib/state_machine/machine_collection.rb +155 -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 +394 -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 +120 -0
- data/test/unit/event_collection_test.rb +326 -0
- data/test/unit/event_test.rb +743 -0
- data/test/unit/guard_test.rb +908 -0
- data/test/unit/integrations/active_record_test.rb +1374 -0
- data/test/unit/integrations/data_mapper_test.rb +962 -0
- data/test/unit/integrations/sequel_test.rb +859 -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 +938 -0
- data/test/unit/machine_test.rb +2004 -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 +1212 -0
- metadata +163 -0
@@ -0,0 +1,859 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
|
2
|
+
|
3
|
+
begin
|
4
|
+
# Load library
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
gem 'sequel', ENV['SEQUEL_VERSION'] ? "=#{ENV['SEQUEL_VERSION']}" : '>=2.8.0'
|
8
|
+
require 'sequel'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
# Establish database connection
|
12
|
+
DB = Sequel.connect('sqlite:///', :loggers => [Logger.new("#{File.dirname(__FILE__)}/../../sequel.log")])
|
13
|
+
|
14
|
+
module SequelTest
|
15
|
+
class BaseTestCase < Test::Unit::TestCase
|
16
|
+
def default_test
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
# Creates a new Sequel model (and the associated table)
|
21
|
+
def new_model(auto_migrate = true, &block)
|
22
|
+
DB.create_table! :foo do
|
23
|
+
primary_key :id
|
24
|
+
column :state, :string
|
25
|
+
end if auto_migrate
|
26
|
+
model = Class.new(Sequel::Model(:foo)) do
|
27
|
+
self.raise_on_save_failure = false
|
28
|
+
def self.name; 'SequelTest::Foo'; end
|
29
|
+
end
|
30
|
+
model.plugin(:validation_class_methods) if model.respond_to?(:plugin)
|
31
|
+
model.plugin(:hook_class_methods) if model.respond_to?(:plugin)
|
32
|
+
model.class_eval(&block) if block_given?
|
33
|
+
model
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class IntegrationTest < BaseTestCase
|
38
|
+
def test_should_match_if_class_inherits_from_sequel
|
39
|
+
assert StateMachine::Integrations::Sequel.matches?(new_model)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_should_not_match_if_class_does_not_inherit_from_sequel
|
43
|
+
assert !StateMachine::Integrations::Sequel.matches?(Class.new)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class MachineByDefaultTest < BaseTestCase
|
48
|
+
def setup
|
49
|
+
@model = new_model
|
50
|
+
@machine = StateMachine::Machine.new(@model)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_should_use_save_as_action
|
54
|
+
assert_equal :save, @machine.action
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_should_use_transactions
|
58
|
+
assert_equal true, @machine.use_transactions
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class MachineTest < BaseTestCase
|
63
|
+
def setup
|
64
|
+
@model = new_model
|
65
|
+
@machine = StateMachine::Machine.new(@model)
|
66
|
+
@machine.state :parked, :first_gear
|
67
|
+
@machine.state :idling, :value => lambda {'idling'}
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_should_create_singular_with_scope
|
71
|
+
assert @model.respond_to?(:with_state)
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_should_only_include_records_with_state_in_singular_with_scope
|
75
|
+
parked = @model.create :state => 'parked'
|
76
|
+
idling = @model.create :state => 'idling'
|
77
|
+
|
78
|
+
assert_equal [parked], @model.with_state(:parked).all
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_should_create_plural_with_scope
|
82
|
+
assert @model.respond_to?(:with_states)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_should_only_include_records_with_states_in_plural_with_scope
|
86
|
+
parked = @model.create :state => 'parked'
|
87
|
+
idling = @model.create :state => 'idling'
|
88
|
+
|
89
|
+
assert_equal [parked, idling], @model.with_states(:parked, :idling).all
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_should_create_singular_without_scope
|
93
|
+
assert @model.respond_to?(:without_state)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_should_only_include_records_without_state_in_singular_without_scope
|
97
|
+
parked = @model.create :state => 'parked'
|
98
|
+
idling = @model.create :state => 'idling'
|
99
|
+
|
100
|
+
assert_equal [parked], @model.without_state(:idling).all
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_should_create_plural_without_scope
|
104
|
+
assert @model.respond_to?(:without_states)
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_should_only_include_records_without_states_in_plural_without_scope
|
108
|
+
parked = @model.create :state => 'parked'
|
109
|
+
idling = @model.create :state => 'idling'
|
110
|
+
first_gear = @model.create :state => 'first_gear'
|
111
|
+
|
112
|
+
assert_equal [parked, idling], @model.without_states(:first_gear).all
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_should_allow_chaining_scopes_and_fitlers
|
116
|
+
parked = @model.create :state => 'parked'
|
117
|
+
idling = @model.create :state => 'idling'
|
118
|
+
|
119
|
+
assert_equal [idling], @model.without_state(:parked).filter(:state => 'idling').all
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_should_rollback_transaction_if_false
|
123
|
+
@machine.within_transaction(@model.new) do
|
124
|
+
@model.create
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
assert_equal 0, @model.count
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_should_not_rollback_transaction_if_true
|
132
|
+
@machine.within_transaction(@model.new) do
|
133
|
+
@model.create
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
assert_equal 1, @model.count
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_should_invalidate_using_errors
|
141
|
+
record = @model.new
|
142
|
+
record.state = 'parked'
|
143
|
+
|
144
|
+
@machine.invalidate(record, :state, :invalid_transition, [[:event, :park]])
|
145
|
+
|
146
|
+
assert_equal ['cannot transition via "park"'], record.errors.on(:state)
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_should_auto_prefix_custom_attributes_on_invalidation
|
150
|
+
record = @model.new
|
151
|
+
@machine.invalidate(record, :event, :invalid)
|
152
|
+
|
153
|
+
assert_equal ['is invalid'], record.errors.on(:state_event)
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_should_clear_errors_on_reset
|
157
|
+
record = @model.new
|
158
|
+
record.state = 'parked'
|
159
|
+
record.errors.add(:state, 'is invalid')
|
160
|
+
|
161
|
+
@machine.reset(record)
|
162
|
+
assert_nil record.errors.on(:id)
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_should_not_override_the_column_reader
|
166
|
+
record = @model.new
|
167
|
+
record[:state] = 'parked'
|
168
|
+
assert_equal 'parked', record.state
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_should_not_override_the_column_writer
|
172
|
+
record = @model.new
|
173
|
+
record.state = 'parked'
|
174
|
+
assert_equal 'parked', record[:state]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class MachineUnmigratedTest < BaseTestCase
|
179
|
+
def setup
|
180
|
+
@model = new_model(false)
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_should_allow_machine_creation
|
184
|
+
assert_nothing_raised { StateMachine::Machine.new(@model) }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class MachineWithStaticInitialStateTest < BaseTestCase
|
189
|
+
def setup
|
190
|
+
@model = new_model do
|
191
|
+
attr_accessor :value
|
192
|
+
end
|
193
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_should_set_initial_state_on_created_object
|
197
|
+
record = @model.new
|
198
|
+
assert_equal 'parked', record.state
|
199
|
+
end
|
200
|
+
|
201
|
+
def test_should_still_set_attributes
|
202
|
+
record = @model.new(:value => 1)
|
203
|
+
assert_equal 1, record.value
|
204
|
+
end
|
205
|
+
|
206
|
+
def test_should_still_allow_initialize_blocks
|
207
|
+
block_args = nil
|
208
|
+
record = @model.new do |*args|
|
209
|
+
block_args = args
|
210
|
+
end
|
211
|
+
|
212
|
+
assert_equal [record], block_args
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_should_not_have_any_changed_columns
|
216
|
+
record = @model.new
|
217
|
+
assert record.changed_columns.empty?
|
218
|
+
end
|
219
|
+
|
220
|
+
def test_should_set_attributes_prior_to_after_initialize_hook
|
221
|
+
state = nil
|
222
|
+
@model.class_eval do
|
223
|
+
define_method(:after_initialize) do
|
224
|
+
state = self.state
|
225
|
+
end
|
226
|
+
end
|
227
|
+
@model.new
|
228
|
+
assert_equal 'parked', state
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_should_set_initial_state_before_setting_attributes
|
232
|
+
@model.class_eval do
|
233
|
+
attr_accessor :state_during_setter
|
234
|
+
|
235
|
+
define_method(:value=) do |value|
|
236
|
+
self.state_during_setter = state
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
record = @model.new(:value => 1)
|
241
|
+
assert_equal 'parked', record.state_during_setter
|
242
|
+
end
|
243
|
+
|
244
|
+
def test_should_not_set_initial_state_after_already_initialized
|
245
|
+
record = @model.new(:value => 1)
|
246
|
+
assert_equal 'parked', record.state
|
247
|
+
|
248
|
+
record.state = 'idling'
|
249
|
+
record.set({})
|
250
|
+
assert_equal 'idling', record.state
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class MachineWithDynamicInitialStateTest < BaseTestCase
|
255
|
+
def setup
|
256
|
+
@model = new_model do
|
257
|
+
attr_accessor :value
|
258
|
+
end
|
259
|
+
@machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked})
|
260
|
+
@machine.state :parked
|
261
|
+
end
|
262
|
+
|
263
|
+
def test_should_set_initial_state_on_created_object
|
264
|
+
record = @model.new
|
265
|
+
assert_equal 'parked', record.state
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_should_still_set_attributes
|
269
|
+
record = @model.new(:value => 1)
|
270
|
+
assert_equal 1, record.value
|
271
|
+
end
|
272
|
+
|
273
|
+
def test_should_still_allow_initialize_blocks
|
274
|
+
block_args = nil
|
275
|
+
record = @model.new do |*args|
|
276
|
+
block_args = args
|
277
|
+
end
|
278
|
+
|
279
|
+
assert_equal [record], block_args
|
280
|
+
end
|
281
|
+
|
282
|
+
def test_should_not_have_any_changed_columns
|
283
|
+
record = @model.new
|
284
|
+
assert record.changed_columns.empty?
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_should_set_attributes_prior_to_after_initialize_hook
|
288
|
+
state = nil
|
289
|
+
@model.class_eval do
|
290
|
+
define_method(:after_initialize) do
|
291
|
+
state = self.state
|
292
|
+
end
|
293
|
+
end
|
294
|
+
@model.new
|
295
|
+
assert_equal 'parked', state
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_should_set_initial_state_after_setting_attributes
|
299
|
+
@model.class_eval do
|
300
|
+
attr_accessor :state_during_setter
|
301
|
+
|
302
|
+
define_method(:value=) do |value|
|
303
|
+
self.state_during_setter = state || 'nil'
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
record = @model.new(:value => 1)
|
308
|
+
assert_equal 'nil', record.state_during_setter
|
309
|
+
end
|
310
|
+
|
311
|
+
def test_should_not_set_initial_state_after_already_initialized
|
312
|
+
record = @model.new(:value => 1)
|
313
|
+
assert_equal 'parked', record.state
|
314
|
+
|
315
|
+
record.state = 'idling'
|
316
|
+
record.set({})
|
317
|
+
assert_equal 'idling', record.state
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class MachineWithColumnDefaultTest < BaseTestCase
|
322
|
+
def setup
|
323
|
+
@model = new_model
|
324
|
+
DB.alter_table :foo do
|
325
|
+
add_column :status, :string, :default => 'idling'
|
326
|
+
end
|
327
|
+
@model.class_eval { get_db_schema(true) }
|
328
|
+
|
329
|
+
@machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
|
330
|
+
@record = @model.new
|
331
|
+
end
|
332
|
+
|
333
|
+
def test_should_use_machine_default
|
334
|
+
assert_equal 'parked', @record.status
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
|
339
|
+
def setup
|
340
|
+
@model = new_model do
|
341
|
+
def initialize
|
342
|
+
# Skip attribute initialization
|
343
|
+
@initialized_state_machines = true
|
344
|
+
super
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
@machine = StateMachine::Machine.new(@model, :status, :initial => 'parked')
|
349
|
+
@record = @model.new
|
350
|
+
end
|
351
|
+
|
352
|
+
def test_should_not_define_a_reader_attribute_for_the_attribute
|
353
|
+
assert !@record.respond_to?(:status)
|
354
|
+
end
|
355
|
+
|
356
|
+
def test_should_not_define_a_writer_attribute_for_the_attribute
|
357
|
+
assert !@record.respond_to?(:status=)
|
358
|
+
end
|
359
|
+
|
360
|
+
def test_should_define_an_attribute_predicate
|
361
|
+
assert @record.respond_to?(:status?)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
|
366
|
+
def setup
|
367
|
+
@model = new_model do
|
368
|
+
attr_accessor :status
|
369
|
+
end
|
370
|
+
|
371
|
+
@machine = StateMachine::Machine.new(@model, :status, :initial => 'parked')
|
372
|
+
@record = @model.new
|
373
|
+
end
|
374
|
+
|
375
|
+
def test_should_set_initial_state_on_created_object
|
376
|
+
assert_equal 'parked', @record.status
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
class MachineWithComplexPluralizationTest < BaseTestCase
|
381
|
+
def setup
|
382
|
+
@model = new_model
|
383
|
+
@machine = StateMachine::Machine.new(@model, :status)
|
384
|
+
end
|
385
|
+
|
386
|
+
def test_should_create_singular_with_scope
|
387
|
+
assert @model.respond_to?(:with_status)
|
388
|
+
end
|
389
|
+
|
390
|
+
def test_should_create_plural_with_scope
|
391
|
+
assert @model.respond_to?(:with_statuses)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
class MachineWithOwnerSubclassTest < BaseTestCase
|
396
|
+
def setup
|
397
|
+
@model = new_model
|
398
|
+
@machine = StateMachine::Machine.new(@model, :state)
|
399
|
+
|
400
|
+
@subclass = Class.new(@model)
|
401
|
+
@subclass_machine = @subclass.state_machine(:state) {}
|
402
|
+
@subclass_machine.state :parked, :idling, :first_gear
|
403
|
+
end
|
404
|
+
|
405
|
+
def test_should_only_include_records_with_subclass_states_in_with_scope
|
406
|
+
parked = @subclass.create :state => 'parked'
|
407
|
+
idling = @subclass.create :state => 'idling'
|
408
|
+
|
409
|
+
assert_equal [parked, idling], @subclass.with_states(:parked, :idling).all
|
410
|
+
end
|
411
|
+
|
412
|
+
def test_should_only_include_records_without_subclass_states_in_without_scope
|
413
|
+
parked = @subclass.create :state => 'parked'
|
414
|
+
idling = @subclass.create :state => 'idling'
|
415
|
+
first_gear = @subclass.create :state => 'first_gear'
|
416
|
+
|
417
|
+
assert_equal [parked, idling], @subclass.without_states(:first_gear).all
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
class MachineWithCustomAttributeTest < BaseTestCase
|
422
|
+
def setup
|
423
|
+
@model = new_model do
|
424
|
+
alias_method :vehicle_status, :state
|
425
|
+
alias_method :vehicle_status=, :state=
|
426
|
+
end
|
427
|
+
|
428
|
+
@machine = StateMachine::Machine.new(@model, :status, :attribute => :vehicle_status)
|
429
|
+
@machine.state :parked
|
430
|
+
|
431
|
+
@record = @model.new
|
432
|
+
end
|
433
|
+
|
434
|
+
def test_should_add_validation_errors_to_custom_attribute
|
435
|
+
@record.vehicle_status = 'invalid'
|
436
|
+
|
437
|
+
assert !@record.valid?
|
438
|
+
assert_equal ['is invalid'], @record.errors.on(:vehicle_status)
|
439
|
+
|
440
|
+
@record.vehicle_status = 'parked'
|
441
|
+
assert @record.valid?
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
class MachineWithInitializedStateTest < BaseTestCase
|
446
|
+
def setup
|
447
|
+
@model = new_model
|
448
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
449
|
+
@machine.state nil, :idling
|
450
|
+
end
|
451
|
+
|
452
|
+
def test_should_allow_nil_initial_state_when_static
|
453
|
+
record = @model.new(:state => nil)
|
454
|
+
assert_nil record.state
|
455
|
+
end
|
456
|
+
|
457
|
+
def test_should_allow_nil_initial_state_when_dynamic
|
458
|
+
@machine.initial_state = lambda {:parked}
|
459
|
+
record = @model.new(:state => nil)
|
460
|
+
assert_nil record.state
|
461
|
+
end
|
462
|
+
|
463
|
+
def test_should_allow_different_initial_state_when_static
|
464
|
+
record = @model.new(:state => 'idling')
|
465
|
+
assert_equal 'idling', record.state
|
466
|
+
end
|
467
|
+
|
468
|
+
def test_should_allow_different_initial_state_when_dynamic
|
469
|
+
@machine.initial_state = lambda {:parked}
|
470
|
+
record = @model.new(:state => 'idling')
|
471
|
+
assert_equal 'idling', record.state
|
472
|
+
end
|
473
|
+
|
474
|
+
def test_should_use_default_state_if_protected
|
475
|
+
@model.class_eval do
|
476
|
+
self.strict_param_setting = false
|
477
|
+
set_restricted_columns :state
|
478
|
+
end
|
479
|
+
|
480
|
+
record = @model.new(:state => 'idling')
|
481
|
+
assert_equal 'parked', record.state
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
class MachineWithCallbacksTest < BaseTestCase
|
486
|
+
def setup
|
487
|
+
@model = new_model
|
488
|
+
@machine = StateMachine::Machine.new(@model)
|
489
|
+
@machine.state :parked, :idling
|
490
|
+
@machine.event :ignite
|
491
|
+
@record = @model.new(:state => 'parked')
|
492
|
+
@transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
493
|
+
end
|
494
|
+
|
495
|
+
def test_should_run_before_callbacks
|
496
|
+
called = false
|
497
|
+
@machine.before_transition(lambda {called = true})
|
498
|
+
|
499
|
+
@transition.perform
|
500
|
+
assert called
|
501
|
+
end
|
502
|
+
|
503
|
+
def test_should_pass_transition_to_before_callbacks_with_one_argument
|
504
|
+
transition = nil
|
505
|
+
@machine.before_transition(lambda {|arg| transition = arg})
|
506
|
+
|
507
|
+
@transition.perform
|
508
|
+
assert_equal @transition, transition
|
509
|
+
end
|
510
|
+
|
511
|
+
def test_should_pass_transition_to_before_callbacks_with_multiple_arguments
|
512
|
+
callback_args = nil
|
513
|
+
@machine.before_transition(lambda {|*args| callback_args = args})
|
514
|
+
|
515
|
+
@transition.perform
|
516
|
+
assert_equal [@transition], callback_args
|
517
|
+
end
|
518
|
+
|
519
|
+
def test_should_run_before_callbacks_within_the_context_of_the_record
|
520
|
+
context = nil
|
521
|
+
@machine.before_transition(lambda {context = self})
|
522
|
+
|
523
|
+
@transition.perform
|
524
|
+
assert_equal @record, context
|
525
|
+
end
|
526
|
+
|
527
|
+
def test_should_run_after_callbacks
|
528
|
+
called = false
|
529
|
+
@machine.after_transition(lambda {called = true})
|
530
|
+
|
531
|
+
@transition.perform
|
532
|
+
assert called
|
533
|
+
end
|
534
|
+
|
535
|
+
def test_should_pass_transition_to_after_callbacks_with_multiple_arguments
|
536
|
+
callback_args = nil
|
537
|
+
@machine.after_transition(lambda {|*args| callback_args = args})
|
538
|
+
|
539
|
+
@transition.perform
|
540
|
+
assert_equal [@transition], callback_args
|
541
|
+
end
|
542
|
+
|
543
|
+
def test_should_run_after_callbacks_with_the_context_of_the_record
|
544
|
+
context = nil
|
545
|
+
@machine.after_transition(lambda {context = self})
|
546
|
+
|
547
|
+
@transition.perform
|
548
|
+
assert_equal @record, context
|
549
|
+
end
|
550
|
+
|
551
|
+
def test_should_allow_symbolic_callbacks
|
552
|
+
callback_args = nil
|
553
|
+
|
554
|
+
klass = class << @record; self; end
|
555
|
+
klass.send(:define_method, :after_ignite) do |*args|
|
556
|
+
callback_args = args
|
557
|
+
end
|
558
|
+
|
559
|
+
@machine.before_transition(:after_ignite)
|
560
|
+
|
561
|
+
@transition.perform
|
562
|
+
assert_equal [@transition], callback_args
|
563
|
+
end
|
564
|
+
|
565
|
+
def test_should_allow_string_callbacks
|
566
|
+
class << @record
|
567
|
+
attr_reader :callback_result
|
568
|
+
end
|
569
|
+
|
570
|
+
@machine.before_transition('@callback_result = [1, 2, 3]')
|
571
|
+
@transition.perform
|
572
|
+
|
573
|
+
assert_equal [1, 2, 3], @record.callback_result
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
class MachineWithLoopbackTest < BaseTestCase
|
578
|
+
def setup
|
579
|
+
changed_columns = nil
|
580
|
+
|
581
|
+
@model = new_model do
|
582
|
+
# Simulate timestamps plugin
|
583
|
+
define_method(:before_update) do
|
584
|
+
changed_columns = self.changed_columns.dup
|
585
|
+
|
586
|
+
super()
|
587
|
+
self.updated_at = Time.now if changed_columns.any?
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
DB.alter_table :foo do
|
592
|
+
add_column :updated_at, :datetime
|
593
|
+
end
|
594
|
+
@model.class_eval { get_db_schema(true) }
|
595
|
+
|
596
|
+
@machine = StateMachine::Machine.new(@model, :initial => :parked)
|
597
|
+
@machine.event :park
|
598
|
+
|
599
|
+
@record = @model.create(:updated_at => Time.now - 1)
|
600
|
+
@timestamp = @record.updated_at
|
601
|
+
|
602
|
+
@transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
|
603
|
+
@transition.perform
|
604
|
+
|
605
|
+
@changed_columns = changed_columns
|
606
|
+
end
|
607
|
+
|
608
|
+
def test_should_include_state_in_changed_columns
|
609
|
+
assert_equal [:state], @changed_columns
|
610
|
+
end
|
611
|
+
|
612
|
+
def test_should_update_record
|
613
|
+
assert_not_equal @timestamp, @record.updated_at
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
class MachineWithValidationsTest < BaseTestCase
|
618
|
+
def setup
|
619
|
+
@model = new_model
|
620
|
+
@machine = StateMachine::Machine.new(@model)
|
621
|
+
@machine.state :parked
|
622
|
+
|
623
|
+
@record = @model.new
|
624
|
+
end
|
625
|
+
|
626
|
+
def test_should_be_valid_if_state_is_known
|
627
|
+
@record.state = 'parked'
|
628
|
+
|
629
|
+
assert @record.valid?
|
630
|
+
end
|
631
|
+
|
632
|
+
def test_should_not_be_valid_if_state_is_unknown
|
633
|
+
@record.state = 'invalid'
|
634
|
+
|
635
|
+
assert !@record.valid?
|
636
|
+
assert_equal ['state is invalid'], @record.errors.full_messages
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
class MachineWithStateDrivenValidationsTest < BaseTestCase
|
641
|
+
def setup
|
642
|
+
@model = new_model do
|
643
|
+
attr_accessor :seatbelt
|
644
|
+
end
|
645
|
+
|
646
|
+
@machine = StateMachine::Machine.new(@model)
|
647
|
+
@machine.state :first_gear do
|
648
|
+
validates_presence_of :seatbelt
|
649
|
+
end
|
650
|
+
@machine.other_states :parked
|
651
|
+
end
|
652
|
+
|
653
|
+
def test_should_be_valid_if_validation_fails_outside_state_scope
|
654
|
+
record = @model.new(:state => 'parked', :seatbelt => nil)
|
655
|
+
assert record.valid?
|
656
|
+
end
|
657
|
+
|
658
|
+
def test_should_be_invalid_if_validation_fails_within_state_scope
|
659
|
+
record = @model.new(:state => 'first_gear', :seatbelt => nil)
|
660
|
+
assert !record.valid?
|
661
|
+
end
|
662
|
+
|
663
|
+
def test_should_be_valid_if_validation_succeeds_within_state_scope
|
664
|
+
record = @model.new(:state => 'first_gear', :seatbelt => true)
|
665
|
+
assert record.valid?
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
class MachineWithEventAttributesOnValidationTest < BaseTestCase
|
670
|
+
def setup
|
671
|
+
@model = new_model
|
672
|
+
@machine = StateMachine::Machine.new(@model)
|
673
|
+
@machine.event :ignite do
|
674
|
+
transition :parked => :idling
|
675
|
+
end
|
676
|
+
|
677
|
+
@record = @model.new
|
678
|
+
@record.state = 'parked'
|
679
|
+
@record.state_event = 'ignite'
|
680
|
+
end
|
681
|
+
|
682
|
+
def test_should_fail_if_event_is_invalid
|
683
|
+
@record.state_event = 'invalid'
|
684
|
+
assert !@record.valid?
|
685
|
+
assert_equal ['state_event is invalid'], @record.errors.full_messages
|
686
|
+
end
|
687
|
+
|
688
|
+
def test_should_fail_if_event_has_no_transition
|
689
|
+
@record.state = 'idling'
|
690
|
+
assert !@record.valid?
|
691
|
+
assert_equal ['state_event cannot transition when idling'], @record.errors.full_messages
|
692
|
+
end
|
693
|
+
|
694
|
+
def test_should_be_successful_if_event_has_transition
|
695
|
+
assert @record.valid?
|
696
|
+
end
|
697
|
+
|
698
|
+
def test_should_run_before_callbacks
|
699
|
+
ran_callback = false
|
700
|
+
@machine.before_transition { ran_callback = true }
|
701
|
+
|
702
|
+
@record.valid?
|
703
|
+
assert ran_callback
|
704
|
+
end
|
705
|
+
|
706
|
+
def test_should_persist_new_state
|
707
|
+
@record.valid?
|
708
|
+
assert_equal 'idling', @record.state
|
709
|
+
end
|
710
|
+
|
711
|
+
def test_should_not_run_after_callbacks
|
712
|
+
ran_callback = false
|
713
|
+
@machine.after_transition { ran_callback = true }
|
714
|
+
|
715
|
+
@record.valid?
|
716
|
+
assert !ran_callback
|
717
|
+
end
|
718
|
+
|
719
|
+
def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
|
720
|
+
@model.class_eval do
|
721
|
+
attr_accessor :seatbelt
|
722
|
+
validates_presence_of :seatbelt
|
723
|
+
end
|
724
|
+
|
725
|
+
ran_callback = false
|
726
|
+
@machine.after_transition { ran_callback = true }
|
727
|
+
|
728
|
+
@record.valid?
|
729
|
+
assert !ran_callback
|
730
|
+
end
|
731
|
+
|
732
|
+
def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
|
733
|
+
@model.class_eval do
|
734
|
+
attr_accessor :seatbelt
|
735
|
+
validates_presence_of :seatbelt
|
736
|
+
end
|
737
|
+
|
738
|
+
ran_callback = false
|
739
|
+
@machine.after_transition(:include_failures => true) { ran_callback = true }
|
740
|
+
|
741
|
+
@record.valid?
|
742
|
+
assert ran_callback
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
class MachineWithEventAttributesOnSaveTest < BaseTestCase
|
747
|
+
def setup
|
748
|
+
@model = new_model
|
749
|
+
@machine = StateMachine::Machine.new(@model)
|
750
|
+
@machine.event :ignite do
|
751
|
+
transition :parked => :idling
|
752
|
+
end
|
753
|
+
|
754
|
+
@record = @model.new
|
755
|
+
@record.state = 'parked'
|
756
|
+
@record.state_event = 'ignite'
|
757
|
+
end
|
758
|
+
|
759
|
+
def test_should_fail_if_event_is_invalid
|
760
|
+
@record.state_event = 'invalid'
|
761
|
+
assert !@record.save
|
762
|
+
end
|
763
|
+
|
764
|
+
def test_should_fail_if_event_has_no_transition
|
765
|
+
@record.state = 'idling'
|
766
|
+
assert !@record.save
|
767
|
+
end
|
768
|
+
|
769
|
+
def test_should_be_successful_if_event_has_transition
|
770
|
+
assert @record.save
|
771
|
+
end
|
772
|
+
|
773
|
+
def test_should_run_before_callbacks
|
774
|
+
ran_callback = false
|
775
|
+
@machine.before_transition { ran_callback = true }
|
776
|
+
|
777
|
+
@record.save
|
778
|
+
assert ran_callback
|
779
|
+
end
|
780
|
+
|
781
|
+
def test_should_run_before_callbacks_once
|
782
|
+
before_count = 0
|
783
|
+
@machine.before_transition { before_count += 1 }
|
784
|
+
|
785
|
+
@record.save
|
786
|
+
assert_equal 1, before_count
|
787
|
+
end
|
788
|
+
|
789
|
+
def test_should_persist_new_state
|
790
|
+
@record.save
|
791
|
+
assert_equal 'idling', @record.state
|
792
|
+
end
|
793
|
+
|
794
|
+
def test_should_run_after_callbacks
|
795
|
+
ran_callback = false
|
796
|
+
@machine.after_transition { ran_callback = true }
|
797
|
+
|
798
|
+
@record.save
|
799
|
+
assert ran_callback
|
800
|
+
end
|
801
|
+
|
802
|
+
def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
|
803
|
+
@model.before_create {|record| false}
|
804
|
+
|
805
|
+
ran_callback = false
|
806
|
+
@machine.after_transition { ran_callback = true }
|
807
|
+
|
808
|
+
@record.save
|
809
|
+
assert !ran_callback
|
810
|
+
end
|
811
|
+
|
812
|
+
def test_should_run_after_callbacks_with_failures_enabled_if_fails
|
813
|
+
@model.before_create {|record| false}
|
814
|
+
|
815
|
+
ran_callback = false
|
816
|
+
@machine.after_transition(:include_failures => true) { ran_callback = true }
|
817
|
+
|
818
|
+
@record.save
|
819
|
+
assert ran_callback
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
823
|
+
class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
|
824
|
+
def setup
|
825
|
+
@superclass = new_model do
|
826
|
+
def persist
|
827
|
+
save
|
828
|
+
end
|
829
|
+
end
|
830
|
+
@model = Class.new(@superclass)
|
831
|
+
@machine = StateMachine::Machine.new(@model, :action => :persist)
|
832
|
+
@machine.event :ignite do
|
833
|
+
transition :parked => :idling
|
834
|
+
end
|
835
|
+
|
836
|
+
@record = @model.new
|
837
|
+
@record.state = 'parked'
|
838
|
+
@record.state_event = 'ignite'
|
839
|
+
end
|
840
|
+
|
841
|
+
def test_should_not_transition_on_valid?
|
842
|
+
@record.valid?
|
843
|
+
assert_equal 'parked', @record.state
|
844
|
+
end
|
845
|
+
|
846
|
+
def test_should_not_transition_on_save
|
847
|
+
@record.save
|
848
|
+
assert_equal 'parked', @record.state
|
849
|
+
end
|
850
|
+
|
851
|
+
def test_should_transition_on_custom_action
|
852
|
+
@record.persist
|
853
|
+
assert_equal 'idling', @record.state
|
854
|
+
end
|
855
|
+
end
|
856
|
+
end
|
857
|
+
rescue LoadError
|
858
|
+
$stderr.puts "Skipping Sequel tests. `gem install sequel#{" -v #{ENV['SEQUEL_VERSION']}" if ENV['SEQUEL_VERSION']}` and try again."
|
859
|
+
end
|