validating-workflow 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
data/test/main_test.rb ADDED
@@ -0,0 +1,483 @@
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'
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 do
17
+ event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
18
+ end
19
+ end
20
+ state :accepted do
21
+ event :ship, :transitions_to => :shipped
22
+ end
23
+ state :shipped
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 => {:doc_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, :meta => {:importance => 8}
214
+ state :supplemental, :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)
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
+ end
272
+ c.new.my_transition!(args)
273
+ end
274
+
275
+ test 'Single table inheritance (STI)' do
276
+ class BigOrder < Order
277
+ end
278
+
279
+ bo = BigOrder.new
280
+ assert bo.submitted?
281
+ assert !bo.accepted?
282
+ end
283
+
284
+ test 'STI when parent changed the workflow_state column' do
285
+ assert_equal 'status', Image.workflow_column.to_s
286
+ assert_equal 'status', SmallImage.workflow_column.to_s
287
+ assert_equal 'status', SpecialSmallImage.workflow_column.to_s
288
+ end
289
+
290
+ test 'Two-level inheritance' do
291
+ class BigOrder < Order
292
+ end
293
+
294
+ class EvenBiggerOrder < BigOrder
295
+ end
296
+
297
+ assert EvenBiggerOrder.new.submitted?
298
+ end
299
+
300
+ test 'Iheritance with workflow definition override' do
301
+ class BigOrder < Order
302
+ end
303
+
304
+ class SpecialBigOrder < BigOrder
305
+ workflow do
306
+ state :start_big
307
+ end
308
+ end
309
+
310
+ special = SpecialBigOrder.new
311
+ assert_equal 'start_big', special.current_state.to_s
312
+ end
313
+
314
+ test 'Better error message for missing target state' do
315
+ class Problem
316
+ include Workflow
317
+ workflow do
318
+ state :initial do
319
+ event :solve, :transitions_to => :solved
320
+ end
321
+ end
322
+ end
323
+ assert_raise Workflow::WorkflowError do
324
+ Problem.new.solve!
325
+ end
326
+ end
327
+
328
+ # Intermixing of transition graph definition (states, transitions)
329
+ # on the one side and implementation of the actions on the other side
330
+ # for a bigger state machine can introduce clutter.
331
+ #
332
+ # To reduce this clutter it is now possible to use state entry- and
333
+ # exit- hooks defined through a naming convention. For example, if there
334
+ # is a state :pending, then you can hook in by defining method
335
+ # `def on_pending_exit(new_state, event, *args)` instead of using a
336
+ # block:
337
+ #
338
+ # state :pending do
339
+ # on_entry do
340
+ # # your implementation here
341
+ # end
342
+ # end
343
+ #
344
+ # If both a function with a name according to naming convention and the
345
+ # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
346
+ test 'on_entry and on_exit hooks in separate methods' do
347
+ c = Class.new
348
+ c.class_eval do
349
+ include Workflow
350
+ attr_reader :history
351
+ def initialize
352
+ @history = []
353
+ end
354
+ workflow do
355
+ state :new do
356
+ event :next, :transitions_to => :next_state
357
+ end
358
+ state :next_state
359
+ end
360
+
361
+ def on_next_state_entry(prior_state, event, *args)
362
+ @history << "on_next_state_entry #{event} #{prior_state} ->"
363
+ end
364
+
365
+ def on_new_exit(new_state, event, *args)
366
+ @history << "on_new_exit #{event} -> #{new_state}"
367
+ end
368
+ end
369
+
370
+ o = c.new
371
+ assert_equal 'new', o.current_state.to_s
372
+ assert_equal [], o.history
373
+ o.next!
374
+ assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
375
+
376
+ end
377
+
378
+ test 'diagram generation' do
379
+ begin
380
+ $stdout = StringIO.new('', 'w')
381
+ Workflow::create_workflow_diagram(Order, 'doc')
382
+ assert_match(/open.+\.pdf/, $stdout.string,
383
+ 'PDF should be generate and a hint be given to the user.')
384
+ ensure
385
+ $stdout = STDOUT
386
+ end
387
+ end
388
+
389
+ test 'halt stops the transition' do
390
+ c = Class.new do
391
+ include Workflow
392
+ workflow do
393
+ state :young do
394
+ event :age, :transitions_to => :old
395
+ end
396
+ state :old
397
+ end
398
+
399
+ def age(by=1)
400
+ halt 'too fast' if by > 100
401
+ end
402
+ end
403
+
404
+ joe = c.new
405
+ assert joe.young?
406
+ joe.age! 120
407
+ assert joe.young?, 'Transition should have been halted'
408
+ assert_equal 'too fast', joe.halted_because
409
+ end
410
+
411
+ test 'halt! raises exception immediately' do
412
+ article_class = Class.new do
413
+ include Workflow
414
+ attr_accessor :too_far
415
+ workflow do
416
+ state :new do
417
+ event :reject, :transitions_to => :rejected
418
+ end
419
+ state :rejected
420
+ end
421
+
422
+ def reject(reason)
423
+ halt! 'We do not reject articles unless the reason is important' \
424
+ unless reason =~ /important/i
425
+ self.too_far = "This line should not be executed"
426
+ end
427
+ end
428
+
429
+ article = article_class.new
430
+ assert article.new?
431
+ assert_raise Workflow::TransitionHalted do
432
+ article.reject! 'Too funny'
433
+ end
434
+ assert_nil article.too_far
435
+ assert article.new?, 'Transition should have been halted'
436
+ article.reject! 'Important: too short'
437
+ assert article.rejected?, 'Transition should happen now'
438
+ end
439
+
440
+ test 'can fire event?' do
441
+ c = Class.new do
442
+ include Workflow
443
+ workflow do
444
+ state :newborn do
445
+ event :go_to_school, :transitions_to => :schoolboy
446
+ end
447
+ state :schoolboy do
448
+ event :go_to_college, :transitions_to => :student
449
+ end
450
+ state :student
451
+ end
452
+ end
453
+
454
+ human = c.new
455
+ assert human.can_go_to_school?
456
+ assert_equal false, human.can_go_to_college?
457
+ end
458
+
459
+ test 'workflow graph generation' do
460
+ Dir.chdir('tmp') do
461
+ capture_streams do
462
+ Workflow::create_workflow_diagram(Order)
463
+ end
464
+ end
465
+ end
466
+
467
+ test 'workflow graph generation in path with spaces' do
468
+ `mkdir -p '/tmp/Workflow test'`
469
+ capture_streams do
470
+ Workflow::create_workflow_diagram(Order, '/tmp/Workflow test')
471
+ end
472
+ end
473
+
474
+ def capture_streams
475
+ old_stdout = $stdout
476
+ $stdout = captured_stdout = StringIO.new
477
+ yield
478
+ $stdout = old_stdout
479
+ captured_stdout
480
+ end
481
+
482
+ end
483
+
@@ -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,37 @@
1
+ require 'workflow'
2
+ class Article
3
+ include Workflow
4
+ workflow do
5
+ state :new do
6
+ event :submit, :transitions_to => :awaiting_review
7
+ end
8
+ state :awaiting_review do
9
+ event :review, :transitions_to => :being_reviewed
10
+ end
11
+ state :being_reviewed do
12
+ event :accept, :transitions_to => :accepted
13
+ event :reject, :transitions_to => :rejected
14
+ end
15
+ state :accepted
16
+ state :rejected
17
+ end
18
+ end
19
+
20
+ article = Article.new
21
+ article.accepted? # => false
22
+ article.new? # => true
23
+ article.submit!
24
+ article.review!
25
+
26
+ puts article.current_state # => being_reviewed
27
+
28
+
29
+ class Article
30
+ def reject
31
+ puts "send email to the author here explaining the reason for the rejection"
32
+ end
33
+ end
34
+
35
+ article.reject! # will cause a state transition, would persist the new
36
+ # state (if inherited from ActiveRecord), and invoke the callback -
37
+ # send email to the author.
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_record'
4
+
5
+ class << Test::Unit::TestCase
6
+ def test(name, &block)
7
+ test_name = :"test_#{name.gsub(' ','_')}"
8
+ raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
9
+ if block
10
+ define_method test_name, &block
11
+ else
12
+ puts "PENDING: #{name}"
13
+ end
14
+ end
15
+ end
16
+
17
+ class ActiveRecordTestCase < Test::Unit::TestCase
18
+ def exec(sql)
19
+ ActiveRecord::Base.connection.execute sql
20
+ end
21
+
22
+ def setup
23
+ ActiveRecord::Base.establish_connection(
24
+ :adapter => "sqlite3",
25
+ :database => ":memory:" #"tmp/test"
26
+ )
27
+
28
+ # eliminate ActiveRecord warning. TODO: delete as soon as ActiveRecord is fixed
29
+ ActiveRecord::Base.connection.reconnect!
30
+ end
31
+
32
+ def teardown
33
+ ActiveRecord::Base.connection.disconnect!
34
+ end
35
+
36
+ def default_test
37
+ end
38
+ end
39
+
@@ -0,0 +1,54 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'workflow'
3
+
4
+ class WithoutWorkflowTest < Test::Unit::TestCase
5
+ class Article
6
+ include Workflow
7
+ workflow do
8
+ state :new do
9
+ event :submit, :transitions_to => :awaiting_review
10
+ end
11
+ state :awaiting_review do
12
+ event :review, :transitions_to => :being_reviewed
13
+ end
14
+ state :being_reviewed do
15
+ event :accept, :transitions_to => :accepted
16
+ event :reject, :transitions_to => :rejected
17
+ end
18
+ state :accepted
19
+ state :rejected
20
+ end
21
+ end
22
+
23
+ def test_readme_example_article
24
+ article = Article.new
25
+ assert article.new?
26
+ end
27
+
28
+ test 'better error message on transitions_to typo' do
29
+ assert_raise Workflow::WorkflowDefinitionError do
30
+ Class.new do
31
+ include Workflow
32
+ workflow do
33
+ state :new do
34
+ event :event1, :transitionnn => :next # missing transitions_to target
35
+ end
36
+ state :next
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ test 'check transition_to alias' do
43
+ Class.new do
44
+ include Workflow
45
+ workflow do
46
+ state :new do
47
+ event :event1, :transition_to => :next
48
+ end
49
+ state :next
50
+ end
51
+ end
52
+ end
53
+ end
54
+
data/workflow.rb ADDED
@@ -0,0 +1 @@
1
+ Dir["#{File.dirname(__FILE__)}/lib/*.rb"].each { |rb| require rb }