rspec-fortify 1.0.0.pre
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/.github/workflows/ci.yml +32 -0
- data/.github/workflows/linter.yml +16 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +21 -0
- data/.ruby-version +1 -0
- data/.travis.yml +15 -0
- data/Appraisals +21 -0
- data/Gemfile +10 -0
- data/Guardfile +7 -0
- data/LICENSE +22 -0
- data/README.md +94 -0
- data/Rakefile +11 -0
- data/changelog.md +99 -0
- data/gemfiles/rspec_3.10.gemfile +12 -0
- data/gemfiles/rspec_3.10.gemfile.lock +123 -0
- data/gemfiles/rspec_3.11.gemfile +12 -0
- data/gemfiles/rspec_3.11.gemfile.lock +123 -0
- data/gemfiles/rspec_3.12.gemfile +12 -0
- data/gemfiles/rspec_3.12.gemfile.lock +123 -0
- data/gemfiles/rspec_3.13.gemfile +12 -0
- data/gemfiles/rspec_3.13.gemfile.lock +123 -0
- data/gemfiles/rspec_3.9.gemfile +12 -0
- data/gemfiles/rspec_3.9.gemfile.lock +123 -0
- data/lib/rspec/fortify/formatter.rb +65 -0
- data/lib/rspec/fortify/version.rb +7 -0
- data/lib/rspec/fortify.rb +255 -0
- data/lib/rspec_ext/rspec_ext.rb +41 -0
- data/rspec-fortify.gemspec +22 -0
- data/spec/fixtures/bad_test.rb +11 -0
- data/spec/fixtures/flaky_test.rb +15 -0
- data/spec/fixtures/good_test.rb +11 -0
- data/spec/lib/rspec/fortify_spec.rb +212 -0
- data/spec/spec_helper.rb +3 -0
- metadata +93 -0
@@ -0,0 +1,255 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core'
|
4
|
+
require 'rspec/fortify/version'
|
5
|
+
require 'rspec_ext/rspec_ext'
|
6
|
+
|
7
|
+
module RSpec
|
8
|
+
class Fortify
|
9
|
+
def self.setup # rubocop:disable Metrics/AbcSize
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.add_setting :clear_lets_on_failure, default: true
|
12
|
+
config.add_setting :default_retry_count, default: 1
|
13
|
+
config.add_setting :default_sleep_interval, default: 0
|
14
|
+
config.add_setting :display_try_failure_messages, default: true
|
15
|
+
config.add_setting :exponential_backoff, default: false
|
16
|
+
config.add_setting :retry_on_failure, default: main? || pr?
|
17
|
+
config.add_setting :retry_on_failure_count, default: 2
|
18
|
+
config.add_setting :retry_on_success, default: pr? && changed_specs.size < 30
|
19
|
+
config.add_setting :retry_on_success_count, default: 10
|
20
|
+
config.add_setting :verbose_retry, default: true
|
21
|
+
|
22
|
+
# retry based on example metadata
|
23
|
+
config.add_setting :retry_count_condition, default: ->(_) {}
|
24
|
+
|
25
|
+
# If a list of exceptions is provided and 'retry' > 1, we only retry if
|
26
|
+
# the exception that was raised by the example is NOT in that list. Otherwise
|
27
|
+
# we ignore the 'retry' value and fail immediately.
|
28
|
+
#
|
29
|
+
# If no list of exceptions is provided and 'retry' > 1, we always retry.
|
30
|
+
config.add_setting :exceptions_to_hard_fail, default: []
|
31
|
+
|
32
|
+
# If a list of exceptions is provided and 'retry' > 1, we only retry if
|
33
|
+
# the exception that was raised by the example is in that list. Otherwise
|
34
|
+
# we ignore the 'retry' value and fail immediately.
|
35
|
+
#
|
36
|
+
# If no list of exceptions is provided and 'retry' > 1, we always retry.
|
37
|
+
config.add_setting :exceptions_to_retry, default: []
|
38
|
+
|
39
|
+
# Callback between retries
|
40
|
+
config.add_setting :retry_callback, default: nil
|
41
|
+
|
42
|
+
config.around :each, &:run_with_retry
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :context, :ex
|
47
|
+
|
48
|
+
def initialize(example, opts = {})
|
49
|
+
@ex = example
|
50
|
+
@ex.metadata.merge!(opts)
|
51
|
+
current_example.attempts ||= 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_example
|
55
|
+
@current_example ||= RSpec.current_example
|
56
|
+
end
|
57
|
+
|
58
|
+
def retry_count # rubocop:disable Metrics/AbcSize
|
59
|
+
if retry_on_success?
|
60
|
+
RSpec.configuration.retry_on_success_count
|
61
|
+
elsif retry_on_failure?
|
62
|
+
RSpec.configuration.retry_on_failure_count
|
63
|
+
else
|
64
|
+
[
|
65
|
+
(
|
66
|
+
ENV['RSPEC_FORTIFY_RETRY_COUNT'] ||
|
67
|
+
ex.metadata[:retry] ||
|
68
|
+
RSpec.configuration.retry_count_condition.call(ex) ||
|
69
|
+
RSpec.configuration.default_retry_count
|
70
|
+
).to_i,
|
71
|
+
1,
|
72
|
+
].max
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def attempts
|
77
|
+
current_example.attempts ||= 0
|
78
|
+
end
|
79
|
+
|
80
|
+
def attempts=(val)
|
81
|
+
current_example.attempts = val
|
82
|
+
end
|
83
|
+
|
84
|
+
def clear_lets
|
85
|
+
if ex.metadata[:clear_lets_on_failure].nil?
|
86
|
+
RSpec.configuration.clear_lets_on_failure
|
87
|
+
else
|
88
|
+
ex.metadata[:clear_lets_on_failure]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def sleep_interval
|
93
|
+
if ex.metadata[:exponential_backoff]
|
94
|
+
(2**(current_example.attempts - 1)) * ex.metadata[:retry_wait]
|
95
|
+
else
|
96
|
+
ex.metadata[:retry_wait] ||
|
97
|
+
RSpec.configuration.default_sleep_interval
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def exceptions_to_hard_fail
|
102
|
+
ex.metadata[:exceptions_to_hard_fail] ||
|
103
|
+
RSpec.configuration.exceptions_to_hard_fail
|
104
|
+
end
|
105
|
+
|
106
|
+
def exceptions_to_retry
|
107
|
+
ex.metadata[:exceptions_to_retry] ||
|
108
|
+
RSpec.configuration.exceptions_to_retry
|
109
|
+
end
|
110
|
+
|
111
|
+
def verbose_retry?
|
112
|
+
RSpec.configuration.verbose_retry?
|
113
|
+
end
|
114
|
+
|
115
|
+
def display_try_failure_messages?
|
116
|
+
RSpec.configuration.display_try_failure_messages?
|
117
|
+
end
|
118
|
+
|
119
|
+
def run # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
120
|
+
loop do
|
121
|
+
RSpec.configuration.formatters.each { |f| f.retry(ex) if f.respond_to? :retry } if attempts.positive?
|
122
|
+
|
123
|
+
if verbose_retry? && (log_first_attempt? || attempts.positive?)
|
124
|
+
message = "RSpec::Fortify: #{ordinalize(attempts + 1)} try #{ex.location}"
|
125
|
+
message = "\n#{message}" if attempts == 1
|
126
|
+
RSpec.configuration.reporter.message(message)
|
127
|
+
end
|
128
|
+
|
129
|
+
ex.metadata[:retry_attempts] = attempts
|
130
|
+
ex.metadata[:retry_exceptions] ||= []
|
131
|
+
|
132
|
+
current_example.clear_exception
|
133
|
+
ex.run
|
134
|
+
|
135
|
+
self.attempts += 1
|
136
|
+
|
137
|
+
break unless should_retry?
|
138
|
+
|
139
|
+
ex.metadata[:retry_exceptions] << ex.exception if ex.exception
|
140
|
+
|
141
|
+
break if attempts >= retry_count
|
142
|
+
|
143
|
+
if exceptions_to_hard_fail.any? && ex.exception && exception_exists_in?(exceptions_to_hard_fail, ex.exception)
|
144
|
+
break
|
145
|
+
end
|
146
|
+
|
147
|
+
break if exceptions_to_retry.any? && ex.exception && !exception_exists_in?(exceptions_to_retry, ex.exception)
|
148
|
+
|
149
|
+
if verbose_retry? && display_try_failure_messages? && retry_on_failure? && (attempts != retry_count)
|
150
|
+
exception_strings =
|
151
|
+
if ex.exception.is_a?(::RSpec::Core::MultipleExceptionError::InterfaceTag)
|
152
|
+
ex.exception.all_exceptions.map(&:to_s)
|
153
|
+
else
|
154
|
+
[ex.exception.to_s]
|
155
|
+
end
|
156
|
+
|
157
|
+
try_message = "\n#{ordinalize(attempts)} Try error in #{ex.location}:\n#{exception_strings.join "\n"}\n"
|
158
|
+
RSpec.configuration.reporter.message(try_message)
|
159
|
+
end
|
160
|
+
|
161
|
+
ex.example_group_instance.clear_lets if clear_lets
|
162
|
+
|
163
|
+
if RSpec.configuration.retry_callback
|
164
|
+
ex.ex_group_instance.instance_exec(ex, &RSpec.configuration.retry_callback)
|
165
|
+
end
|
166
|
+
|
167
|
+
sleep sleep_interval if sleep_interval.to_f.positive?
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# borrowed from ActiveSupport::Inflector
|
174
|
+
def ordinalize(number)
|
175
|
+
if (11..13).cover?(number.to_i % 100)
|
176
|
+
"#{number}th"
|
177
|
+
else
|
178
|
+
case number.to_i % 10
|
179
|
+
when 1 then "#{number}st"
|
180
|
+
when 2 then "#{number}nd"
|
181
|
+
when 3 then "#{number}rd"
|
182
|
+
else "#{number}th"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def exception_exists_in?(list, exception)
|
188
|
+
list.any? do |exception_klass|
|
189
|
+
exception.is_a?(exception_klass) || exception_klass === exception # rubocop:disable Style/CaseEquality
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def log_first_attempt?
|
194
|
+
cast_to_boolean(ENV.fetch('RSPEC_FORTIFY_LOG_FIRST_ATTEMPT', 'false'))
|
195
|
+
end
|
196
|
+
|
197
|
+
def retry_on_failure?
|
198
|
+
RSpec.configuration.retry_on_failure
|
199
|
+
end
|
200
|
+
|
201
|
+
def retry_on_success?
|
202
|
+
RSpec.configuration.retry_on_success && current_example_changed?
|
203
|
+
end
|
204
|
+
|
205
|
+
def current_example_changed?
|
206
|
+
# Added or modified specs can be passed in via this
|
207
|
+
# CHANGED_SPECS env var using some version of git diff like:
|
208
|
+
# git diff origin/main...HEAD --name-only --relative --diff-filter=AM | grep '_spec.rb$' | tr '\n' ',' | sed 's/,$//'
|
209
|
+
changed_specs = ENV.fetch('CHANGED_SPECS', nil)&.split(',') || []
|
210
|
+
changed_specs.include?(ex.file_path.sub(%r{^\./}, ''))
|
211
|
+
end
|
212
|
+
|
213
|
+
def current_attempt_failed?
|
214
|
+
!ex.exception.nil?
|
215
|
+
end
|
216
|
+
|
217
|
+
def current_attempt_succeeded?
|
218
|
+
ex.exception.nil?
|
219
|
+
end
|
220
|
+
|
221
|
+
def should_retry?
|
222
|
+
if retry_on_success?
|
223
|
+
current_attempt_succeeded?
|
224
|
+
elsif retry_on_failure?
|
225
|
+
current_attempt_failed?
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def cast_to_boolean(value)
|
232
|
+
if value.nil? || %w(false f 0 no n).include?(value.to_s.downcase) # rubocop:disable Style/IfWithBooleanLiteralBranches
|
233
|
+
false
|
234
|
+
else
|
235
|
+
true
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def ci?
|
240
|
+
cast_to_boolean(ENV.fetch('CI', 'false'))
|
241
|
+
end
|
242
|
+
|
243
|
+
def pr?
|
244
|
+
ci? && cast_to_boolean(ENV.fetch('CIRCLE_PULL_REQUEST', 'false'))
|
245
|
+
end
|
246
|
+
|
247
|
+
def main?
|
248
|
+
ci? && !pr?
|
249
|
+
end
|
250
|
+
|
251
|
+
def changed_specs
|
252
|
+
ENV.fetch('CHANGED_SPECS', nil)&.split(',') || []
|
253
|
+
end
|
254
|
+
|
255
|
+
RSpec::Fortify.setup
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
class Example
|
6
|
+
attr_accessor :attempts
|
7
|
+
|
8
|
+
def clear_exception
|
9
|
+
@exception = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
class Procsy
|
13
|
+
def run_with_retry(opts = {})
|
14
|
+
RSpec::Fortify.new(self, opts).run
|
15
|
+
end
|
16
|
+
|
17
|
+
def attempts
|
18
|
+
@example.attempts
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module RSpec
|
26
|
+
module Core
|
27
|
+
class ExampleGroup
|
28
|
+
def clear_memoized
|
29
|
+
if respond_to? :__init_memoized, true
|
30
|
+
__init_memoized
|
31
|
+
else
|
32
|
+
@__memoized = nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear_lets
|
37
|
+
clear_memoized
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require File.expand_path('lib/rspec/fortify/version', __dir__)
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.authors = ['Yusuke Mito', 'Michael Glass', 'Devin Burnette']
|
8
|
+
spec.email = ['devin@betterment.com']
|
9
|
+
spec.description = 'retry intermittently failing rspec examples'
|
10
|
+
spec.summary = 'retry intermittently failing rspec examples'
|
11
|
+
spec.homepage = 'https://github.com/Betterment/rspec-fortify'
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
16
|
+
spec.executables = []
|
17
|
+
spec.name = 'rspec-fortify'
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
spec.version = RSpec::Fortify::VERSION
|
20
|
+
spec.required_ruby_version = '>= 3.0'
|
21
|
+
spec.add_runtime_dependency 'rspec-core', '>3.9'
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Flaky Test Example' do # rubocop:disable RSpec/FilePath
|
6
|
+
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
7
|
+
$try_counter = 0 # rubocop:disable Style/GlobalVars
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'fails on the third try' do
|
11
|
+
$try_counter += 1 # rubocop:disable Style/GlobalVars
|
12
|
+
|
13
|
+
expect($try_counter).not_to eq(3), 'Intentionally failing on the third try' # rubocop:disable Style/GlobalVars
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'tty-command'
|
5
|
+
|
6
|
+
describe RSpec::Fortify do
|
7
|
+
let(:cmd) { TTY::Command.new printer: :null }
|
8
|
+
|
9
|
+
describe '#run' do
|
10
|
+
context 'when main' do
|
11
|
+
context 'when example changed' do
|
12
|
+
let(:env) do
|
13
|
+
{
|
14
|
+
'CHANGED_SPECS' => 'spec/fixtures/flaky_test.rb,spec/fixtures/good_test.rb,spec/fixtures/bad_test.rb',
|
15
|
+
'CI' => 'true',
|
16
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'retries bad test examples the configured failed retry amount' do
|
21
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
22
|
+
expect(out).to include('2nd try')
|
23
|
+
expect(out).not_to include('10th try')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'runs good test examples once' do
|
27
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
28
|
+
expect(out).to include('1st try')
|
29
|
+
expect(out).not_to include('2nd try')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'runs flaky test examples once' do
|
33
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
34
|
+
expect(out).to include('1st try')
|
35
|
+
expect(out).not_to include('2nd try')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when example did not change' do
|
40
|
+
let(:env) do
|
41
|
+
{
|
42
|
+
'CI' => 'true',
|
43
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'retries bad test examples the configured failed retry amount' do
|
48
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
49
|
+
expect(out).to include('2nd try')
|
50
|
+
expect(out).not_to include('10th try')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'runs good test examples once' do
|
54
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
55
|
+
expect(out).to include('1st try')
|
56
|
+
expect(out).not_to include('2nd try')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'runs flaky test examples once' do
|
60
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
61
|
+
expect(out).to include('1st try')
|
62
|
+
expect(out).not_to include('2nd try')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when pr' do
|
68
|
+
context 'when example changed' do
|
69
|
+
context 'when too many changed specs' do
|
70
|
+
let(:env) do
|
71
|
+
{
|
72
|
+
'CHANGED_SPECS' => 'spec/fixtures/flaky_test.rb,spec/fixtures/good_test.rb,spec/fixtures/bad_test.rb,spec/fixtures/fake_test1.rb,spec/fixtures/fake_test2.rb,spec/fixtures/fake_test3.rb,spec/fixtures/fake_test4.rb,spec/fixtures/fake_test5.rb,spec/fixtures/fake_test6.rb,spec/fixtures/fake_test7.rb,spec/fixtures/fake_test8.rb,spec/fixtures/fake_test9.rb,spec/fixtures/fake_test10.rb,spec/fixtures/fake_test11.rb,spec/fixtures/fake_test12.rb,spec/fixtures/fake_test13.rb,spec/fixtures/fake_test14.rb,spec/fixtures/fake_test15.rb,spec/fixtures/fake_test16.rb,spec/fixtures/fake_test17.rb,spec/fixtures/fake_test18.rb,spec/fixtures/fake_test19.rb,spec/fixtures/fake_test20.rb,spec/fixtures/fake_test21.rb,spec/fixtures/fake_test22.rb,spec/fixtures/fake_test23.rb,spec/fixtures/fake_test24.rb,spec/fixtures/fake_test25.rb,spec/fixtures/fake_test26.rb,spec/fixtures/fake_test27.rb,spec/fixtures/fake_test28.rb,spec/fixtures/fake_test29.rb,spec/fixtures/fake_test30.rb', # rubocop:disable Layout/LineLength
|
73
|
+
'CI' => 'true',
|
74
|
+
'CIRCLE_PULL_REQUEST' => 'https://github.com/foo/bar/pull/123',
|
75
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'retries bad test examples the configured failed retry amount' do
|
80
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
81
|
+
expect(out).to include('2nd try')
|
82
|
+
expect(out).not_to include('10th try')
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'runs good test examples once' do
|
86
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
87
|
+
expect(out).to include('1st try')
|
88
|
+
expect(out).not_to include('2nd try')
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'runs flaky test examples once' do
|
92
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
93
|
+
expect(out).to include('1st try')
|
94
|
+
expect(out).not_to include('2nd try')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when not too many changed specs' do
|
99
|
+
let(:env) do
|
100
|
+
{
|
101
|
+
'CHANGED_SPECS' => 'spec/fixtures/flaky_test.rb,spec/fixtures/good_test.rb,spec/fixtures/bad_test.rb',
|
102
|
+
'CI' => 'true',
|
103
|
+
'CIRCLE_PULL_REQUEST' => 'https://github.com/foo/bar/pull/123',
|
104
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'retries bad test examples the configured failed retry amount' do
|
109
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
110
|
+
expect(out).to include('1st try')
|
111
|
+
expect(out).not_to include('2nd try')
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'retries good test examples the configured success retry amount' do
|
115
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
116
|
+
expect(out).to include('10th try')
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'retries flaky test examples until they flake' do
|
120
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
121
|
+
expect(out).to include('2nd try')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'when example did not change' do
|
127
|
+
let(:env) do
|
128
|
+
{
|
129
|
+
'CI' => 'true',
|
130
|
+
'CIRCLE_PULL_REQUEST' => 'https://github.com/foo/bar/pull/123',
|
131
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'retries bad test examples the configured failed retry amount' do
|
136
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
137
|
+
expect(out).to include('2nd try')
|
138
|
+
expect(out).not_to include('10th try')
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'runs good test examples once' do
|
142
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
143
|
+
expect(out).to include('1st try')
|
144
|
+
expect(out).not_to include('2nd try')
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'runs flaky test examples once' do
|
148
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
149
|
+
expect(out).to include('1st try')
|
150
|
+
expect(out).not_to include('2nd try')
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when local dev' do
|
156
|
+
context 'when example changed' do
|
157
|
+
let(:env) do
|
158
|
+
{
|
159
|
+
'CHANGED_SPECS' => 'spec/fixtures/flaky_test.rb,spec/fixtures/good_test.rb,spec/fixtures/bad_test.rb',
|
160
|
+
'CI' => 'false',
|
161
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'runs bad test examples once' do
|
166
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
167
|
+
expect(out).to include('1st try')
|
168
|
+
expect(out).not_to include('2nd try')
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'runs good test examples once' do
|
172
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
173
|
+
expect(out).to include('1st try')
|
174
|
+
expect(out).not_to include('2nd try')
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'runs flaky test examples once' do
|
178
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
179
|
+
expect(out).to include('1st try')
|
180
|
+
expect(out).not_to include('2nd try')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'when example did not change' do
|
185
|
+
let(:env) do
|
186
|
+
{
|
187
|
+
'CI' => 'false',
|
188
|
+
'RSPEC_FORTIFY_LOG_FIRST_ATTEMPT' => 'true',
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'runs bad test examples once' do
|
193
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/bad_test.rb', env: env)
|
194
|
+
expect(out).to include('1st try')
|
195
|
+
expect(out).not_to include('2nd try')
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'runs good test examples once' do
|
199
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/good_test.rb', env: env)
|
200
|
+
expect(out).to include('1st try')
|
201
|
+
expect(out).not_to include('2nd try')
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'runs flaky test examples once' do
|
205
|
+
out, _err = cmd.run!('bundle exec rspec spec/fixtures/flaky_test.rb', env: env)
|
206
|
+
expect(out).to include('1st try')
|
207
|
+
expect(out).not_to include('2nd try')
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-fortify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yusuke Mito
|
8
|
+
- Michael Glass
|
9
|
+
- Devin Burnette
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2025-06-12 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec-core
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.9'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '3.9'
|
29
|
+
description: retry intermittently failing rspec examples
|
30
|
+
email:
|
31
|
+
- devin@betterment.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".github/workflows/ci.yml"
|
37
|
+
- ".github/workflows/linter.yml"
|
38
|
+
- ".gitignore"
|
39
|
+
- ".rubocop.yml"
|
40
|
+
- ".ruby-version"
|
41
|
+
- ".travis.yml"
|
42
|
+
- Appraisals
|
43
|
+
- Gemfile
|
44
|
+
- Guardfile
|
45
|
+
- LICENSE
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- changelog.md
|
49
|
+
- gemfiles/rspec_3.10.gemfile
|
50
|
+
- gemfiles/rspec_3.10.gemfile.lock
|
51
|
+
- gemfiles/rspec_3.11.gemfile
|
52
|
+
- gemfiles/rspec_3.11.gemfile.lock
|
53
|
+
- gemfiles/rspec_3.12.gemfile
|
54
|
+
- gemfiles/rspec_3.12.gemfile.lock
|
55
|
+
- gemfiles/rspec_3.13.gemfile
|
56
|
+
- gemfiles/rspec_3.13.gemfile.lock
|
57
|
+
- gemfiles/rspec_3.9.gemfile
|
58
|
+
- gemfiles/rspec_3.9.gemfile.lock
|
59
|
+
- lib/rspec/fortify.rb
|
60
|
+
- lib/rspec/fortify/formatter.rb
|
61
|
+
- lib/rspec/fortify/version.rb
|
62
|
+
- lib/rspec_ext/rspec_ext.rb
|
63
|
+
- rspec-fortify.gemspec
|
64
|
+
- spec/fixtures/bad_test.rb
|
65
|
+
- spec/fixtures/flaky_test.rb
|
66
|
+
- spec/fixtures/good_test.rb
|
67
|
+
- spec/lib/rspec/fortify_spec.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
homepage: https://github.com/Betterment/rspec-fortify
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata:
|
73
|
+
rubygems_mfa_required: 'true'
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 1.3.1
|
88
|
+
requirements: []
|
89
|
+
rubygems_version: 3.3.26
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: retry intermittently failing rspec examples
|
93
|
+
test_files: []
|