workflow-rails4 1.1.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.
@@ -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
+