apollo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module Apollo
2
+ class State
3
+ attr_accessor :name, :events, :meta, :on_entry, :on_exit
4
+
5
+ def initialize(name, meta = {})
6
+ @name, @events, @meta = name, Hash.new, meta
7
+ end
8
+
9
+ def to_s
10
+ "#{name}"
11
+ end
12
+
13
+ def to_sym
14
+ name.to_sym
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'couchtiny'
3
+ require 'couchtiny/document'
4
+ require 'apollo'
5
+
6
+ class User < CouchTiny::Document
7
+ include Apollo
8
+ apollo do
9
+ state :submitted do
10
+ event :activate_via_link, :to => :proved_email
11
+ end
12
+ state :proved_email
13
+ end
14
+
15
+ def load_apollo_state
16
+ self[:apollo_state]
17
+ end
18
+
19
+ def persist_apollo_state(new_value)
20
+ self[:apollo_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-apollo")
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 apollo state'
45
+ end
46
+ end
@@ -0,0 +1,418 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ $VERBOSE = false
4
+ require 'active_record'
5
+ require 'sqlite3'
6
+ require 'apollo'
7
+ require 'mocha'
8
+ #require 'ruby-debug'
9
+
10
+ ActiveRecord::Migration.verbose = false
11
+
12
+ class Order < ActiveRecord::Base
13
+ include Apollo
14
+ apollo do
15
+ state :submitted do
16
+ event :accept, :to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
17
+ end
18
+ end
19
+ state :accepted do
20
+ event :ship, :to => :shipped
21
+ end
22
+ state :shipped
23
+ end
24
+
25
+ end
26
+
27
+ class LegacyOrder < ActiveRecord::Base
28
+ include Apollo
29
+
30
+ apollo_column :foo_bar # use this legacy database column for persistence
31
+
32
+ apollo do
33
+ state :submitted do
34
+ event :accept, :to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
35
+ end
36
+ end
37
+ state :accepted do
38
+ event :ship, :to => :shipped
39
+ end
40
+ state :shipped
41
+ end
42
+
43
+ end
44
+
45
+
46
+ class MainTest < Test::Unit::TestCase
47
+
48
+ def exec(sql)
49
+ ActiveRecord::Base.connection.execute sql
50
+ end
51
+
52
+ def setup
53
+ ActiveRecord::Base.establish_connection(
54
+ :adapter => "sqlite3",
55
+ :database => ":memory:" #"tmp/test"
56
+ )
57
+ ActiveRecord::Base.connection.reconnect! # eliminate ActiveRecord warning. TODO: delete as soon as ActiveRecord is fixed
58
+
59
+ ActiveRecord::Schema.define do
60
+ create_table :orders do |t|
61
+ t.string :title, :null => false
62
+ t.string :apollo_state
63
+ end
64
+ end
65
+
66
+ exec "INSERT INTO orders(title, apollo_state) VALUES('some order', 'accepted')"
67
+
68
+ ActiveRecord::Schema.define do
69
+ create_table :legacy_orders do |t|
70
+ t.string :title, :null => false
71
+ t.string :foo_bar
72
+ end
73
+ end
74
+
75
+ exec "INSERT INTO legacy_orders(title, foo_bar) VALUES('some order', 'accepted')"
76
+
77
+ end
78
+
79
+ def teardown
80
+ ActiveRecord::Base.connection.disconnect!
81
+ end
82
+
83
+ def assert_state(title, expected_state, klass = Order)
84
+ o = klass.find_by_title(title)
85
+ assert_equal expected_state, o.read_attribute(klass.apollo_column)
86
+ o
87
+ end
88
+
89
+ test 'immediately save the new apollo_state on state machine transition' do
90
+ o = assert_state 'some order', 'accepted'
91
+ o.ship!
92
+ assert_state 'some order', 'shipped'
93
+ end
94
+
95
+ test 'immediately save the new apollo_state on state machine transition with custom column name' do
96
+ o = assert_state 'some order', 'accepted', LegacyOrder
97
+ o.ship!
98
+ assert_state 'some order', 'shipped', LegacyOrder
99
+ end
100
+
101
+ test 'persist apollo_state in the db and reload' do
102
+ o = assert_state 'some order', 'accepted'
103
+ assert_equal :accepted, o.current_state.name
104
+ o.ship!
105
+ o.save!
106
+
107
+ assert_state 'some order', 'shipped'
108
+
109
+ o.reload
110
+ assert_equal 'shipped', o.read_attribute(:apollo_state)
111
+ end
112
+
113
+ test 'persist apollo_state in the db with_custom_name and reload' do
114
+ o = assert_state 'some order', 'accepted', LegacyOrder
115
+ assert_equal :accepted, o.current_state.name
116
+ o.ship!
117
+ o.save!
118
+
119
+ assert_state 'some order', 'shipped', LegacyOrder
120
+
121
+ o.reload
122
+ assert_equal 'shipped', o.read_attribute(:foo_bar)
123
+ end
124
+
125
+ test 'default apollo column should be apollo_state' do
126
+ o = assert_state 'some order', 'accepted'
127
+ assert_equal :apollo_state, o.class.apollo_column
128
+ end
129
+
130
+ test 'custom apollo column should be foo_bar' do
131
+ o = assert_state 'some order', 'accepted', LegacyOrder
132
+ assert_equal :foo_bar, o.class.apollo_column
133
+ end
134
+
135
+ test 'access apollo specification' do
136
+ assert_equal 3, Order.apollo_spec.states.length
137
+ end
138
+
139
+ test 'current state object' do
140
+ o = assert_state 'some order', 'accepted'
141
+ assert_equal 'accepted', o.current_state.to_s
142
+ assert_equal 1, o.current_state.events.length
143
+ end
144
+
145
+ test 'on_entry and on_exit invoked' do
146
+ c = Class.new
147
+ callbacks = mock()
148
+ callbacks.expects(:my_on_exit_new).once
149
+ callbacks.expects(:my_on_entry_old).once
150
+ c.class_eval do
151
+ include Apollo
152
+ apollo do
153
+ state :new do
154
+ event :age, :to => :old
155
+ end
156
+ on_exit do
157
+ callbacks.my_on_exit_new
158
+ end
159
+ state :old
160
+ on_entry do
161
+ callbacks.my_on_entry_old
162
+ end
163
+ on_exit do
164
+ fail "wrong on_exit executed"
165
+ end
166
+ end
167
+ end
168
+
169
+ o = c.new
170
+ assert_equal 'new', o.current_state.to_s
171
+ o.age!
172
+ end
173
+
174
+ test 'on_transition invoked' do
175
+ callbacks = mock()
176
+ callbacks.expects(:on_tran).once # this is validated at the end
177
+ c = Class.new
178
+ c.class_eval do
179
+ include Apollo
180
+ apollo do
181
+ state :one do
182
+ event :increment, :to => :two
183
+ end
184
+ state :two
185
+ on_transition do |from, to, triggering_event, *event_args|
186
+ callbacks.on_tran
187
+ end
188
+ end
189
+ end
190
+ assert_not_nil c.apollo_spec.on_transition_proc
191
+ c.new.increment!
192
+ end
193
+
194
+ test 'access event meta information' do
195
+ c = Class.new
196
+ c.class_eval do
197
+ include Apollo
198
+ apollo do
199
+ state :main, :meta => {:importance => 8}
200
+ state :supplemental, :meta => {:importance => 1}
201
+ end
202
+ end
203
+ assert_equal 1, c.apollo_spec.states[:supplemental].meta[:importance]
204
+ end
205
+
206
+ test 'initial state' do
207
+ c = Class.new
208
+ c.class_eval do
209
+ include Apollo
210
+ apollo { state :one; state :two }
211
+ end
212
+ assert_equal 'one', c.new.current_state.to_s
213
+ end
214
+
215
+ test 'nil as initial state' do
216
+ exec "INSERT INTO orders(title, apollo_state) VALUES('nil state', NULL)"
217
+ o = Order.find_by_title('nil state')
218
+ assert o.submitted?, 'if apollo_state is nil, the initial state should be assumed'
219
+ assert !o.shipped?
220
+ end
221
+
222
+ test 'initial state immediately set as ActiveRecord attribute for new objects' do
223
+ o = Order.create(:title => 'new object')
224
+ assert_equal 'submitted', o.read_attribute(:apollo_state)
225
+ end
226
+
227
+ test 'question methods for state' do
228
+ o = assert_state 'some order', 'accepted'
229
+ assert o.accepted?
230
+ assert !o.shipped?
231
+ end
232
+
233
+ test 'correct exception for event, that is not allowed in current state' do
234
+ o = assert_state 'some order', 'accepted'
235
+ assert_raise Apollo::NoTransitionAllowed do
236
+ o.accept!
237
+ end
238
+ end
239
+
240
+ test 'multiple events with the same name and different arguments lists from different states'
241
+
242
+ test 'implicit transition callback' do
243
+ args = mock()
244
+ args.expects(:my_tran).once # this is validated at the end
245
+ c = Class.new
246
+ c.class_eval do
247
+ include Apollo
248
+ def my_transition(args)
249
+ args.my_tran
250
+ end
251
+ apollo do
252
+ state :one do
253
+ event :my_transition, :to => :two
254
+ end
255
+ state :two
256
+ end
257
+ end
258
+ c.new.my_transition!(args)
259
+ end
260
+
261
+ test 'Single table inheritance (STI)' do
262
+ class BigOrder < Order
263
+ end
264
+
265
+ bo = BigOrder.new
266
+ assert bo.submitted?
267
+ assert !bo.accepted?
268
+ end
269
+
270
+ test 'Two-level inheritance' do
271
+ class BigOrder < Order
272
+ end
273
+
274
+ class EvenBiggerOrder < BigOrder
275
+ end
276
+
277
+ assert EvenBiggerOrder.new.submitted?
278
+ end
279
+
280
+ test 'Iheritance with apollo definition override' do
281
+ class BigOrder < Order
282
+ end
283
+
284
+ class SpecialBigOrder < BigOrder
285
+ apollo do
286
+ state :start_big
287
+ end
288
+ end
289
+
290
+ special = SpecialBigOrder.new
291
+ assert_equal 'start_big', special.current_state.to_s
292
+ end
293
+
294
+ test 'Better error message for missing target state' do
295
+ class Problem
296
+ include Apollo
297
+ apollo do
298
+ state :initial do
299
+ event :solve, :to => :solved
300
+ end
301
+ end
302
+ end
303
+ assert_raise Apollo::ApolloError do
304
+ Problem.new.solve!
305
+ end
306
+ end
307
+
308
+ # Intermixing of transition graph definition (states, transitions)
309
+ # on the one side and implementation of the actions on the other side
310
+ # for a bigger state machine can introduce clutter.
311
+ #
312
+ # To reduce this clutter it is now possible to use state entry- and
313
+ # exit- hooks defined through a naming convention. For example, if there
314
+ # is a state :pending, then you can hook in by defining method
315
+ # `def on_pending_exit(new_state, event, *args)` instead of using a
316
+ # block:
317
+ #
318
+ # state :pending do
319
+ # on_entry do
320
+ # # your implementation here
321
+ # end
322
+ # end
323
+ #
324
+ # If both a function with a name according to naming convention and the
325
+ # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
326
+ test 'on_entry and on_exit hooks in separate methods' do
327
+ c = Class.new
328
+ c.class_eval do
329
+ include Apollo
330
+ attr_reader :history
331
+ def initialize
332
+ @history = []
333
+ end
334
+ apollo do
335
+ state :new do
336
+ event :next, :to => :next_state
337
+ end
338
+ state :next_state
339
+ end
340
+
341
+ def on_next_state_entry(prior_state, event, *args)
342
+ @history << "on_next_state_entry #{event} #{prior_state} ->"
343
+ end
344
+
345
+ def on_new_exit(new_state, event, *args)
346
+ @history << "on_new_exit #{event} -> #{new_state}"
347
+ end
348
+ end
349
+
350
+ o = c.new
351
+ assert_equal 'new', o.current_state.to_s
352
+ assert_equal [], o.history
353
+ o.next!
354
+ assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
355
+
356
+ end
357
+
358
+ test 'diagram generation' do
359
+ begin
360
+ $stdout = StringIO.new('', 'w')
361
+ Apollo::create_apollo_diagram(Order, 'doc')
362
+ assert_match(/open.+\.pdf/, $stdout.string,
363
+ 'PDF should be generate and a hint be given to the user.')
364
+ ensure
365
+ $stdout = STDOUT
366
+ end
367
+ end
368
+
369
+ test 'halt stops the transition' do
370
+ c = Class.new do
371
+ include Apollo
372
+ apollo do
373
+ state :young do
374
+ event :age, :to => :old
375
+ end
376
+ state :old
377
+ end
378
+
379
+ def age(by=1)
380
+ halt 'too fast' if by > 100
381
+ end
382
+ end
383
+
384
+ joe = c.new
385
+ assert joe.young?
386
+ joe.age! 120
387
+ assert joe.young?, 'Transition should have been halted'
388
+ assert_equal 'too fast', joe.halted_because
389
+ end
390
+
391
+ test 'halt! raises exception' do
392
+ article_class = Class.new do
393
+ include Apollo
394
+ apollo do
395
+ state :new do
396
+ event :reject, :to => :rejected
397
+ end
398
+ state :rejected
399
+ end
400
+
401
+ def reject(reason)
402
+ halt! 'We do not reject articles unless the reason is important' \
403
+ unless reason =~ /important/i
404
+ end
405
+ end
406
+
407
+ article = article_class.new
408
+ assert article.new?
409
+ assert_raise Apollo::TransitionHalted do
410
+ article.reject! 'Too funny'
411
+ end
412
+ assert article.new?, 'Transition should have been halted'
413
+ article.reject! 'Important: too short'
414
+ assert article.rejected?, 'Transition should happen now'
415
+ end
416
+
417
+ end
418
+