workflow-rails4 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'workflow'
3
+
4
+ class BeforeTransitionTest < Test::Unit::TestCase
5
+ class MyFlow
6
+ attr_reader :history
7
+ def initialize
8
+ @history = []
9
+ end
10
+
11
+ include Workflow
12
+ workflow do
13
+ state :first do
14
+ event :forward, :transitions_to => :second do
15
+ @history << 'forward'
16
+ end
17
+ end
18
+ state :second do
19
+ event :back, :transitions_to => :first do
20
+ @history << 'back'
21
+ end
22
+ end
23
+
24
+ before_transition { @history << 'before' }
25
+ after_transition { @history << 'after' }
26
+ on_transition { @history << 'on' }
27
+ end
28
+ end
29
+
30
+ test 'that before_transition is run before the action' do
31
+ flow = MyFlow.new
32
+ flow.forward!
33
+ flow.back!
34
+ assert flow.history == ['before', 'forward', 'on', 'after', 'before', 'back', 'on', 'after']
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'couchtiny'
3
+ require 'couchtiny/document'
4
+ require 'workflow'
5
+
6
+ class User < CouchTiny::Document
7
+ include Workflow
8
+ workflow do
9
+ state :submitted do
10
+ event :activate_via_link, :transitions_to => :proved_email
11
+ end
12
+ state :proved_email
13
+ end
14
+
15
+ def load_workflow_state
16
+ self[:workflow_state]
17
+ end
18
+
19
+ def persist_workflow_state(new_value)
20
+ self[:workflow_state] = new_value
21
+ save!
22
+ end
23
+ end
24
+
25
+
26
+ class CouchtinyExample < Test::Unit::TestCase
27
+
28
+ def setup
29
+ db = CouchTiny::Database.url("http://127.0.0.1:5984/test-workflow")
30
+ db.delete_database! rescue nil
31
+ db.create_database!
32
+ User.use_database db
33
+ end
34
+
35
+ test 'CouchDB persistence' do
36
+ user = User.new :email => 'manya@example.com'
37
+ user.save!
38
+ assert user.submitted?
39
+ user.activate_via_link!
40
+ assert user.proved_email?
41
+
42
+ reloaded_user = User.get user.id
43
+ puts reloaded_user.inspect
44
+ assert reloaded_user.proved_email?, 'Reloaded user should have the desired workflow state'
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'workflow'
3
+ class InheritanceTest < ActiveRecordTestCase
4
+
5
+ test '#69 inheritance' do
6
+ class Animal
7
+ include Workflow
8
+
9
+ workflow do
10
+
11
+ state :conceived do
12
+ event :birth, :transition_to => :born
13
+ end
14
+
15
+ state :born do
16
+
17
+ end
18
+ end
19
+ end
20
+
21
+ class Cat < Animal
22
+ include Workflow
23
+ workflow do
24
+
25
+ state :upset do
26
+ event :scratch, :transition_to => :hiding
27
+ end
28
+
29
+ state :hiding do
30
+
31
+ end
32
+ end
33
+ end
34
+
35
+ assert_equal [:born, :conceived] , sort_sym_array(Animal.workflow_spec.states.keys)
36
+ assert_equal [:hiding, :upset], sort_sym_array(Cat.workflow_spec.states.keys), "Workflow definitions are not inherited"
37
+
38
+ animal = Animal.new
39
+ cat = Cat.new
40
+
41
+ animal.birth!
42
+
43
+ assert_raise NoMethodError, 'Methods defined by the old workflow spec should have be gone away' do
44
+ cat.birth!
45
+ end
46
+
47
+ assert_equal [:birth!, :halt!, :process_event!], bang_methods(animal)
48
+ assert_equal [:halt!, :process_event!, :scratch!], bang_methods(cat)
49
+ end
50
+
51
+ def sort_sym_array(a)
52
+ a.sort { |a, b| a.to_s <=> b.to_s } # workaround for Ruby 1.8.7
53
+ end
54
+
55
+ def bang_methods(obj)
56
+ non_trivial_methods = obj.public_methods-Object.public_methods
57
+ methods_with_bang = non_trivial_methods.select {|m| m =~ /!$/}
58
+ sort_sym_array(methods_with_bang).map {|m| m.to_sym}
59
+ end
60
+ end
@@ -0,0 +1,544 @@
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 do
17
+ event :accept, :transitions_to => :accepted, :meta => {: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 => {: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
+
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
+
286
+ b = Class.new # the base class with a protected callback
287
+ b.class_eval do
288
+ protected
289
+ def assign_old(args)
290
+ args.log('in protected callback in the base class')
291
+ end
292
+ end
293
+
294
+ c = Class.new(b) # inheriting class with an additional protected callback
295
+ c.class_eval do
296
+ include Workflow
297
+ workflow do
298
+ state :new do
299
+ event :assign, :transitions_to => :assigned
300
+ event :assign_old, :transitions_to => :assigned_old
301
+ end
302
+ state :assigned
303
+ state :assigned_old
304
+ end
305
+
306
+ private
307
+ def assign(args)
308
+ args.log('in private callback')
309
+ end
310
+ end
311
+
312
+ a = c.new
313
+ a.assign!(args)
314
+
315
+ a2 = c.new
316
+ a2.assign_old!(args)
317
+ end
318
+
319
+ test '#58 Limited private transition callback lookup' do
320
+ args = mock()
321
+ c = Class.new
322
+ c.class_eval do
323
+ include Workflow
324
+ workflow do
325
+ state :new do
326
+ event :fail, :transitions_to => :failed
327
+ end
328
+ state :failed
329
+ end
330
+ end
331
+ a = c.new
332
+ a.fail!(args)
333
+ end
334
+
335
+ test 'Single table inheritance (STI)' do
336
+ class BigOrder < Order
337
+ end
338
+
339
+ bo = BigOrder.new
340
+ assert bo.submitted?
341
+ assert !bo.accepted?
342
+ end
343
+
344
+ test 'STI when parent changed the workflow_state column' do
345
+ assert_equal 'status', Image.workflow_column.to_s
346
+ assert_equal 'status', SmallImage.workflow_column.to_s
347
+ assert_equal 'status', SpecialSmallImage.workflow_column.to_s
348
+ end
349
+
350
+ test 'Two-level inheritance' do
351
+ class BigOrder < Order
352
+ end
353
+
354
+ class EvenBiggerOrder < BigOrder
355
+ end
356
+
357
+ assert EvenBiggerOrder.new.submitted?
358
+ end
359
+
360
+ test 'Iheritance with workflow definition override' do
361
+ class BigOrder < Order
362
+ end
363
+
364
+ class SpecialBigOrder < BigOrder
365
+ workflow do
366
+ state :start_big
367
+ end
368
+ end
369
+
370
+ special = SpecialBigOrder.new
371
+ assert_equal 'start_big', special.current_state.to_s
372
+ end
373
+
374
+ test 'Better error message for missing target state' do
375
+ class Problem
376
+ include Workflow
377
+ workflow do
378
+ state :initial do
379
+ event :solve, :transitions_to => :solved
380
+ end
381
+ end
382
+ end
383
+ assert_raise Workflow::WorkflowError do
384
+ Problem.new.solve!
385
+ end
386
+ end
387
+
388
+ # Intermixing of transition graph definition (states, transitions)
389
+ # on the one side and implementation of the actions on the other side
390
+ # for a bigger state machine can introduce clutter.
391
+ #
392
+ # To reduce this clutter it is now possible to use state entry- and
393
+ # exit- hooks defined through a naming convention. For example, if there
394
+ # is a state :pending, then you can hook in by defining method
395
+ # `def on_pending_exit(new_state, event, *args)` instead of using a
396
+ # block:
397
+ #
398
+ # state :pending do
399
+ # on_entry do
400
+ # # your implementation here
401
+ # end
402
+ # end
403
+ #
404
+ # If both a function with a name according to naming convention and the
405
+ # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
406
+ test 'on_entry and on_exit hooks in separate methods' do
407
+ c = Class.new
408
+ c.class_eval do
409
+ include Workflow
410
+ attr_reader :history
411
+ def initialize
412
+ @history = []
413
+ end
414
+ workflow do
415
+ state :new do
416
+ event :next, :transitions_to => :next_state
417
+ end
418
+ state :next_state
419
+ end
420
+
421
+ def on_next_state_entry(prior_state, event, *args)
422
+ @history << "on_next_state_entry #{event} #{prior_state} ->"
423
+ end
424
+
425
+ def on_new_exit(new_state, event, *args)
426
+ @history << "on_new_exit #{event} -> #{new_state}"
427
+ end
428
+ end
429
+
430
+ o = c.new
431
+ assert_equal 'new', o.current_state.to_s
432
+ assert_equal [], o.history
433
+ o.next!
434
+ assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
435
+
436
+ end
437
+
438
+ test 'diagram generation' do
439
+ begin
440
+ $stdout = StringIO.new('', 'w')
441
+ require 'workflow/draw'
442
+ Workflow::Draw::workflow_diagram(Order, :path => '/tmp')
443
+ assert_match(/run the following/, $stdout.string,
444
+ 'PDF should be generate and a hint be given to the user.')
445
+ ensure
446
+ $stdout = STDOUT
447
+ end
448
+ end
449
+
450
+ test 'halt stops the transition' do
451
+ c = Class.new do
452
+ include Workflow
453
+ workflow do
454
+ state :young do
455
+ event :age, :transitions_to => :old
456
+ end
457
+ state :old
458
+ end
459
+
460
+ def age(by=1)
461
+ halt 'too fast' if by > 100
462
+ end
463
+ end
464
+
465
+ joe = c.new
466
+ assert joe.young?
467
+ joe.age! 120
468
+ assert joe.young?, 'Transition should have been halted'
469
+ assert_equal 'too fast', joe.halted_because
470
+ end
471
+
472
+ test 'halt! raises exception immediately' do
473
+ article_class = Class.new do
474
+ include Workflow
475
+ attr_accessor :too_far
476
+ workflow do
477
+ state :new do
478
+ event :reject, :transitions_to => :rejected
479
+ end
480
+ state :rejected
481
+ end
482
+
483
+ def reject(reason)
484
+ halt! 'We do not reject articles unless the reason is important' \
485
+ unless reason =~ /important/i
486
+ self.too_far = "This line should not be executed"
487
+ end
488
+ end
489
+
490
+ article = article_class.new
491
+ assert article.new?
492
+ assert_raise Workflow::TransitionHalted do
493
+ article.reject! 'Too funny'
494
+ end
495
+ assert_nil article.too_far
496
+ assert article.new?, 'Transition should have been halted'
497
+ article.reject! 'Important: too short'
498
+ assert article.rejected?, 'Transition should happen now'
499
+ end
500
+
501
+ test 'can fire event?' do
502
+ c = Class.new do
503
+ include Workflow
504
+ workflow do
505
+ state :newborn do
506
+ event :go_to_school, :transitions_to => :schoolboy
507
+ end
508
+ state :schoolboy do
509
+ event :go_to_college, :transitions_to => :student
510
+ end
511
+ state :student
512
+ end
513
+ end
514
+
515
+ human = c.new
516
+ assert human.can_go_to_school?
517
+ assert_equal false, human.can_go_to_college?
518
+ end
519
+
520
+ test 'workflow graph generation' do
521
+ Dir.chdir('/tmp') do
522
+ capture_streams do
523
+ Workflow::Draw::workflow_diagram(Order, :path => '/tmp')
524
+ end
525
+ end
526
+ end
527
+
528
+ test 'workflow graph generation in a path with spaces' do
529
+ `mkdir -p '/tmp/Workflow test'`
530
+ capture_streams do
531
+ Workflow::Draw::workflow_diagram(Order, :path => '/tmp/Workflow test')
532
+ end
533
+ end
534
+
535
+ def capture_streams
536
+ old_stdout = $stdout
537
+ $stdout = captured_stdout = StringIO.new
538
+ yield
539
+ $stdout = old_stdout
540
+ captured_stdout
541
+ end
542
+
543
+ end
544
+