distribot 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +3 -0
  5. data/.travis.yml +10 -0
  6. data/Dockerfile +9 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +153 -0
  9. data/LICENSE +201 -0
  10. data/README.md +107 -0
  11. data/Rakefile +16 -0
  12. data/bin/distribot.flow-created +6 -0
  13. data/bin/distribot.flow-finished +6 -0
  14. data/bin/distribot.handler-finished +6 -0
  15. data/bin/distribot.phase-finished +6 -0
  16. data/bin/distribot.phase-started +6 -0
  17. data/bin/distribot.task-finished +6 -0
  18. data/distribot.gemspec +35 -0
  19. data/docker-compose.yml +29 -0
  20. data/examples/controller +168 -0
  21. data/examples/distribot.eye +49 -0
  22. data/examples/status +38 -0
  23. data/examples/worker +135 -0
  24. data/lib/distribot/connector.rb +162 -0
  25. data/lib/distribot/flow.rb +200 -0
  26. data/lib/distribot/flow_created_handler.rb +12 -0
  27. data/lib/distribot/flow_finished_handler.rb +12 -0
  28. data/lib/distribot/handler.rb +40 -0
  29. data/lib/distribot/handler_finished_handler.rb +29 -0
  30. data/lib/distribot/phase.rb +46 -0
  31. data/lib/distribot/phase_finished_handler.rb +19 -0
  32. data/lib/distribot/phase_handler.rb +15 -0
  33. data/lib/distribot/phase_started_handler.rb +69 -0
  34. data/lib/distribot/task_finished_handler.rb +37 -0
  35. data/lib/distribot/worker.rb +148 -0
  36. data/lib/distribot.rb +108 -0
  37. data/provision/nodes.sh +80 -0
  38. data/provision/templates/fluentd.conf +27 -0
  39. data/spec/distribot/bunny_connector_spec.rb +196 -0
  40. data/spec/distribot/connection_sharer_spec.rb +34 -0
  41. data/spec/distribot/connector_spec.rb +63 -0
  42. data/spec/distribot/flow_created_handler_spec.rb +32 -0
  43. data/spec/distribot/flow_finished_handler_spec.rb +32 -0
  44. data/spec/distribot/flow_spec.rb +661 -0
  45. data/spec/distribot/handler_finished_handler_spec.rb +112 -0
  46. data/spec/distribot/handler_spec.rb +32 -0
  47. data/spec/distribot/module_spec.rb +163 -0
  48. data/spec/distribot/multi_subscription_spec.rb +37 -0
  49. data/spec/distribot/phase_finished_handler_spec.rb +61 -0
  50. data/spec/distribot/phase_started_handler_spec.rb +150 -0
  51. data/spec/distribot/subscription_spec.rb +40 -0
  52. data/spec/distribot/task_finished_handler_spec.rb +71 -0
  53. data/spec/distribot/worker_spec.rb +281 -0
  54. data/spec/fixtures/simple_flow.json +49 -0
  55. data/spec/spec_helper.rb +74 -0
  56. 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
+