distribot 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +10 -0
- data/Dockerfile +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +153 -0
- data/LICENSE +201 -0
- data/README.md +107 -0
- data/Rakefile +16 -0
- data/bin/distribot.flow-created +6 -0
- data/bin/distribot.flow-finished +6 -0
- data/bin/distribot.handler-finished +6 -0
- data/bin/distribot.phase-finished +6 -0
- data/bin/distribot.phase-started +6 -0
- data/bin/distribot.task-finished +6 -0
- data/distribot.gemspec +35 -0
- data/docker-compose.yml +29 -0
- data/examples/controller +168 -0
- data/examples/distribot.eye +49 -0
- data/examples/status +38 -0
- data/examples/worker +135 -0
- data/lib/distribot/connector.rb +162 -0
- data/lib/distribot/flow.rb +200 -0
- data/lib/distribot/flow_created_handler.rb +12 -0
- data/lib/distribot/flow_finished_handler.rb +12 -0
- data/lib/distribot/handler.rb +40 -0
- data/lib/distribot/handler_finished_handler.rb +29 -0
- data/lib/distribot/phase.rb +46 -0
- data/lib/distribot/phase_finished_handler.rb +19 -0
- data/lib/distribot/phase_handler.rb +15 -0
- data/lib/distribot/phase_started_handler.rb +69 -0
- data/lib/distribot/task_finished_handler.rb +37 -0
- data/lib/distribot/worker.rb +148 -0
- data/lib/distribot.rb +108 -0
- data/provision/nodes.sh +80 -0
- data/provision/templates/fluentd.conf +27 -0
- data/spec/distribot/bunny_connector_spec.rb +196 -0
- data/spec/distribot/connection_sharer_spec.rb +34 -0
- data/spec/distribot/connector_spec.rb +63 -0
- data/spec/distribot/flow_created_handler_spec.rb +32 -0
- data/spec/distribot/flow_finished_handler_spec.rb +32 -0
- data/spec/distribot/flow_spec.rb +661 -0
- data/spec/distribot/handler_finished_handler_spec.rb +112 -0
- data/spec/distribot/handler_spec.rb +32 -0
- data/spec/distribot/module_spec.rb +163 -0
- data/spec/distribot/multi_subscription_spec.rb +37 -0
- data/spec/distribot/phase_finished_handler_spec.rb +61 -0
- data/spec/distribot/phase_started_handler_spec.rb +150 -0
- data/spec/distribot/subscription_spec.rb +40 -0
- data/spec/distribot/task_finished_handler_spec.rb +71 -0
- data/spec/distribot/worker_spec.rb +281 -0
- data/spec/fixtures/simple_flow.json +49 -0
- data/spec/spec_helper.rb +74 -0
- metadata +371 -0
@@ -0,0 +1,661 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Distribot::Flow do
|
4
|
+
before do
|
5
|
+
@json = JSON.parse( File.read('spec/fixtures/simple_flow.json'), symbolize_names: true )
|
6
|
+
end
|
7
|
+
it 'can be initialized' do
|
8
|
+
flow = Distribot::Flow.new(
|
9
|
+
phases: @json[:phases],
|
10
|
+
data: {
|
11
|
+
foo: :bar,
|
12
|
+
items: [ {item1: 'Hello', item2: 'World'} ]
|
13
|
+
}
|
14
|
+
)
|
15
|
+
expect(flow.phases.count).to eq @json[:phases].count
|
16
|
+
expect(flow.data[:foo]).to eq :bar
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.active' do
|
20
|
+
context 'when there are' do
|
21
|
+
context 'no active flows' do
|
22
|
+
before do
|
23
|
+
redis = double('redis')
|
24
|
+
expect(redis).to receive(:smembers).with('distribot.flows.active'){ [] }
|
25
|
+
expect(Distribot::Flow).to receive(:redis){ redis }
|
26
|
+
end
|
27
|
+
it 'returns an empty list' do
|
28
|
+
expect(Distribot::Flow.active).to eq []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
context 'some active flows' do
|
32
|
+
before do
|
33
|
+
@flow_ids = ['foo', 'bar']
|
34
|
+
redis = double('redis')
|
35
|
+
expect(redis).to receive(:smembers).with('distribot.flows.active'){ @flow_ids }
|
36
|
+
@flow_ids.each do |id|
|
37
|
+
expect(redis).to receive(:get).with("distribot-flow:#{id}:definition") do
|
38
|
+
{
|
39
|
+
id: id,
|
40
|
+
phases: [ ]
|
41
|
+
}.to_json
|
42
|
+
end
|
43
|
+
end
|
44
|
+
expect(Distribot::Flow).to receive(:redis).exactly(3).times{ redis }
|
45
|
+
end
|
46
|
+
it 'returns them' do
|
47
|
+
flows = Distribot::Flow.active
|
48
|
+
expect(flows).to be_an Array
|
49
|
+
expect(flows.map(&:id)).to eq @flow_ids
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#redis_id' do
|
56
|
+
before do
|
57
|
+
@flow = Distribot::Flow.new(
|
58
|
+
id: 'fake-id'
|
59
|
+
)
|
60
|
+
end
|
61
|
+
it 'returns the redis id' do
|
62
|
+
expect(@flow.redis_id).to eq 'distribot-flow:' + 'fake-id'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#save!' do
|
67
|
+
before do
|
68
|
+
@flow = Distribot::Flow.new(
|
69
|
+
phases: @json[:phases]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
context 'when saving' do
|
73
|
+
context 'fails' do
|
74
|
+
context 'because the flow already has an id' do
|
75
|
+
before do
|
76
|
+
@flow.id = 'some-id'
|
77
|
+
end
|
78
|
+
it 'raises an error' do
|
79
|
+
expect{@flow.save!}.to raise_error StandardError
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
context 'succeeds' do
|
84
|
+
before do
|
85
|
+
# Fake id:
|
86
|
+
expect(SecureRandom).to receive(:uuid){ 'xxx' }
|
87
|
+
|
88
|
+
# Redis-saving:
|
89
|
+
redis = double('redis')
|
90
|
+
expect(redis).to receive(:set).with('distribot-flow:xxx:definition', anything)
|
91
|
+
expect(redis).to receive(:sadd).with('distribot.flows.active', 'xxx')
|
92
|
+
expect(redis).to receive(:incr).with('distribot.flows.running')
|
93
|
+
expect(@flow).to receive(:redis).exactly(3).times{ redis }
|
94
|
+
|
95
|
+
# Transition-making:
|
96
|
+
expect(@flow).to receive(:current_phase){ 'start' }
|
97
|
+
expect(@flow).to receive(:add_transition).with(hash_including(to: 'start'))
|
98
|
+
|
99
|
+
# Announcement-publishing:
|
100
|
+
expect(Distribot).to receive(:publish!).with('distribot.flow.created', {
|
101
|
+
flow_id: 'xxx'
|
102
|
+
})
|
103
|
+
end
|
104
|
+
context 'when a callback is provided' do
|
105
|
+
before do
|
106
|
+
expect(Thread).to receive(:new) do |&block|
|
107
|
+
block.call
|
108
|
+
end
|
109
|
+
expect(@flow).to receive(:finished?).ordered{false}
|
110
|
+
expect(@flow).to receive(:canceled?).ordered{false}
|
111
|
+
expect(@flow).to receive(:finished?).ordered{true}
|
112
|
+
end
|
113
|
+
it 'waits until finished, then calls the callback with {flow_id: self.id}' do
|
114
|
+
@callback_args = nil
|
115
|
+
@flow.save! do |info|
|
116
|
+
@callback_args = info
|
117
|
+
end
|
118
|
+
expect(@callback_args).to eq(flow_id: 'xxx')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
context 'when no callback is provided' do
|
122
|
+
it 'saves it in redis' do
|
123
|
+
@flow.save!
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '.create!' do
|
131
|
+
before do
|
132
|
+
expect_any_instance_of(Distribot::Flow).to receive(:save!)
|
133
|
+
end
|
134
|
+
it 'saves the object' do
|
135
|
+
flow = Distribot::Flow.create!(phases: [ ])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe '.find' do
|
140
|
+
before do
|
141
|
+
expect(Distribot).to receive(:redis_id){ 'fake-redis-id' }
|
142
|
+
expect(described_class).to receive(:redis) do
|
143
|
+
redis = double('redis')
|
144
|
+
expect(redis).to receive(:get).with('fake-redis-id:definition'){ @stored_json }
|
145
|
+
redis
|
146
|
+
end
|
147
|
+
end
|
148
|
+
context 'when it can be found' do
|
149
|
+
before do
|
150
|
+
@stored_json = @json.to_json
|
151
|
+
end
|
152
|
+
it 'returns the correct flow' do
|
153
|
+
expect(described_class.find('any-id')).to be_a described_class
|
154
|
+
end
|
155
|
+
context 'the data' do
|
156
|
+
before do
|
157
|
+
@flow = described_class.find('any-id')
|
158
|
+
end
|
159
|
+
it 'is intact' do
|
160
|
+
expect(@flow.data[:flow_info]).to eq(foo: 'bar')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
context 'when it cannot be found' do
|
165
|
+
before do
|
166
|
+
@stored_json = nil
|
167
|
+
end
|
168
|
+
it 'returns nil' do
|
169
|
+
expect(described_class.find('any-id')).to be_nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '#phase(name)' do
|
175
|
+
before do
|
176
|
+
@flow = Distribot::Flow.new(
|
177
|
+
id: 'xxx',
|
178
|
+
phases: [{is_initial: true, name: 'testy'} ]
|
179
|
+
)
|
180
|
+
end
|
181
|
+
context 'when the phase' do
|
182
|
+
context 'exists' do
|
183
|
+
it 'returns the phase object' do
|
184
|
+
expect(@flow.phase('testy')).to be_a Distribot::Phase
|
185
|
+
end
|
186
|
+
end
|
187
|
+
context 'does not exist' do
|
188
|
+
it 'returns nil' do
|
189
|
+
expect(@flow.phase('missing-phase')).to be_nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe '#transition_to!(:phase_name)' do
|
196
|
+
context 'when the flow' do
|
197
|
+
before do
|
198
|
+
@flow = Distribot::Flow.new(
|
199
|
+
id: 'xxx',
|
200
|
+
phases: [
|
201
|
+
{is_initial: true, name: 'start'},
|
202
|
+
{is_final: true, name: 'finish'},
|
203
|
+
]
|
204
|
+
)
|
205
|
+
end
|
206
|
+
context 'did not have a previous phase' do
|
207
|
+
before do
|
208
|
+
@next_phase = 'start'
|
209
|
+
expect(@flow).to receive(:transitions){ [ ] }
|
210
|
+
expect(Distribot).to receive(:publish!).with('distribot.flow.phase.started', {
|
211
|
+
flow_id: @flow.id,
|
212
|
+
phase: @next_phase
|
213
|
+
})
|
214
|
+
end
|
215
|
+
it 'stores a transition from nil to the new phase' do
|
216
|
+
expect(@flow).to receive(:add_transition).with(hash_including(from: nil, to: @next_phase))
|
217
|
+
@flow.transition_to!(@next_phase)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
context 'had a previous phase' do
|
221
|
+
before do
|
222
|
+
@next_phase = 'finish'
|
223
|
+
expect(@flow).to receive(:transitions) do
|
224
|
+
[
|
225
|
+
{from: nil, to: 'start'}
|
226
|
+
]
|
227
|
+
end
|
228
|
+
expect(Distribot).to receive(:publish!).with('distribot.flow.phase.started', {
|
229
|
+
flow_id: @flow.id,
|
230
|
+
phase: @next_phase
|
231
|
+
})
|
232
|
+
end
|
233
|
+
it 'stores a transition from the previous phase to the new phase' do
|
234
|
+
expect(@flow).to receive(:add_transition).with(hash_including(from: 'start', to: @next_phase))
|
235
|
+
@flow.transition_to!(@next_phase)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe '#current_phase' do
|
242
|
+
before do
|
243
|
+
@flow = Distribot::Flow.new(
|
244
|
+
id: 'xxx',
|
245
|
+
phases: [
|
246
|
+
{name: 'start', is_initial: true},
|
247
|
+
{name: 'finish', is_final: true},
|
248
|
+
]
|
249
|
+
)
|
250
|
+
end
|
251
|
+
context 'when the flow' do
|
252
|
+
context 'has previous transitions' do
|
253
|
+
before do
|
254
|
+
expect(@flow).to receive(:transitions) do
|
255
|
+
[
|
256
|
+
OpenStruct.new(from: nil, to: 'start', timestamp: 60.seconds.ago.to_i ),
|
257
|
+
OpenStruct.new(from: 'start', to: 'finish', timestamp: 30.seconds.ago.to_i )
|
258
|
+
]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
it 'returns the "to" value of the latest transition' do
|
262
|
+
expect(@flow.current_phase).to eq 'finish'
|
263
|
+
end
|
264
|
+
end
|
265
|
+
context 'has no previous transitions' do
|
266
|
+
before do
|
267
|
+
expect(@flow).to receive(:transitions){ [ ] }
|
268
|
+
end
|
269
|
+
it 'returns the first is_initial:true phase name' do
|
270
|
+
expect(@flow.current_phase).to eq 'start'
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
describe '#next_phase' do
|
277
|
+
before do
|
278
|
+
@flow = Distribot::Flow.new(
|
279
|
+
id: 'xxx',
|
280
|
+
phases: [
|
281
|
+
{name: 'step1', is_initial: true, transitions_to: 'step2'},
|
282
|
+
{name: 'step2', is_final: true},
|
283
|
+
]
|
284
|
+
)
|
285
|
+
end
|
286
|
+
context 'when there is a next phase' do
|
287
|
+
before do
|
288
|
+
expect(@flow).to receive(:current_phase){ 'step1' }
|
289
|
+
end
|
290
|
+
it 'returns the next phase name' do
|
291
|
+
expect(@flow.next_phase).to eq 'step2'
|
292
|
+
end
|
293
|
+
end
|
294
|
+
context 'when there is no next phase' do
|
295
|
+
before do
|
296
|
+
expect(@flow).to receive(:current_phase){ 'step2' }
|
297
|
+
end
|
298
|
+
it 'returns nil' do
|
299
|
+
expect(@flow.next_phase).to be_nil
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
describe '#pause!' do
|
305
|
+
before do
|
306
|
+
@flow = described_class.new(
|
307
|
+
id: 'xxx',
|
308
|
+
phases: [
|
309
|
+
{name: 'start', is_initial: true},
|
310
|
+
{name: 'finish', is_final: true},
|
311
|
+
]
|
312
|
+
)
|
313
|
+
end
|
314
|
+
context 'when running' do
|
315
|
+
before do
|
316
|
+
expect(@flow).to receive(:running?){ true }
|
317
|
+
expect(@flow).to receive(:current_phase){ 'start' }
|
318
|
+
expect(@flow).to receive(:add_transition).with(hash_including(
|
319
|
+
from: 'start',
|
320
|
+
to: 'paused'
|
321
|
+
))
|
322
|
+
end
|
323
|
+
it 'pauses' do
|
324
|
+
@flow.pause!
|
325
|
+
end
|
326
|
+
end
|
327
|
+
context 'when not running' do
|
328
|
+
before do
|
329
|
+
expect(@flow).to receive(:running?){ false }
|
330
|
+
expect(@flow).not_to receive(:current_phase)
|
331
|
+
expect(@flow).not_to receive(:add_transition)
|
332
|
+
end
|
333
|
+
it 'raises an exception' do
|
334
|
+
expect{@flow.pause!}.to raise_error Distribot::NotRunningError
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
describe '#paused?' do
|
339
|
+
before do
|
340
|
+
@flow = described_class.new(
|
341
|
+
id: 'xxx',
|
342
|
+
phases: [
|
343
|
+
{name: 'start', is_initial: true},
|
344
|
+
{name: 'finish', is_final: true},
|
345
|
+
]
|
346
|
+
)
|
347
|
+
end
|
348
|
+
context 'when paused' do
|
349
|
+
before do
|
350
|
+
expect(@flow).to receive(:current_phase){ 'paused' }
|
351
|
+
end
|
352
|
+
it 'returns true' do
|
353
|
+
expect(@flow.paused?).to be_truthy
|
354
|
+
end
|
355
|
+
end
|
356
|
+
context 'when not paused' do
|
357
|
+
before do
|
358
|
+
expect(@flow).to receive(:current_phase){ 'start' }
|
359
|
+
end
|
360
|
+
it 'returns false' do
|
361
|
+
expect(@flow.paused?).to be_falsey
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
describe '#resume!' do
|
366
|
+
before do
|
367
|
+
@flow = described_class.new(
|
368
|
+
id: 'xxx',
|
369
|
+
phases: [
|
370
|
+
{name: 'start', is_initial: true},
|
371
|
+
{name: 'finish', is_final: true},
|
372
|
+
]
|
373
|
+
)
|
374
|
+
end
|
375
|
+
context 'when paused' do
|
376
|
+
before do
|
377
|
+
expect(@flow).to receive(:paused?){ true }
|
378
|
+
expect(@flow).to receive(:transitions) do
|
379
|
+
to_start = {from: nil, to: 'start', timestamp: 60.seconds.ago.to_f}
|
380
|
+
to_paused = {from: 'start', to: 'paused', timestamp: 30.seconds.ago.to_f}
|
381
|
+
[to_start, to_paused].map{|x| OpenStruct.new x }
|
382
|
+
end
|
383
|
+
expect(@flow).to receive(:add_transition).with(hash_including(
|
384
|
+
from: 'paused',
|
385
|
+
to: 'start'
|
386
|
+
))
|
387
|
+
end
|
388
|
+
it 'transitions back to the last phase transitioned to' do
|
389
|
+
@flow.resume!
|
390
|
+
end
|
391
|
+
end
|
392
|
+
context 'when not paused' do
|
393
|
+
before do
|
394
|
+
expect(@flow).to receive(:paused?){ false }
|
395
|
+
end
|
396
|
+
it 'raises an exception' do
|
397
|
+
expect{@flow.resume!}.to raise_error Distribot::NotPausedError
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
describe '#cancel!' do
|
402
|
+
before do
|
403
|
+
@flow = described_class.new(
|
404
|
+
id: 'xxx',
|
405
|
+
phases: [
|
406
|
+
{name: 'start', is_initial: true},
|
407
|
+
{name: 'finish', is_final: true},
|
408
|
+
]
|
409
|
+
)
|
410
|
+
end
|
411
|
+
context 'when running' do
|
412
|
+
before do
|
413
|
+
expect(@flow).to receive(:running?){ true }
|
414
|
+
expect(@flow).to receive(:current_phase){ 'start' }
|
415
|
+
expect(@flow).to receive(:add_transition).with(hash_including(
|
416
|
+
from: 'start',
|
417
|
+
to: 'canceled'
|
418
|
+
))
|
419
|
+
redis = double('redis')
|
420
|
+
expect(@flow).to receive(:redis).exactly(2).times{ redis }
|
421
|
+
expect(redis).to receive(:srem).with('distribot.flows.active', @flow.id)
|
422
|
+
expect(redis).to receive(:decr).with('distribot.flows.running')
|
423
|
+
end
|
424
|
+
it 'cancels the flow' do
|
425
|
+
@flow.cancel!
|
426
|
+
end
|
427
|
+
end
|
428
|
+
context 'when not running' do
|
429
|
+
before do
|
430
|
+
expect(@flow).to receive(:running?){ false }
|
431
|
+
expect(@flow).not_to receive(:current_phase)
|
432
|
+
expect(@flow).not_to receive(:add_transition)
|
433
|
+
end
|
434
|
+
it 'raises an exception' do
|
435
|
+
expect{@flow.cancel!}.to raise_error Distribot::NotRunningError
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
describe '#canceled?' do
|
440
|
+
before do
|
441
|
+
@flow = described_class.new(
|
442
|
+
id: 'xxx',
|
443
|
+
phases: [
|
444
|
+
{name: 'start', is_initial: true},
|
445
|
+
{name: 'finish', is_final: true},
|
446
|
+
]
|
447
|
+
)
|
448
|
+
end
|
449
|
+
context 'when canceled' do
|
450
|
+
before do
|
451
|
+
expect(@flow).to receive(:current_phase){ 'canceled' }
|
452
|
+
end
|
453
|
+
it 'returns true' do
|
454
|
+
expect(@flow.canceled?).to be_truthy
|
455
|
+
end
|
456
|
+
end
|
457
|
+
context 'when not canceled' do
|
458
|
+
before do
|
459
|
+
expect(@flow).to receive(:current_phase){ 'start' }
|
460
|
+
end
|
461
|
+
it 'returns false' do
|
462
|
+
expect(@flow.canceled?).to be_falsey
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
describe '#running?' do
|
467
|
+
before do
|
468
|
+
@flow = described_class.new(
|
469
|
+
id: 'xxx',
|
470
|
+
phases: [
|
471
|
+
{name: 'start', is_initial: true},
|
472
|
+
{name: 'finish', is_final: true},
|
473
|
+
]
|
474
|
+
)
|
475
|
+
end
|
476
|
+
context 'when paused' do
|
477
|
+
before do
|
478
|
+
expect(@flow).to receive(:paused?){ true }
|
479
|
+
end
|
480
|
+
it 'returns false' do
|
481
|
+
expect(@flow.running?).to be_falsey
|
482
|
+
end
|
483
|
+
end
|
484
|
+
context 'when canceled' do
|
485
|
+
before do
|
486
|
+
expect(@flow).to receive(:paused?){ false }
|
487
|
+
expect(@flow).to receive(:canceled?){ true }
|
488
|
+
end
|
489
|
+
it 'returns false' do
|
490
|
+
expect(@flow.running?).to be_falsey
|
491
|
+
end
|
492
|
+
end
|
493
|
+
context 'when finished' do
|
494
|
+
before do
|
495
|
+
expect(@flow).to receive(:paused?){ false }
|
496
|
+
expect(@flow).to receive(:canceled?){ false }
|
497
|
+
expect(@flow).to receive(:finished?){ true }
|
498
|
+
end
|
499
|
+
it 'returns false' do
|
500
|
+
expect(@flow.running?).to be_falsey
|
501
|
+
end
|
502
|
+
end
|
503
|
+
context 'when neither canceled, paused nor finished' do
|
504
|
+
before do
|
505
|
+
expect(@flow).to receive(:paused?){ false }
|
506
|
+
expect(@flow).to receive(:canceled?){ false }
|
507
|
+
expect(@flow).to receive(:finished?){ false }
|
508
|
+
end
|
509
|
+
it 'returns true' do
|
510
|
+
expect(@flow.running?).to be_truthy
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
describe '#finished?' do
|
516
|
+
before do
|
517
|
+
@flow = described_class.new(
|
518
|
+
id: 'xxx',
|
519
|
+
phases: [
|
520
|
+
{name: 'start', is_initial: true},
|
521
|
+
{name: 'finish', is_final: true},
|
522
|
+
]
|
523
|
+
)
|
524
|
+
end
|
525
|
+
context 'when the latest transition is to a phase that' do
|
526
|
+
before do
|
527
|
+
expect(@flow).to receive(:transitions) do
|
528
|
+
[OpenStruct.new( to: 'latest-phase' )]
|
529
|
+
end
|
530
|
+
expect(@flow).to receive(:phase) do
|
531
|
+
phase = double('phase')
|
532
|
+
expect(phase).to receive(:is_final){ @is_final }
|
533
|
+
phase
|
534
|
+
end
|
535
|
+
end
|
536
|
+
context 'is final' do
|
537
|
+
before do
|
538
|
+
@is_final = true
|
539
|
+
end
|
540
|
+
it 'returns true' do
|
541
|
+
expect(@flow.finished?).to be_truthy
|
542
|
+
end
|
543
|
+
end
|
544
|
+
context 'is not final' do
|
545
|
+
before do
|
546
|
+
@is_final = false
|
547
|
+
end
|
548
|
+
it 'returns false' do
|
549
|
+
expect(@flow.finished?).to be_falsey
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
describe '#add_transition(...)' do
|
556
|
+
before do
|
557
|
+
@flow = described_class.new(
|
558
|
+
id: 'xxx',
|
559
|
+
phases: [
|
560
|
+
{name: 'start', is_initial: true},
|
561
|
+
{name: 'finish', is_final: true},
|
562
|
+
]
|
563
|
+
)
|
564
|
+
end
|
565
|
+
before do
|
566
|
+
@transition_info = {
|
567
|
+
from: 'start',
|
568
|
+
to: 'finish',
|
569
|
+
timestamp: Time.now.utc.to_f
|
570
|
+
}
|
571
|
+
redis = double('redis')
|
572
|
+
expect(redis).to receive(:sadd).with(@flow.redis_id + ":transitions", @transition_info.to_json)
|
573
|
+
expect(@flow).to receive(:redis){ redis }
|
574
|
+
end
|
575
|
+
it 'adds a transition record for the flow' do
|
576
|
+
@flow.add_transition(@transition_info)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
describe '#transitions' do
|
581
|
+
before do
|
582
|
+
@flow = described_class.new(
|
583
|
+
id: 'xxx',
|
584
|
+
phases: [
|
585
|
+
{name: 'start', is_initial: true},
|
586
|
+
{name: 'finish', is_final: true},
|
587
|
+
]
|
588
|
+
)
|
589
|
+
redis = double('redis')
|
590
|
+
expect(redis).to receive(:smembers).with(@flow.redis_id + ':transitions'){ @transitions }
|
591
|
+
expect(@flow).to receive(:redis){ redis }
|
592
|
+
end
|
593
|
+
context 'when there are no transitions yet' do
|
594
|
+
before do
|
595
|
+
@transitions = [ ]
|
596
|
+
end
|
597
|
+
it 'returns an empty list' do
|
598
|
+
expect(@flow.transitions).to eq [ ]
|
599
|
+
end
|
600
|
+
end
|
601
|
+
context 'when there are transitions' do
|
602
|
+
before do
|
603
|
+
@transitions = [
|
604
|
+
{from: 'paused', to: 'start', timestamp: 20.seconds.ago.to_f},
|
605
|
+
{from: nil, to: 'start', timestamp: 60.seconds.ago.to_f},
|
606
|
+
{from: 'start', to: 'paused', timestamp: 40.seconds.ago.to_f},
|
607
|
+
].map(&:to_json)
|
608
|
+
end
|
609
|
+
it 'returns them sorted by timestamp' do
|
610
|
+
original_transitions = @transitions.map{|x| JSON.parse(x, symbolize_names: true)}
|
611
|
+
@results = @flow.transitions
|
612
|
+
expect(@results.first.timestamp).to eq original_transitions[1][:timestamp]
|
613
|
+
expect(@results.last.timestamp).to eq original_transitions.first[:timestamp]
|
614
|
+
end
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
describe '#redis' do
|
619
|
+
it 'returns redis' do
|
620
|
+
expect(Distribot::Flow).to receive(:redis){ 'redis-lol' }
|
621
|
+
expect(described_class.new.send(:redis)).to eq 'redis-lol'
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
describe '.redis' do
|
626
|
+
it 'returns redis' do
|
627
|
+
expect(Distribot).to receive(:redis){ 'redis-lol' }
|
628
|
+
expect(described_class.send(:redis)).to eq 'redis-lol'
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
describe '#stubbornly' do
|
633
|
+
context 'when the block' do
|
634
|
+
context 'raises an error' do
|
635
|
+
it 'keeps trying forever, until it stops raising an error' do
|
636
|
+
@return_value = SecureRandom.uuid
|
637
|
+
flow = described_class.new
|
638
|
+
@max_tries = 3
|
639
|
+
@total_tries = 0
|
640
|
+
expect(flow).to receive(:warn).exactly(3).times
|
641
|
+
expect(flow.stubbornly(:foo){
|
642
|
+
if @total_tries >= @max_tries
|
643
|
+
@return_value
|
644
|
+
else
|
645
|
+
@total_tries += 1
|
646
|
+
raise NoMethodError.new
|
647
|
+
end
|
648
|
+
}).to eq @return_value
|
649
|
+
end
|
650
|
+
end
|
651
|
+
context 'does not raise an error' do
|
652
|
+
it 'returns the result of the block' do
|
653
|
+
@return_value = SecureRandom.uuid
|
654
|
+
flow = described_class.new
|
655
|
+
expect(flow.stubbornly(:foo){ @return_value }).to eq @return_value
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|