nimboids-workflow 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,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
+