rspec-rebound 0.1.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,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.10.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.11.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.12.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.13.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.3.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.4.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.5.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.6.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.7.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.8.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rspec", "~> 3.9.0"
6
+
7
+ gemspec path: '../..'
@@ -0,0 +1,421 @@
1
+ require 'spec_helper'
2
+
3
+ describe RSpec::Rebound do
4
+ def count
5
+ @count ||= 0
6
+ @count
7
+ end
8
+
9
+ def count_up
10
+ @count ||= 0
11
+ @count += 1
12
+ end
13
+
14
+ def set_expectations(expectations)
15
+ @expectations = expectations
16
+ end
17
+
18
+ def shift_expectation
19
+ @expectations.shift
20
+ end
21
+
22
+ class RetryError < StandardError; end
23
+ class RetryChildError < RetryError; end
24
+ class HardFailError < StandardError; end
25
+ class HardFailChildError < HardFailError; end
26
+ class OtherError < StandardError; end
27
+ class SharedError < StandardError; end
28
+ before(:all) do
29
+ ENV.delete('RSPEC_RETRY_RETRY_COUNT')
30
+ end
31
+
32
+ context 'no retry option' do
33
+ it 'should work' do
34
+ expect(true).to be(true)
35
+ end
36
+ end
37
+
38
+ context 'with retry option' do
39
+ before(:each) { count_up }
40
+
41
+ context do
42
+ before(:all) { set_expectations([false, false, true]) }
43
+
44
+ it 'should run example until :retry times', :retry => 3 do
45
+ expect(true).to be(shift_expectation)
46
+ expect(count).to eq(3)
47
+ end
48
+ end
49
+
50
+ context do
51
+ before(:all) { set_expectations([false, true, false]) }
52
+
53
+ it 'should stop retrying if example is succeeded', :retry => 3 do
54
+ expect(true).to be(shift_expectation)
55
+ expect(count).to eq(2)
56
+ end
57
+ end
58
+
59
+ context 'with lambda condition' do
60
+ before(:all) { set_expectations([false, true]) }
61
+
62
+ it "should get retry count from condition call", retry_me_once: true do
63
+ expect(true).to be(shift_expectation)
64
+ expect(count).to eq(2)
65
+ end
66
+ end
67
+
68
+ context 'with :retry => 0' do
69
+ class Fred
70
+ @@attempt_count = 0
71
+ def attempt_count
72
+ @@attempt_count
73
+ end
74
+ end
75
+ it 'should still run once', retry: 0 do
76
+ Fred.class_variable_set(:@@attempt_count, 1)
77
+ end
78
+
79
+ it 'should have run exactly once' do
80
+ expect(Fred.class_variable_get(:@@attempt_count)).to eq(1)
81
+ end
82
+ end
83
+
84
+ context 'with the environment variable RSPEC_RETRY_RETRY_COUNT' do
85
+ before(:all) do
86
+ set_expectations([false, false, true])
87
+ ENV['RSPEC_RETRY_RETRY_COUNT'] = '3'
88
+ end
89
+
90
+ after(:all) do
91
+ ENV.delete('RSPEC_RETRY_RETRY_COUNT')
92
+ end
93
+
94
+ it 'should override the retry count set in an example', :retry => 2 do
95
+ expect(true).to be(shift_expectation)
96
+ expect(count).to eq(3)
97
+ end
98
+ end
99
+
100
+ context "with exponential backoff enabled", :retry => 3, :retry_wait => 0.001, :exponential_backoff => true do
101
+ context do
102
+ before(:all) do
103
+ set_expectations([false, false, true])
104
+ @start_time = Time.now
105
+ end
106
+
107
+ it 'should run example until :retry times', :retry => 3 do
108
+ expect(true).to be(shift_expectation)
109
+ expect(count).to eq(3)
110
+ expect(Time.now - @start_time).to be >= (0.001)
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "with a list of exceptions to immediately fail on", :retry => 2, :exceptions_to_hard_fail => [HardFailError] do
116
+ context "the example throws an exception contained in the hard fail list" do
117
+ it "does not retry" do
118
+ expect(count).to be < 2
119
+ pending "This should fail with a count of 1: Count was #{count}"
120
+ raise HardFailError unless count > 1
121
+ end
122
+ end
123
+
124
+ context "the example throws a child of an exception contained in the hard fail list" do
125
+ it "does not retry" do
126
+ expect(count).to be < 2
127
+ pending "This should fail with a count of 1: Count was #{count}"
128
+ raise HardFailChildError unless count > 1
129
+ end
130
+ end
131
+
132
+ context "the throws an exception not contained in the hard fail list" do
133
+ it "retries the maximum number of times" do
134
+ raise OtherError unless count > 1
135
+ expect(count).to eq(2)
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "with a list of exceptions to retry on", :retry => 2, :exceptions_to_retry => [RetryError] do
141
+ context do
142
+ let(:rspec_version) { RSpec::Core::Version::STRING }
143
+
144
+ let(:example_code) do
145
+ %{
146
+ $count ||= 0
147
+ $count += 1
148
+
149
+ raise NameError unless $count > 2
150
+ }
151
+ end
152
+
153
+ let!(:example_group) do
154
+ $count, $example_code = 0, example_code
155
+
156
+ RSpec.describe("example group", exceptions_to_retry: [NameError], retry: 3).tap do |this|
157
+ this.run # initialize for rspec 3.3+ with no examples
158
+ end
159
+ end
160
+
161
+ let(:retry_attempts) do
162
+ example_group.examples.first.metadata[:retry_attempts]
163
+ end
164
+
165
+ it 'should retry and match attempts metadata' do
166
+ example_group.example { instance_eval($example_code) }
167
+ example_group.run
168
+
169
+ expect(retry_attempts).to eq(2)
170
+ end
171
+
172
+ let(:retry_exceptions) do
173
+ example_group.examples.first.metadata[:retry_exceptions]
174
+ end
175
+
176
+ it 'should add exceptions into retry_exceptions metadata array' do
177
+ example_group.example { instance_eval($example_code) }
178
+ example_group.run
179
+
180
+ expect(retry_exceptions.count).to eq(2)
181
+ expect(retry_exceptions[0].class).to eq NameError
182
+ expect(retry_exceptions[1].class).to eq NameError
183
+ end
184
+ end
185
+
186
+ context "the example throws an exception contained in the retry list" do
187
+ it "retries the maximum number of times" do
188
+ raise RetryError unless count > 1
189
+ expect(count).to eq(2)
190
+ end
191
+ end
192
+
193
+ context "the example throws a child of an exception contained in the retry list" do
194
+ it "retries the maximum number of times" do
195
+ raise RetryChildError unless count > 1
196
+ expect(count).to eq(2)
197
+ end
198
+ end
199
+
200
+ context "the example fails (with an exception not in the retry list)" do
201
+ it "only runs once" do
202
+ set_expectations([false])
203
+ expect(count).to eq(1)
204
+ end
205
+ end
206
+
207
+ context 'the example retries exceptions which match with case equality' do
208
+ class CaseEqualityError < StandardError
209
+ def self.===(other)
210
+ # An example of dynamic matching
211
+ other.message == 'Rescue me!'
212
+ end
213
+ end
214
+
215
+ it 'retries the maximum number of times', exceptions_to_retry: [CaseEqualityError] do
216
+ raise StandardError, 'Rescue me!' unless count > 1
217
+ expect(count).to eq(2)
218
+ end
219
+ end
220
+ end
221
+
222
+ describe "with both hard fail and retry list of exceptions", :retry => 2, :exceptions_to_retry => [SharedError, RetryError], :exceptions_to_hard_fail => [SharedError, HardFailError] do
223
+ context "the exception thrown exists in both lists" do
224
+ it "does not retry because the hard fail list takes precedence" do
225
+ expect(count).to be < 2
226
+ pending "This should fail with a count of 1: Count was #{count}"
227
+ raise SharedError unless count > 1
228
+ end
229
+ end
230
+
231
+ context "the example throws an exception contained in the hard fail list" do
232
+ it "does not retry because the hard fail list takes precedence" do
233
+ expect(count).to be < 2
234
+ pending "This should fail with a count of 1: Count was #{count}"
235
+ raise HardFailError unless count > 1
236
+ end
237
+ end
238
+
239
+ context "the example throws an exception contained in the retry list" do
240
+ it "retries the maximum number of times because the hard fail list doesn't affect this exception" do
241
+ raise RetryError unless count > 1
242
+ expect(count).to eq(2)
243
+ end
244
+ end
245
+
246
+ context "the example throws an exception contained in neither list" do
247
+ it "does not retry because the the exception is not in the retry list" do
248
+ expect(count).to be < 2
249
+ pending "This should fail with a count of 1: Count was #{count}"
250
+ raise OtherError unless count > 1
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ describe 'clearing lets' do
257
+ before(:all) do
258
+ @control = true
259
+ end
260
+
261
+ let(:let_based_on_control) { @control }
262
+
263
+ after do
264
+ @control = false
265
+ end
266
+
267
+ it 'should clear the let when the test fails so it can be reset', :retry => 2 do
268
+ expect(let_based_on_control).to be(false)
269
+ end
270
+
271
+ it 'should not clear the let when the test fails', :retry => 2, :clear_lets_on_failure => false do
272
+ expect(let_based_on_control).to be(!@control)
273
+ end
274
+ end
275
+
276
+ describe 'running example.run_with_retry in an around filter', retry: 2 do
277
+ before(:each) { count_up }
278
+ before(:all) do
279
+ set_expectations([false, false, true])
280
+ end
281
+
282
+ it 'allows retry options to be overridden', :overridden do
283
+ expect(RSpec.current_example.metadata[:retry]).to eq(3)
284
+ end
285
+
286
+ it 'uses the overridden options', :overridden do
287
+ expect(true).to be(shift_expectation)
288
+ expect(count).to eq(3)
289
+ end
290
+ end
291
+
292
+ describe 'calling retry_callback between retries', retry: 2 do
293
+ before(:all) do
294
+ RSpec.configuration.retry_callback = proc do |example|
295
+ @retry_callback_called = true
296
+ @example = example
297
+ end
298
+ end
299
+
300
+ after(:all) do
301
+ RSpec.configuration.retry_callback = nil
302
+ end
303
+
304
+ context 'if failure' do
305
+ before(:all) do
306
+ @retry_callback_called = false
307
+ @example = nil
308
+ @retry_attempts = 0
309
+ end
310
+
311
+ it 'should call retry callback', with_some: 'metadata' do |example|
312
+ if @retry_attempts == 0
313
+ @retry_attempts += 1
314
+ expect(@retry_callback_called).to be(false)
315
+ expect(@example).to eq(nil)
316
+ raise "let's retry once!"
317
+ elsif @retry_attempts > 0
318
+ expect(@retry_callback_called).to be(true)
319
+ expect(@example).to eq(example)
320
+ expect(@example.metadata[:with_some]).to eq('metadata')
321
+ end
322
+ end
323
+ end
324
+
325
+ context 'does not call retry_callback if no errors' do
326
+ before(:all) do
327
+ @retry_callback_called = false
328
+ @example = nil
329
+ end
330
+
331
+ after do
332
+ expect(@retry_callback_called).to be(false)
333
+ expect(@example).to be_nil
334
+ end
335
+
336
+ it { true }
337
+ end
338
+ end
339
+
340
+ describe 'Example::Procsy#attempts' do
341
+ let!(:example_group) do
342
+ RSpec.describe do
343
+ class ReboundResults
344
+ @@results = {}
345
+
346
+ def self.results
347
+ @@results
348
+ end
349
+
350
+ def add(example)
351
+ @@results[example.description] = [example.exception.nil?, example.attempts]
352
+ end
353
+ end
354
+
355
+ around do |example|
356
+ example.run_with_retry
357
+ results = ReboundResults.results
358
+ results[example.description] = [example.exception.nil?, example.attempts]
359
+ ReboundResults.class_variable_set(:@@results, results)
360
+ end
361
+
362
+ specify 'without retry option' do
363
+ expect(true).to be(true)
364
+ end
365
+
366
+ specify 'with retry option', retry: 2 do
367
+ expect(true).to be(false)
368
+ end
369
+ end
370
+ end
371
+
372
+ it 'should be exposed' do
373
+ example_group.run
374
+ expect(ReboundResults.results).to eq({
375
+ 'without retry option' => [true, 1],
376
+ 'with retry option' => [false, 3]
377
+ })
378
+ end
379
+ end
380
+
381
+ describe 'output in verbose mode' do
382
+
383
+ line_1 = __LINE__ + 8
384
+ line_2 = __LINE__ + 11
385
+ let(:group) do
386
+ RSpec.describe 'ExampleGroup', retry: 1 do
387
+ after do
388
+ fail 'broken after hook'
389
+ end
390
+
391
+ it 'passes' do
392
+ true
393
+ end
394
+
395
+ it 'fails' do
396
+ fail 'broken spec'
397
+ end
398
+ end
399
+ end
400
+
401
+ it 'outputs failures correctly' do
402
+ RSpec.configuration.output_stream = output = StringIO.new
403
+ RSpec.configuration.verbose_retry = true
404
+ RSpec.configuration.display_try_failure_messages = true
405
+ expect {
406
+ group.run RSpec.configuration.reporter
407
+ }.to change { output.string }.to a_string_including <<-STRING.gsub(/^\s+\| ?/, '')
408
+ | 1st Try error in ./spec/lib/rspec/rebound_spec.rb:#{line_1}:
409
+ | broken after hook
410
+ |
411
+ | RSpec::Rebound: 2nd try ./spec/lib/rspec/rebound_spec.rb:#{line_1}
412
+ | F
413
+ | 1st Try error in ./spec/lib/rspec/rebound_spec.rb:#{line_2}:
414
+ | broken spec
415
+ | broken after hook
416
+ |
417
+ | RSpec::Rebound: 2nd try ./spec/lib/rspec/rebound_spec.rb:#{line_2}
418
+ STRING
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,27 @@
1
+ require 'rspec'
2
+ require 'rspec/core/sandbox'
3
+
4
+ require 'rspec/rebound'
5
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2')
6
+ require "pry-debugger"
7
+ else
8
+ require "pry-byebug"
9
+ end
10
+
11
+ RSpec.configure do |config|
12
+ config.verbose_retry = true
13
+ config.display_try_failure_messages = true
14
+
15
+ config.around :example do |ex|
16
+ RSpec::Core::Sandbox.sandboxed do |config|
17
+ RSpec::Rebound.setup
18
+ ex.run
19
+ end
20
+ end
21
+
22
+ config.around :each, :overridden do |ex|
23
+ ex.run_with_retry retry: 3
24
+ end
25
+
26
+ config.retry_count_condition = ->(example) { example.metadata[:retry_me_once] ? 2 : nil }
27
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-rebound
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Federico Aldunate
8
+ - Agustin Fornio
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2025-03-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec-core
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.3'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.3'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3.3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3.3'
42
+ description: A RSpec extension that automatically retries intermittently failing examples
43
+ to reduce test flakiness and improve reliability in your test suite.
44
+ email:
45
+ - tech@windmotion.io
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".github/workflows/ci.yml"
51
+ - ".gitignore"
52
+ - ".travis.yml"
53
+ - Appraisals
54
+ - Gemfile
55
+ - Guardfile
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - changelog.md
60
+ - lib/rspec/rebound.rb
61
+ - lib/rspec/rebound/formatter.rb
62
+ - lib/rspec/rebound/version.rb
63
+ - lib/rspec_ext/rspec_ext.rb
64
+ - rspec-rebound.gemspec
65
+ - spec/gemfiles/rspec_3.10.gemfile
66
+ - spec/gemfiles/rspec_3.11.gemfile
67
+ - spec/gemfiles/rspec_3.12.gemfile
68
+ - spec/gemfiles/rspec_3.13.gemfile
69
+ - spec/gemfiles/rspec_3.3.gemfile
70
+ - spec/gemfiles/rspec_3.4.gemfile
71
+ - spec/gemfiles/rspec_3.5.gemfile
72
+ - spec/gemfiles/rspec_3.6.gemfile
73
+ - spec/gemfiles/rspec_3.7.gemfile
74
+ - spec/gemfiles/rspec_3.8.gemfile
75
+ - spec/gemfiles/rspec_3.9.gemfile
76
+ - spec/lib/rspec/rebound_spec.rb
77
+ - spec/spec_helper.rb
78
+ homepage: https://github.com/windmotion-io/rspec-rebound
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '2.0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.5.11
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Retry intermittently failing RSpec examples to eliminate flaky tests and
101
+ increase test suite stability without modifying your existing specs.
102
+ test_files:
103
+ - spec/gemfiles/rspec_3.10.gemfile
104
+ - spec/gemfiles/rspec_3.11.gemfile
105
+ - spec/gemfiles/rspec_3.12.gemfile
106
+ - spec/gemfiles/rspec_3.13.gemfile
107
+ - spec/gemfiles/rspec_3.3.gemfile
108
+ - spec/gemfiles/rspec_3.4.gemfile
109
+ - spec/gemfiles/rspec_3.5.gemfile
110
+ - spec/gemfiles/rspec_3.6.gemfile
111
+ - spec/gemfiles/rspec_3.7.gemfile
112
+ - spec/gemfiles/rspec_3.8.gemfile
113
+ - spec/gemfiles/rspec_3.9.gemfile
114
+ - spec/lib/rspec/rebound_spec.rb
115
+ - spec/spec_helper.rb