nimboids-workflow 0.8.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,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
+