workflow-orchestrator 1.3.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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.travis.yml +36 -0
- data/CHANGELOG.md +133 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +22 -0
- data/README.md +707 -0
- data/Rakefile +30 -0
- data/gemfiles/Gemfile.rails-3.x +12 -0
- data/gemfiles/Gemfile.rails-4.0 +14 -0
- data/gemfiles/Gemfile.rails-4.1 +14 -0
- data/gemfiles/Gemfile.rails-4.2 +14 -0
- data/gemfiles/Gemfile.rails-edge +14 -0
- data/lib/workflow/adapters/active_record.rb +75 -0
- data/lib/workflow/adapters/remodel.rb +15 -0
- data/lib/workflow/draw.rb +79 -0
- data/lib/workflow/errors.rb +20 -0
- data/lib/workflow/event.rb +38 -0
- data/lib/workflow/event_collection.rb +36 -0
- data/lib/workflow/specification.rb +83 -0
- data/lib/workflow/state.rb +44 -0
- data/lib/workflow/version.rb +3 -0
- data/lib/workflow.rb +307 -0
- data/orders_workflow.png +0 -0
- data/test/active_record_scopes_test.rb +56 -0
- data/test/active_record_scopes_with_values_test.rb +79 -0
- data/test/adapter_hook_test.rb +52 -0
- data/test/advanced_examples_test.rb +84 -0
- data/test/advanced_hooks_and_validation_test.rb +119 -0
- data/test/attr_protected_test.rb +107 -0
- data/test/before_transition_test.rb +36 -0
- data/test/couchtiny_example.rb +46 -0
- data/test/enum_values_in_memory_test.rb +23 -0
- data/test/enum_values_test.rb +30 -0
- data/test/incline_column_test.rb +54 -0
- data/test/inheritance_test.rb +56 -0
- data/test/main_test.rb +588 -0
- data/test/multiple_workflows_test.rb +84 -0
- data/test/new_versions/compare_states_test.rb +32 -0
- data/test/new_versions/persistence_test.rb +62 -0
- data/test/on_error_test.rb +52 -0
- data/test/on_unavailable_transition_test.rb +85 -0
- data/test/readme_example.rb +37 -0
- data/test/test_helper.rb +39 -0
- data/test/without_active_record_test.rb +54 -0
- data/workflow-orchestrator.gemspec +42 -0
- metadata +267 -0
data/test/main_test.rb
ADDED
@@ -0,0 +1,588 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
$VERBOSE = false
|
4
|
+
require 'active_record'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'workflow'
|
7
|
+
require 'mocha/setup'
|
8
|
+
require 'stringio'
|
9
|
+
#require 'ruby-debug'
|
10
|
+
|
11
|
+
ActiveRecord::Migration.verbose = false
|
12
|
+
|
13
|
+
class Order < ActiveRecord::Base
|
14
|
+
include Workflow
|
15
|
+
workflow do
|
16
|
+
state :submitted, :meta => {refundable: true} do
|
17
|
+
event :accept, :transitions_to => :accepted, :meta => {:weight => 8} do |reviewer, args|
|
18
|
+
end
|
19
|
+
end
|
20
|
+
state :accepted, :meta => {refundable: true} do
|
21
|
+
event :ship, :transitions_to => :shipped
|
22
|
+
end
|
23
|
+
state :shipped, :meta => {refundable: false}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class LegacyOrder < ActiveRecord::Base
|
28
|
+
include Workflow
|
29
|
+
|
30
|
+
workflow_column :foo_bar # use this legacy database column for persistence
|
31
|
+
|
32
|
+
workflow do
|
33
|
+
state :submitted do
|
34
|
+
event :accept, :transitions_to => :accepted, :meta => {:weight => 8} do |reviewer, args|
|
35
|
+
end
|
36
|
+
end
|
37
|
+
state :accepted do
|
38
|
+
event :ship, :transitions_to => :shipped
|
39
|
+
end
|
40
|
+
state :shipped
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Image < ActiveRecord::Base
|
45
|
+
include Workflow
|
46
|
+
|
47
|
+
workflow_column :status
|
48
|
+
|
49
|
+
workflow do
|
50
|
+
state :unconverted do
|
51
|
+
event :convert, :transitions_to => :converted
|
52
|
+
end
|
53
|
+
state :converted
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class SmallImage < Image
|
58
|
+
end
|
59
|
+
|
60
|
+
class SpecialSmallImage < SmallImage
|
61
|
+
end
|
62
|
+
|
63
|
+
class MainTest < ActiveRecordTestCase
|
64
|
+
|
65
|
+
def setup
|
66
|
+
super
|
67
|
+
|
68
|
+
ActiveRecord::Schema.define do
|
69
|
+
create_table :orders do |t|
|
70
|
+
t.string :title, :null => false
|
71
|
+
t.string :workflow_state
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
exec "INSERT INTO orders(title, workflow_state) VALUES('some order', 'accepted')"
|
76
|
+
|
77
|
+
ActiveRecord::Schema.define do
|
78
|
+
create_table :legacy_orders do |t|
|
79
|
+
t.string :title, :null => false
|
80
|
+
t.string :foo_bar
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
exec "INSERT INTO legacy_orders(title, foo_bar) VALUES('some order', 'accepted')"
|
85
|
+
|
86
|
+
ActiveRecord::Schema.define do
|
87
|
+
create_table :images do |t|
|
88
|
+
t.string :title, :null => false
|
89
|
+
t.string :state
|
90
|
+
t.string :type
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def assert_state(title, expected_state, klass = Order)
|
96
|
+
o = klass.find_by_title(title)
|
97
|
+
assert_equal expected_state, o.read_attribute(klass.workflow_column)
|
98
|
+
o
|
99
|
+
end
|
100
|
+
|
101
|
+
test 'immediately save the new workflow_state on state machine transition' do
|
102
|
+
o = assert_state 'some order', 'accepted'
|
103
|
+
assert o.ship!
|
104
|
+
assert_state 'some order', 'shipped'
|
105
|
+
end
|
106
|
+
|
107
|
+
test 'immediately save the new workflow_state on state machine transition with custom column name' do
|
108
|
+
o = assert_state 'some order', 'accepted', LegacyOrder
|
109
|
+
assert o.ship!
|
110
|
+
assert_state 'some order', 'shipped', LegacyOrder
|
111
|
+
end
|
112
|
+
|
113
|
+
test 'persist workflow_state in the db and reload' do
|
114
|
+
o = assert_state 'some order', 'accepted'
|
115
|
+
assert_equal :accepted, o.current_state.name
|
116
|
+
o.ship!
|
117
|
+
o.save!
|
118
|
+
|
119
|
+
assert_state 'some order', 'shipped'
|
120
|
+
|
121
|
+
o.reload
|
122
|
+
assert_equal 'shipped', o.read_attribute(:workflow_state)
|
123
|
+
end
|
124
|
+
|
125
|
+
test 'persist workflow_state in the db with_custom_name and reload' do
|
126
|
+
o = assert_state 'some order', 'accepted', LegacyOrder
|
127
|
+
assert_equal :accepted, o.current_state.name
|
128
|
+
o.ship!
|
129
|
+
o.save!
|
130
|
+
|
131
|
+
assert_state 'some order', 'shipped', LegacyOrder
|
132
|
+
|
133
|
+
o.reload
|
134
|
+
assert_equal 'shipped', o.read_attribute(:foo_bar)
|
135
|
+
end
|
136
|
+
|
137
|
+
test 'default workflow column should be workflow_state' do
|
138
|
+
o = assert_state 'some order', 'accepted'
|
139
|
+
assert_equal :workflow_state, o.class.workflow_column
|
140
|
+
end
|
141
|
+
|
142
|
+
test 'custom workflow column should be foo_bar' do
|
143
|
+
o = assert_state 'some order', 'accepted', LegacyOrder
|
144
|
+
assert_equal :foo_bar, o.class.workflow_column
|
145
|
+
end
|
146
|
+
|
147
|
+
test 'access workflow specification' do
|
148
|
+
assert_equal 3, Order.workflow_spec.states.length
|
149
|
+
assert_equal ['submitted', 'accepted', 'shipped'].sort,
|
150
|
+
Order.workflow_spec.state_names.map{|n| n.to_s}.sort
|
151
|
+
end
|
152
|
+
|
153
|
+
test 'current state object' do
|
154
|
+
o = assert_state 'some order', 'accepted'
|
155
|
+
assert_equal 'accepted', o.current_state.to_s
|
156
|
+
assert_equal 1, o.current_state.events.length
|
157
|
+
end
|
158
|
+
|
159
|
+
test 'on_entry and on_exit invoked' do
|
160
|
+
c = Class.new
|
161
|
+
callbacks = mock()
|
162
|
+
callbacks.expects(:my_on_exit_new).once
|
163
|
+
callbacks.expects(:my_on_entry_old).once
|
164
|
+
c.class_eval do
|
165
|
+
include Workflow
|
166
|
+
workflow do
|
167
|
+
state :new do
|
168
|
+
event :age, :transitions_to => :old
|
169
|
+
end
|
170
|
+
on_exit do
|
171
|
+
callbacks.my_on_exit_new
|
172
|
+
end
|
173
|
+
state :old
|
174
|
+
on_entry do
|
175
|
+
callbacks.my_on_entry_old
|
176
|
+
end
|
177
|
+
on_exit do
|
178
|
+
fail "wrong on_exit executed"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
o = c.new
|
184
|
+
assert_equal 'new', o.current_state.to_s
|
185
|
+
o.age!
|
186
|
+
end
|
187
|
+
|
188
|
+
test 'on_transition invoked' do
|
189
|
+
callbacks = mock()
|
190
|
+
callbacks.expects(:on_tran).once # this is validated at the end
|
191
|
+
c = Class.new
|
192
|
+
c.class_eval do
|
193
|
+
include Workflow
|
194
|
+
workflow do
|
195
|
+
state :one do
|
196
|
+
event :increment, :transitions_to => :two
|
197
|
+
end
|
198
|
+
state :two
|
199
|
+
on_transition do |from, to, triggering_event, *event_args|
|
200
|
+
callbacks.on_tran
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
assert_not_nil c.workflow_spec.on_transition_proc
|
205
|
+
c.new.increment!
|
206
|
+
end
|
207
|
+
|
208
|
+
test 'access event meta information' do
|
209
|
+
c = Class.new
|
210
|
+
c.class_eval do
|
211
|
+
include Workflow
|
212
|
+
workflow do
|
213
|
+
state :main, 1, :meta => {:importance => 8}
|
214
|
+
state :supplemental, 2, :meta => {:importance => 1}
|
215
|
+
end
|
216
|
+
end
|
217
|
+
assert_equal 1, c.workflow_spec.states[:supplemental].meta[:importance]
|
218
|
+
end
|
219
|
+
|
220
|
+
test 'initial state' do
|
221
|
+
c = Class.new
|
222
|
+
c.class_eval do
|
223
|
+
include Workflow
|
224
|
+
workflow { state :one; state :two }
|
225
|
+
end
|
226
|
+
assert_equal 'one', c.new.current_state.to_s
|
227
|
+
end
|
228
|
+
|
229
|
+
test 'nil as initial state' do
|
230
|
+
exec "INSERT INTO orders(title, workflow_state) VALUES('nil state', NULL)"
|
231
|
+
o = Order.find_by_title('nil state')
|
232
|
+
assert o.submitted?, 'if workflow_state is nil, the initial state should be assumed'
|
233
|
+
assert !o.shipped?
|
234
|
+
end
|
235
|
+
|
236
|
+
test 'initial state immediately set as ActiveRecord attribute for new objects' do
|
237
|
+
o = Order.create(:title => 'new object')
|
238
|
+
assert_equal 'submitted', o.read_attribute(:workflow_state).to_s
|
239
|
+
end
|
240
|
+
|
241
|
+
test 'question methods for state' do
|
242
|
+
o = assert_state 'some order', 'accepted'
|
243
|
+
assert o.accepted?
|
244
|
+
assert !o.shipped?
|
245
|
+
end
|
246
|
+
|
247
|
+
test 'correct exception for event, that is not allowed in current state' do
|
248
|
+
o = assert_state 'some order', 'accepted'
|
249
|
+
assert_raise Workflow::NoTransitionAllowed do
|
250
|
+
o.accept!
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
test 'multiple events with the same name and different arguments lists from different states'
|
255
|
+
|
256
|
+
test 'implicit transition callback' do
|
257
|
+
args = mock()
|
258
|
+
args.expects(:my_tran).once # this is validated at the end
|
259
|
+
c = Class.new
|
260
|
+
c.class_eval do
|
261
|
+
include Workflow
|
262
|
+
def my_transition(args)
|
263
|
+
args.my_tran
|
264
|
+
end
|
265
|
+
workflow do
|
266
|
+
state :one do
|
267
|
+
event :my_transition, :transitions_to => :two
|
268
|
+
end
|
269
|
+
state :two
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
def another_transition(args)
|
274
|
+
args.another_tran
|
275
|
+
end
|
276
|
+
end
|
277
|
+
a = c.new
|
278
|
+
a.my_transition!(args)
|
279
|
+
end
|
280
|
+
|
281
|
+
test '#53 Support for non public transition callbacks' do
|
282
|
+
args = mock()
|
283
|
+
args.expects(:log).with('in private callback').once
|
284
|
+
args.expects(:log).with('in protected callback in the base class').once
|
285
|
+
args.expects(:log).with('in protected callback `on_assigned_entry`').once
|
286
|
+
|
287
|
+
b = Class.new # the base class with a protected callback
|
288
|
+
b.class_eval do
|
289
|
+
protected
|
290
|
+
def assign_old(args)
|
291
|
+
args.log('in protected callback in the base class')
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
c = Class.new(b) # inheriting class with an additional protected callback
|
297
|
+
c.class_eval do
|
298
|
+
include Workflow
|
299
|
+
workflow do
|
300
|
+
state :new do
|
301
|
+
event :assign, :transitions_to => :assigned
|
302
|
+
event :assign_old, :transitions_to => :assigned_old
|
303
|
+
end
|
304
|
+
state :assigned
|
305
|
+
state :assigned_old
|
306
|
+
end
|
307
|
+
|
308
|
+
protected
|
309
|
+
def on_assigned_entry(prev_state, event, args)
|
310
|
+
args.log('in protected callback `on_assigned_entry`')
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
def assign(args)
|
315
|
+
args.log('in private callback')
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
a = c.new
|
320
|
+
a.assign!(args)
|
321
|
+
|
322
|
+
a2 = c.new
|
323
|
+
a2.assign_old!(args)
|
324
|
+
end
|
325
|
+
|
326
|
+
test '#58 Limited private transition callback lookup' do
|
327
|
+
args = mock()
|
328
|
+
c = Class.new
|
329
|
+
c.class_eval do
|
330
|
+
include Workflow
|
331
|
+
workflow do
|
332
|
+
state :new do
|
333
|
+
event :fail, :transitions_to => :failed
|
334
|
+
end
|
335
|
+
state :failed
|
336
|
+
end
|
337
|
+
end
|
338
|
+
a = c.new
|
339
|
+
a.fail!(args)
|
340
|
+
end
|
341
|
+
|
342
|
+
test 'Single table inheritance (STI)' do
|
343
|
+
class BigOrder < Order
|
344
|
+
end
|
345
|
+
|
346
|
+
bo = BigOrder.new
|
347
|
+
assert bo.submitted?
|
348
|
+
assert !bo.accepted?
|
349
|
+
end
|
350
|
+
|
351
|
+
test 'STI when parent changed the workflow_state column' do
|
352
|
+
assert_equal 'status', Image.workflow_column.to_s
|
353
|
+
assert_equal 'status', SmallImage.workflow_column.to_s
|
354
|
+
assert_equal 'status', SpecialSmallImage.workflow_column.to_s
|
355
|
+
end
|
356
|
+
|
357
|
+
test 'Two-level inheritance' do
|
358
|
+
class BigOrder < Order
|
359
|
+
end
|
360
|
+
|
361
|
+
class EvenBiggerOrder < BigOrder
|
362
|
+
end
|
363
|
+
|
364
|
+
assert EvenBiggerOrder.new.submitted?
|
365
|
+
end
|
366
|
+
|
367
|
+
test 'Iheritance with workflow definition override' do
|
368
|
+
class BigOrder < Order
|
369
|
+
end
|
370
|
+
|
371
|
+
class SpecialBigOrder < BigOrder
|
372
|
+
workflow do
|
373
|
+
state :start_big
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
special = SpecialBigOrder.new
|
378
|
+
assert_equal 'start_big', special.current_state.to_s
|
379
|
+
end
|
380
|
+
|
381
|
+
test 'Better error message for missing target state' do
|
382
|
+
class Problem
|
383
|
+
include Workflow
|
384
|
+
workflow do
|
385
|
+
state :initial do
|
386
|
+
event :solve, :transitions_to => :solved
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
assert_raise Workflow::WorkflowError do
|
391
|
+
Problem.new.solve!
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Intermixing of transition graph definition (states, transitions)
|
396
|
+
# on the one side and implementation of the actions on the other side
|
397
|
+
# for a bigger state machine can introduce clutter.
|
398
|
+
#
|
399
|
+
# To reduce this clutter it is now possible to use state entry- and
|
400
|
+
# exit- hooks defined through a naming convention. For example, if there
|
401
|
+
# is a state :pending, then you can hook in by defining method
|
402
|
+
# `def on_pending_exit(new_state, event, *args)` instead of using a
|
403
|
+
# block:
|
404
|
+
#
|
405
|
+
# state :pending do
|
406
|
+
# on_entry do
|
407
|
+
# # your implementation here
|
408
|
+
# end
|
409
|
+
# end
|
410
|
+
#
|
411
|
+
# If both a function with a name according to naming convention and the
|
412
|
+
# on_entry/on_exit block are given, then only on_entry/on_exit block is used.
|
413
|
+
test 'on_entry and on_exit hooks in separate methods' do
|
414
|
+
c = Class.new
|
415
|
+
c.class_eval do
|
416
|
+
include Workflow
|
417
|
+
attr_reader :history
|
418
|
+
def initialize
|
419
|
+
@history = []
|
420
|
+
end
|
421
|
+
workflow do
|
422
|
+
state :new do
|
423
|
+
event :next, :transitions_to => :next_state
|
424
|
+
end
|
425
|
+
state :next_state
|
426
|
+
end
|
427
|
+
|
428
|
+
def on_next_state_entry(prior_state, event, *args)
|
429
|
+
@history << "on_next_state_entry #{event} #{prior_state} ->"
|
430
|
+
end
|
431
|
+
|
432
|
+
def on_new_exit(new_state, event, *args)
|
433
|
+
@history << "on_new_exit #{event} -> #{new_state}"
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
o = c.new
|
438
|
+
assert_equal 'new', o.current_state.to_s
|
439
|
+
assert_equal [], o.history
|
440
|
+
o.next!
|
441
|
+
assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
|
442
|
+
|
443
|
+
end
|
444
|
+
|
445
|
+
test 'diagram generation' do
|
446
|
+
begin
|
447
|
+
$stdout = StringIO.new('', 'w')
|
448
|
+
require 'workflow/draw'
|
449
|
+
Workflow::Draw::workflow_diagram(Order, :path => '/tmp')
|
450
|
+
assert_match(/run the following/, $stdout.string,
|
451
|
+
'PDF should be generate and a hint be given to the user.')
|
452
|
+
ensure
|
453
|
+
$stdout = STDOUT
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
test 'halt stops the transition' do
|
458
|
+
c = Class.new do
|
459
|
+
include Workflow
|
460
|
+
workflow do
|
461
|
+
state :young do
|
462
|
+
event :age, :transitions_to => :old
|
463
|
+
end
|
464
|
+
state :old
|
465
|
+
end
|
466
|
+
|
467
|
+
def age(by=1)
|
468
|
+
halt 'too fast' if by > 100
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
joe = c.new
|
473
|
+
assert joe.young?
|
474
|
+
joe.age! 120
|
475
|
+
assert joe.young?, 'Transition should have been halted'
|
476
|
+
assert_equal 'too fast', joe.halted_because
|
477
|
+
end
|
478
|
+
|
479
|
+
test 'halt! raises exception immediately' do
|
480
|
+
article_class = Class.new do
|
481
|
+
include Workflow
|
482
|
+
attr_accessor :too_far
|
483
|
+
workflow do
|
484
|
+
state :new do
|
485
|
+
event :reject, :transitions_to => :rejected
|
486
|
+
end
|
487
|
+
state :rejected
|
488
|
+
end
|
489
|
+
|
490
|
+
def reject(reason)
|
491
|
+
halt! 'We do not reject articles unless the reason is important' \
|
492
|
+
unless reason =~ /important/i
|
493
|
+
self.too_far = "This line should not be executed"
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
article = article_class.new
|
498
|
+
assert article.new?
|
499
|
+
assert_raise Workflow::TransitionHalted do
|
500
|
+
article.reject! 'Too funny'
|
501
|
+
end
|
502
|
+
assert_nil article.too_far
|
503
|
+
assert article.new?, 'Transition should have been halted'
|
504
|
+
article.reject! 'Important: too short'
|
505
|
+
assert article.rejected?, 'Transition should happen now'
|
506
|
+
end
|
507
|
+
|
508
|
+
test 'can fire event?' do
|
509
|
+
c = Class.new do
|
510
|
+
include Workflow
|
511
|
+
workflow do
|
512
|
+
state :newborn do
|
513
|
+
event :go_to_school, :transitions_to => :schoolboy
|
514
|
+
end
|
515
|
+
state :schoolboy do
|
516
|
+
event :go_to_college, :transitions_to => :student
|
517
|
+
end
|
518
|
+
state :student
|
519
|
+
state :street
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
human = c.new
|
524
|
+
assert human.can_go_to_school?
|
525
|
+
assert_equal false, human.can_go_to_college?
|
526
|
+
end
|
527
|
+
|
528
|
+
test 'can_<fire_event>? with conditions' do
|
529
|
+
c = Class.new do
|
530
|
+
include Workflow
|
531
|
+
workflow do
|
532
|
+
state :off do
|
533
|
+
event :turn_on, :transitions_to => :on, :if => :sufficient_battery_level?
|
534
|
+
event :turn_on, :transitions_to => :low_battery, :if => proc { |obj| obj.battery > 0 }
|
535
|
+
end
|
536
|
+
state :on
|
537
|
+
state :low_battery
|
538
|
+
end
|
539
|
+
attr_reader :battery
|
540
|
+
def initialize(battery)
|
541
|
+
@battery = battery
|
542
|
+
end
|
543
|
+
|
544
|
+
def sufficient_battery_level?
|
545
|
+
@battery > 10
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
device = c.new 0
|
550
|
+
assert_equal false, device.can_turn_on?
|
551
|
+
|
552
|
+
device = c.new 5
|
553
|
+
assert device.can_turn_on?
|
554
|
+
device.turn_on!
|
555
|
+
assert device.low_battery?
|
556
|
+
assert_equal false, device.on?
|
557
|
+
|
558
|
+
device = c.new 50
|
559
|
+
assert device.can_turn_on?
|
560
|
+
device.turn_on!
|
561
|
+
assert device.on?
|
562
|
+
end
|
563
|
+
|
564
|
+
test 'workflow graph generation' do
|
565
|
+
Dir.chdir('/tmp') do
|
566
|
+
capture_streams do
|
567
|
+
Workflow::Draw::workflow_diagram(Order, :path => '/tmp')
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
test 'workflow graph generation in a path with spaces' do
|
573
|
+
`mkdir -p '/tmp/Workflow test'`
|
574
|
+
capture_streams do
|
575
|
+
Workflow::Draw::workflow_diagram(Order, :path => '/tmp/Workflow test')
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def capture_streams
|
580
|
+
old_stdout = $stdout
|
581
|
+
$stdout = captured_stdout = StringIO.new
|
582
|
+
yield
|
583
|
+
$stdout = old_stdout
|
584
|
+
captured_stdout
|
585
|
+
end
|
586
|
+
|
587
|
+
end
|
588
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'workflow'
|
3
|
+
class MultipleWorkflowsTest < ActiveRecordTestCase
|
4
|
+
|
5
|
+
test 'multiple workflows' do
|
6
|
+
|
7
|
+
ActiveRecord::Schema.define do
|
8
|
+
create_table :bookings do |t|
|
9
|
+
t.string :title, :null => false
|
10
|
+
t.string :workflow_state
|
11
|
+
t.string :workflow_type
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking1', 'initial', 'workflow_1')"
|
16
|
+
exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking2', 'initial', 'workflow_2')"
|
17
|
+
|
18
|
+
class Booking < ActiveRecord::Base
|
19
|
+
|
20
|
+
include Workflow
|
21
|
+
|
22
|
+
def initialize_workflow
|
23
|
+
# define workflow per object instead of per class
|
24
|
+
case workflow_type
|
25
|
+
when 'workflow_1'
|
26
|
+
class << self
|
27
|
+
workflow do
|
28
|
+
state :initial do
|
29
|
+
event :progress, :transitions_to => :last
|
30
|
+
end
|
31
|
+
state :last
|
32
|
+
end
|
33
|
+
end
|
34
|
+
when 'workflow_2'
|
35
|
+
class << self
|
36
|
+
workflow do
|
37
|
+
state :initial do
|
38
|
+
event :progress, :transitions_to => :intermediate
|
39
|
+
end
|
40
|
+
state :intermediate
|
41
|
+
state :last
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def metaclass; class << self; self; end; end
|
48
|
+
|
49
|
+
def workflow_spec
|
50
|
+
metaclass.workflow_spec
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
booking1 = Booking.find_by_title('booking1')
|
56
|
+
booking1.initialize_workflow
|
57
|
+
|
58
|
+
booking2 = Booking.find_by_title('booking2')
|
59
|
+
booking2.initialize_workflow
|
60
|
+
|
61
|
+
assert booking1.initial?
|
62
|
+
booking1.progress!
|
63
|
+
assert booking1.last?, 'booking1 should transition to the "last" state'
|
64
|
+
|
65
|
+
assert booking2.initial?
|
66
|
+
booking2.progress!
|
67
|
+
assert booking2.intermediate?, 'booking2 should transition to the "intermediate" state'
|
68
|
+
|
69
|
+
assert booking1.workflow_spec, 'can access the individual workflow specification'
|
70
|
+
assert_equal 2, booking1.workflow_spec.states.length
|
71
|
+
assert_equal 3, booking2.workflow_spec.states.length
|
72
|
+
|
73
|
+
# check persistence
|
74
|
+
booking2reloaded = Booking.find_by_title('booking2')
|
75
|
+
booking2reloaded.initialize_workflow
|
76
|
+
assert booking2reloaded.intermediate?, 'persistence of workflow state does not work'
|
77
|
+
end
|
78
|
+
|
79
|
+
class Object
|
80
|
+
# The hidden singleton lurks behind everyone
|
81
|
+
def metaclass; class << self; self; end; end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'workflow'
|
3
|
+
|
4
|
+
class ComparableStatesOrder
|
5
|
+
include Workflow
|
6
|
+
workflow do
|
7
|
+
state :submitted do
|
8
|
+
event :accept, :transitions_to => :accepted, :meta => {:weight => 8} do |reviewer, args|
|
9
|
+
end
|
10
|
+
end
|
11
|
+
state :accepted do
|
12
|
+
event :ship, :transitions_to => :shipped
|
13
|
+
end
|
14
|
+
state :shipped
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class CompareStatesTest < Test::Unit::TestCase
|
19
|
+
|
20
|
+
test 'compare states' do
|
21
|
+
o = ComparableStatesOrder.new
|
22
|
+
o.accept!
|
23
|
+
assert_equal :accepted, o.current_state.name
|
24
|
+
assert o.current_state == :accepted
|
25
|
+
assert o.current_state < :shipped
|
26
|
+
assert o.current_state > :submitted
|
27
|
+
assert_raise ArgumentError do
|
28
|
+
o.current_state > :unknown
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|