apollo 1.0.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,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
+