rspec_trunk_flaky_tests 0.15.6.pre.beta.1-aarch64-linux-gnu → 0.16.0-aarch64-linux-gnu
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 +4 -4
- data/lib/rspec_trunk_flaky_tests/3.1/rspec_trunk_flaky_tests.so +0 -0
- data/lib/rspec_trunk_flaky_tests/3.2/rspec_trunk_flaky_tests.so +0 -0
- data/lib/rspec_trunk_flaky_tests/3.3/rspec_trunk_flaky_tests.so +0 -0
- data/lib/rspec_trunk_flaky_tests/3.4/rspec_trunk_flaky_tests.so +0 -0
- data/lib/rspec_trunk_flaky_tests/4.0/rspec_trunk_flaky_tests.so +0 -0
- data/lib/trunk_spec_helper.rb +364 -333
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a822bb3a51befb00f6c170e316f50b9b07ca0ecca4e21c0bb47a6d8cc5d62ed
|
|
4
|
+
data.tar.gz: 55cb46a8d3d67266cbca9dd56c3eab85485d942456af8c9ed424cc85976032ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0a9205ac3661dca82a6941def29ace6c9d5920fbdef9ed2b462fa16e781f6e6df9fa92f7c75f7ba99783f891d287b939722a126fdeb1b0ea0fc88f8ed2ecfcd
|
|
7
|
+
data.tar.gz: d8cd0b2090fb858859fd29c876ed15ce41136fe2eb468c70b26a3b0b40fd7f20a09ea2f61bda11e9f86da306d83fe4e4eb8d5c9ce08d736343e7c6859d526775
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/trunk_spec_helper.rb
CHANGED
|
@@ -37,407 +37,438 @@ require 'rspec/core/formatters/exception_presenter'
|
|
|
37
37
|
require 'time'
|
|
38
38
|
require 'rspec_trunk_flaky_tests'
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
module RSpec
|
|
41
|
+
# Trunk holds everything this gem defines (the native extension defines the
|
|
42
|
+
# module and its classes: TestReport, Status, CIInfo, ...). Nothing is added to
|
|
43
|
+
# the global namespace or to core classes; test/namespace_spec.rb enforces that.
|
|
44
|
+
module Trunk
|
|
45
|
+
ANSI_ESCAPE_PATTERN = %r{(?:\e[@-Z\\-_]|\e\[[0-?]*[ -/]*[@-~])}
|
|
46
|
+
|
|
47
|
+
# Knapsack example detector instantiates all test cases in order to determine how to shard them
|
|
48
|
+
# These instantiations should not generate test bundles, so we
|
|
49
|
+
# disable the gem when running under knapsack_pro:rspec_test_example_detector
|
|
50
|
+
KNAPSACK_DETECTOR_COMMANDS = %w[
|
|
51
|
+
knapsack_pro:rspec_test_example_detector
|
|
52
|
+
knapsack_pro:queue:rspec:initialize
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
# Example metadata keys this gem sets, each mapped to the unprefixed name it
|
|
56
|
+
# used to be set under. The old names are deprecated: they are still written
|
|
57
|
+
# (see .write_metadata) so existing readers keep working, but will be removed
|
|
58
|
+
# in a future release.
|
|
59
|
+
DEPRECATED_METADATA_KEYS = {
|
|
60
|
+
trunk_quarantined_exception: :quarantined_exception,
|
|
61
|
+
trunk_attempt_number: :attempt_number,
|
|
62
|
+
trunk_is_description_generated: :is_description_generated
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
# The state for this RSpec process, or nil when Trunk is disabled.
|
|
67
|
+
attr_reader :current_run
|
|
68
|
+
|
|
69
|
+
def setup
|
|
70
|
+
return if current_run || disabled?
|
|
71
|
+
|
|
72
|
+
run = @current_run = Run.new
|
|
73
|
+
RSpec::Core::Example.prepend(ExampleExtension)
|
|
74
|
+
RSpec.configure do |config|
|
|
75
|
+
config.before(:example) do
|
|
76
|
+
skip('Quarantine lookup failed, skipping test run') if run.abort_remaining?
|
|
77
|
+
end
|
|
78
|
+
config.around(:each) { |example| Trunk.run_counting_attempts(example) }
|
|
79
|
+
config.reporter.register_listener AnalyticsListener.new(run), :example_finished, :close
|
|
80
|
+
end
|
|
81
|
+
end
|
|
41
82
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"\e[#{color_code}m#{self}\e[0m"
|
|
47
|
-
end
|
|
83
|
+
def disabled?
|
|
84
|
+
knapsack_detector_mode? || ENV['DISABLE_RSPEC_TRUNK_FLAKY_TESTS'] == 'true' ||
|
|
85
|
+
ENV['TRUNK_ORG_URL_SLUG'].nil? || ENV['TRUNK_API_TOKEN'].nil?
|
|
86
|
+
end
|
|
48
87
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
88
|
+
def knapsack_detector_mode?
|
|
89
|
+
KNAPSACK_DETECTOR_COMMANDS.any? { |command| command_line.include?(command) }
|
|
90
|
+
end
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
def quarantine_query_failure_exit?
|
|
93
|
+
ENV['TRUNK_QUARANTINE_QUERY_FAILURE_EXIT'] == 'true'
|
|
94
|
+
end
|
|
56
95
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
96
|
+
def command_line
|
|
97
|
+
"#{$PROGRAM_NAME} #{ARGV.join(' ')}"
|
|
98
|
+
end
|
|
61
99
|
|
|
62
|
-
|
|
100
|
+
def run_counting_attempts(example)
|
|
101
|
+
example.run
|
|
102
|
+
# monitor attempts in the metadata
|
|
103
|
+
attempt_number = example.metadata[:trunk_attempt_number]
|
|
104
|
+
write_metadata(example.metadata, :trunk_attempt_number, attempt_number ? attempt_number + 1 : 0)
|
|
105
|
+
end
|
|
63
106
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
107
|
+
# Sets a trunk_ metadata key, and its deprecated unprefixed name too.
|
|
108
|
+
def write_metadata(metadata, key, value)
|
|
109
|
+
metadata[key] = value
|
|
110
|
+
deprecated_key = DEPRECATED_METADATA_KEYS[key]
|
|
111
|
+
metadata[deprecated_key] = value if deprecated_key
|
|
112
|
+
end
|
|
67
113
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
end
|
|
114
|
+
def escape(str)
|
|
115
|
+
str.dump[1..-2]
|
|
116
|
+
end
|
|
71
117
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def knapsack_detector_mode?
|
|
76
|
-
knapsack_detector_command?
|
|
77
|
-
end
|
|
118
|
+
def strip_ansi_codes(text)
|
|
119
|
+
text.to_s.gsub(ANSI_ESCAPE_PATTERN, '')
|
|
120
|
+
end
|
|
78
121
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
end
|
|
122
|
+
# The escaped file path and the dotted classname derived from it.
|
|
123
|
+
def file_and_classname(example)
|
|
124
|
+
file = escape(example.metadata[:file_path])
|
|
125
|
+
[file, file.sub(%r{\.[^/.]+\Z}, '').gsub('/', '.').gsub(/\A\.+|\.+\Z/, '')]
|
|
126
|
+
end
|
|
84
127
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
128
|
+
def parent_name(example)
|
|
129
|
+
name = example.example_group.metadata[:description]
|
|
130
|
+
name.empty? ? 'rspec' : name
|
|
131
|
+
end
|
|
132
|
+
end
|
|
89
133
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
134
|
+
# ANSI colors for the gem's console output, without patching String.
|
|
135
|
+
module Colors
|
|
136
|
+
module_function
|
|
93
137
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
$failure_encountered_and_quarantine_lookup_failed = false
|
|
138
|
+
def colorize(text, color_code)
|
|
139
|
+
"\e[#{color_code}m#{text}\e[0m"
|
|
140
|
+
end
|
|
98
141
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class Example
|
|
103
|
-
# keep the original method around so we can call it
|
|
104
|
-
alias set_exception_core set_exception
|
|
105
|
-
alias assign_generated_description_core assign_generated_description
|
|
106
|
-
# RSpec uses the existance of an exception to determine if the test failed
|
|
107
|
-
# We need to override this to allow us to capture the exception and then
|
|
108
|
-
# decide if we want to fail the test or not
|
|
109
|
-
# trunk-ignore(rubocop/Naming/AccessorMethodName)
|
|
110
|
-
def set_exception(exception)
|
|
111
|
-
# Pending stays green, except a fixed pending example is a real failure.
|
|
112
|
-
return set_exception_core(exception) if metadata[:pending] && !pending_example_fixed?(exception)
|
|
113
|
-
return set_exception_core(exception) if trunk_disabled
|
|
114
|
-
return set_exception_core(exception) if metadata[:retry_attempts]&.positive?
|
|
142
|
+
def red(text)
|
|
143
|
+
colorize(text, 31)
|
|
144
|
+
end
|
|
115
145
|
|
|
116
|
-
|
|
146
|
+
def green(text)
|
|
147
|
+
colorize(text, 32)
|
|
117
148
|
end
|
|
118
149
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
handle_quarantine_check(exception)
|
|
122
|
-
rescue StandardError => e
|
|
123
|
-
puts "Quarantine check errored (#{e.class}: #{e.message}), treating test as not quarantined".yellow
|
|
124
|
-
set_exception_core(exception)
|
|
150
|
+
def yellow(text)
|
|
151
|
+
colorize(text, 33)
|
|
125
152
|
end
|
|
153
|
+
end
|
|
126
154
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
155
|
+
# Run is the state shared by every example in this RSpec process: the test
|
|
156
|
+
# report (cached in memory so we can add to it as we go and reduce the number
|
|
157
|
+
# of API calls), and whether quarantining turned out to be unavailable, so
|
|
158
|
+
# that's only announced once.
|
|
159
|
+
class Run
|
|
160
|
+
attr_reader :report
|
|
161
|
+
|
|
162
|
+
def initialize(report = TestReport.new('rspec', Trunk.command_line, nil))
|
|
163
|
+
@report = report
|
|
164
|
+
@quarantining_disabled = false
|
|
165
|
+
@lookup_failed = false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def quarantining_disabled?
|
|
169
|
+
@quarantining_disabled
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def lookup_failed?
|
|
173
|
+
@lookup_failed
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def abort_remaining?
|
|
177
|
+
lookup_failed? && Trunk.quarantine_query_failure_exit?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Whether a failure of `example` with `exception` is quarantined. If the
|
|
181
|
+
# quarantine machinery itself blows up, the failure must stand.
|
|
182
|
+
def quarantine?(example, exception)
|
|
183
|
+
check_quarantine(example, exception)
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
puts Colors.yellow("Quarantine check errored (#{e.class}: #{e.message}), treating test as not quarantined")
|
|
186
|
+
false
|
|
130
187
|
end
|
|
131
188
|
|
|
189
|
+
private
|
|
190
|
+
|
|
132
191
|
# trunk-ignore(rubocop/Metrics/AbcSize,rubocop/Metrics/MethodLength,rubocop/Metrics/CyclomaticComplexity)
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
parent_name = parent_name.empty? ? 'rspec' : parent_name
|
|
138
|
-
file = escape(metadata[:file_path])
|
|
139
|
-
classname = file.sub(%r{\.[^/.]+\Z}, '').gsub('/', '.').gsub(/\A\.+|\.+\Z/, '')
|
|
140
|
-
unless $failure_encountered_and_quarantining_disabled
|
|
141
|
-
puts "Test failed, checking if it can be quarantined: `#{location}`".yellow
|
|
192
|
+
def check_quarantine(example, exception)
|
|
193
|
+
file, classname = Trunk.file_and_classname(example)
|
|
194
|
+
unless quarantining_disabled?
|
|
195
|
+
puts Colors.yellow("Test failed, checking if it can be quarantined: `#{example.location}`")
|
|
142
196
|
end
|
|
143
|
-
|
|
197
|
+
result = report.is_quarantined(example.trunk_id, example.full_description, Trunk.parent_name(example),
|
|
198
|
+
classname, file)
|
|
144
199
|
|
|
145
|
-
if
|
|
146
|
-
unless
|
|
147
|
-
puts 'Failed to check quarantining status, no failures will be quarantined'
|
|
148
|
-
|
|
200
|
+
if result.quarantine_lookup_failed
|
|
201
|
+
unless lookup_failed?
|
|
202
|
+
puts Colors.yellow('Failed to check quarantining status, no failures will be quarantined')
|
|
203
|
+
@lookup_failed = true
|
|
149
204
|
end
|
|
150
|
-
if quarantine_query_failure_exit?
|
|
151
|
-
puts 'Quarantine lookup failed, exiting early'
|
|
205
|
+
if Trunk.quarantine_query_failure_exit?
|
|
206
|
+
puts Colors.red('Quarantine lookup failed, exiting early')
|
|
152
207
|
RSpec.world.wants_to_quit = true
|
|
153
208
|
end
|
|
154
|
-
|
|
155
|
-
elsif
|
|
156
|
-
unless
|
|
157
|
-
puts 'Quarantining is disabled for this repo, no failures will be quarantined'
|
|
158
|
-
|
|
209
|
+
false
|
|
210
|
+
elsif result.quarantining_disabled_for_repo
|
|
211
|
+
unless quarantining_disabled?
|
|
212
|
+
puts Colors.yellow('Quarantining is disabled for this repo, no failures will be quarantined')
|
|
213
|
+
@quarantining_disabled = true
|
|
159
214
|
end
|
|
160
|
-
|
|
161
|
-
elsif
|
|
215
|
+
false
|
|
216
|
+
elsif result.test_is_quarantined
|
|
162
217
|
# monitor the override in the metadata
|
|
163
|
-
metadata
|
|
164
|
-
puts "Test is quarantined, overriding exception: #{exception}"
|
|
165
|
-
|
|
218
|
+
Trunk.write_metadata(example.metadata, :trunk_quarantined_exception, exception)
|
|
219
|
+
puts Colors.green("Test is quarantined, overriding exception: #{exception}")
|
|
220
|
+
true
|
|
166
221
|
else
|
|
167
|
-
puts 'Test is not quarantined, continuing'
|
|
168
|
-
|
|
222
|
+
puts Colors.red('Test is not quarantined, continuing')
|
|
223
|
+
false
|
|
169
224
|
end
|
|
170
225
|
end
|
|
226
|
+
end
|
|
171
227
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def
|
|
178
|
-
return
|
|
228
|
+
# Prepended to RSpec::Core::Example. RSpec uses the existence of an exception
|
|
229
|
+
# to determine if the test failed, so #set_exception is where we capture the
|
|
230
|
+
# exception and decide whether to fail the test or not.
|
|
231
|
+
module ExampleExtension
|
|
232
|
+
# trunk-ignore(rubocop/Naming/AccessorMethodName)
|
|
233
|
+
def set_exception(exception)
|
|
234
|
+
return super unless quarantine_candidate?(exception)
|
|
235
|
+
return super unless Trunk.current_run&.quarantine?(self, exception)
|
|
179
236
|
|
|
180
|
-
|
|
237
|
+
nil
|
|
181
238
|
end
|
|
182
239
|
|
|
183
|
-
def
|
|
184
|
-
|
|
240
|
+
def trunk_id
|
|
241
|
+
"trunk:#{id}-#{location}" if trunk_description_generated?
|
|
185
242
|
end
|
|
186
243
|
|
|
187
|
-
|
|
188
|
-
class Procsy
|
|
189
|
-
def run_with_trunk
|
|
190
|
-
RSpec::Trunk.new(self).run
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
244
|
+
private
|
|
196
245
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def self.setup
|
|
201
|
-
RSpec.configure do |config|
|
|
202
|
-
if trunk_disabled
|
|
203
|
-
config.around(:each, &:run)
|
|
204
|
-
else
|
|
205
|
-
config.around(:each, &:run_with_trunk)
|
|
206
|
-
end
|
|
246
|
+
def assign_generated_description
|
|
247
|
+
Trunk.write_metadata(metadata, :trunk_is_description_generated, trunk_description_generated?)
|
|
248
|
+
super
|
|
207
249
|
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def initialize(example)
|
|
211
|
-
@example = example
|
|
212
|
-
end
|
|
213
250
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
251
|
+
def trunk_description_generated?
|
|
252
|
+
cached = metadata[:trunk_is_description_generated]
|
|
253
|
+
return cached unless cached.nil?
|
|
217
254
|
|
|
218
|
-
|
|
219
|
-
# run the test
|
|
220
|
-
@example.run
|
|
221
|
-
# monitor attempts in the metadata
|
|
222
|
-
if @example.metadata[:attempt_number]
|
|
223
|
-
@example.metadata[:attempt_number] += 1
|
|
224
|
-
else
|
|
225
|
-
@example.metadata[:attempt_number] = 0
|
|
255
|
+
description == location_description
|
|
226
256
|
end
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
257
|
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
module_function
|
|
258
|
+
def quarantine_candidate?(exception)
|
|
259
|
+
# Pending stays green, except a fixed pending example is a real failure.
|
|
260
|
+
return false if metadata[:pending] && !pending_example_fixed?(exception)
|
|
235
261
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
end
|
|
239
|
-
end
|
|
262
|
+
!metadata[:retry_attempts]&.positive?
|
|
263
|
+
end
|
|
240
264
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
265
|
+
def pending_example_fixed?(exception)
|
|
266
|
+
defined?(RSpec::Core::Pending::PendingExampleFixedError) &&
|
|
267
|
+
exception.is_a?(RSpec::Core::Pending::PendingExampleFixedError)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
246
270
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
# Formats failures for the report as plain text suitable for storage and the web UI.
|
|
272
|
+
module FailureFormatter
|
|
273
|
+
# A no-op colorizer passed to RSpec's ExceptionPresenter.
|
|
274
|
+
module PlainColorizer
|
|
275
|
+
module_function
|
|
252
276
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
277
|
+
def wrap(text, _code_or_symbol)
|
|
278
|
+
text
|
|
279
|
+
end
|
|
280
|
+
end
|
|
256
281
|
|
|
257
|
-
|
|
282
|
+
module_function
|
|
258
283
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
lines.concat(exception_backtrace_lines(cause, example))
|
|
265
|
-
cause = cause.cause
|
|
266
|
-
depth += 1
|
|
267
|
-
end
|
|
284
|
+
# Defer to RSpec's own ExceptionPresenter so the failure_message field matches
|
|
285
|
+
# what users see in their RSpec console output (Failure/Error: <source line>,
|
|
286
|
+
# the exception class and message, and any "Caused by:" chain).
|
|
287
|
+
def message(exception, example)
|
|
288
|
+
return '' unless exception
|
|
268
289
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
290
|
+
presenter = RSpec::Core::Formatters::ExceptionPresenter.new(exception, example)
|
|
291
|
+
Trunk.strip_ansi_codes(presenter.fully_formatted(nil, PlainColorizer))
|
|
292
|
+
rescue StandardError
|
|
293
|
+
legacy_message(exception)
|
|
294
|
+
end
|
|
273
295
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
end
|
|
296
|
+
# trunk-ignore(rubocop/Metrics/MethodLength,rubocop/Metrics/AbcSize)
|
|
297
|
+
def backtrace(exception, example)
|
|
298
|
+
return '' unless exception
|
|
278
299
|
|
|
279
|
-
|
|
280
|
-
presenter = RSpec::Core::Formatters::ExceptionPresenter.new(exception, example)
|
|
281
|
-
Array(presenter.formatted_backtrace)
|
|
282
|
-
rescue StandardError
|
|
283
|
-
Array(exception.backtrace)
|
|
284
|
-
end
|
|
300
|
+
lines = backtrace_lines(exception, example)
|
|
285
301
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
end
|
|
302
|
+
cause = exception.cause
|
|
303
|
+
depth = 0
|
|
304
|
+
while cause && depth < 10
|
|
305
|
+
lines << ''
|
|
306
|
+
lines << "Caused by: #{cause.class}: #{cause.message}"
|
|
307
|
+
lines.concat(backtrace_lines(cause, example))
|
|
308
|
+
cause = cause.cause
|
|
309
|
+
depth += 1
|
|
310
|
+
end
|
|
295
311
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
strip_ansi_codes(exception.all_exceptions.map do |e|
|
|
301
|
-
if e.backtrace && !e.backtrace.empty?
|
|
302
|
-
"#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
303
|
-
else
|
|
304
|
-
"#{e.class}: #{e.message}"
|
|
305
|
-
end
|
|
306
|
-
end.join("\n\n"))
|
|
307
|
-
else
|
|
308
|
-
strip_ansi_codes(exception.backtrace&.join("\n") || '')
|
|
309
|
-
end
|
|
310
|
-
end
|
|
312
|
+
result = lines.join("\n")
|
|
313
|
+
# The exception presenter may choke on MultipleExceptionError, such as errors in before
|
|
314
|
+
# and after hooks, so we fall back to the legacy formatter
|
|
315
|
+
return legacy_backtrace(exception) if result.strip.empty?
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
317
|
+
Trunk.strip_ansi_codes(result)
|
|
318
|
+
rescue StandardError
|
|
319
|
+
legacy_backtrace(exception)
|
|
320
|
+
end
|
|
316
321
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
def backtrace_lines(exception, example)
|
|
323
|
+
presenter = RSpec::Core::Formatters::ExceptionPresenter.new(exception, example)
|
|
324
|
+
Array(presenter.formatted_backtrace)
|
|
325
|
+
rescue StandardError
|
|
326
|
+
Array(exception.backtrace)
|
|
327
|
+
end
|
|
320
328
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
329
|
+
def legacy_message(exception)
|
|
330
|
+
case exception
|
|
331
|
+
when RSpec::Core::MultipleExceptionError
|
|
332
|
+
messages = exception.all_exceptions.map { |e| "#{e.class}: #{e.message}" }
|
|
333
|
+
Trunk.strip_ansi_codes("#{exception.class}: #{messages.join(' | ')}")
|
|
334
|
+
else
|
|
335
|
+
Trunk.strip_ansi_codes(exception.to_s)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
324
338
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
339
|
+
# trunk-ignore(rubocop/Metrics/MethodLength,rubocop/Metrics/AbcSize)
|
|
340
|
+
def legacy_backtrace(exception)
|
|
341
|
+
case exception
|
|
342
|
+
when RSpec::Core::MultipleExceptionError
|
|
343
|
+
Trunk.strip_ansi_codes(exception.all_exceptions.map do |e|
|
|
344
|
+
if e.backtrace && !e.backtrace.empty?
|
|
345
|
+
"#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
346
|
+
else
|
|
347
|
+
"#{e.class}: #{e.message}"
|
|
348
|
+
end
|
|
349
|
+
end.join("\n\n"))
|
|
350
|
+
else
|
|
351
|
+
Trunk.strip_ansi_codes(exception.backtrace&.join("\n") || '')
|
|
352
|
+
end
|
|
353
|
+
end
|
|
332
354
|
end
|
|
333
355
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
356
|
+
# AnalyticsListener is an RSpec reporter listener that records each finished
|
|
357
|
+
# example in the run's test report and submits the report at the end.
|
|
358
|
+
class AnalyticsListener
|
|
359
|
+
MAX_TEXT_FIELD_SIZE = 8_000
|
|
360
|
+
|
|
361
|
+
def initialize(run)
|
|
362
|
+
@run = run
|
|
340
363
|
end
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
puts 'Flaky tests report upload complete'.green
|
|
345
|
-
else
|
|
346
|
-
puts 'Failed to publish flaky tests report'.red
|
|
364
|
+
|
|
365
|
+
def example_finished(notification)
|
|
366
|
+
add_test_case(notification.example)
|
|
347
367
|
end
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
368
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
end
|
|
360
|
-
failure_message = failure_message[0...MAX_TEXT_FIELD_SIZE] if failure_message.length > MAX_TEXT_FIELD_SIZE
|
|
361
|
-
backtrace = backtrace[0...MAX_TEXT_FIELD_SIZE] if backtrace.length > MAX_TEXT_FIELD_SIZE
|
|
362
|
-
# TODO: should we use concatenated string or alias when auto-generated description?
|
|
363
|
-
name = example.full_description
|
|
364
|
-
file = escape(example.metadata[:file_path])
|
|
365
|
-
classname = file.sub(%r{\.[^/.]+\Z}, '').gsub('/', '.').gsub(/\A\.+|\.+\Z/, '')
|
|
366
|
-
line = example.metadata[:line_number]
|
|
367
|
-
started_at = example.execution_result.started_at.to_i
|
|
368
|
-
finished_at = example.execution_result.finished_at.to_i
|
|
369
|
-
id = example.generate_trunk_id
|
|
370
|
-
|
|
371
|
-
attempt_number = example.metadata[:retry_attempts] || example.metadata[:attempt_number] || 0
|
|
372
|
-
# set the status to failure, but mark it as quarantined
|
|
373
|
-
is_quarantined = example.metadata[:quarantined_exception] ? true : false
|
|
374
|
-
parent_name = example.example_group.metadata[:description]
|
|
375
|
-
parent_name = parent_name.empty? ? 'rspec' : parent_name
|
|
376
|
-
@testreport.add_test(id, name, classname, file, parent_name, line, status, attempt_number,
|
|
377
|
-
started_at, finished_at, failure_message || '', backtrace || '', is_quarantined)
|
|
378
|
-
end
|
|
369
|
+
# trunk-ignore(rubocop/Metrics/MethodLength,rubocop/Metrics/AbcSize)
|
|
370
|
+
def close(_notification)
|
|
371
|
+
if @run.quarantining_disabled?
|
|
372
|
+
puts Colors.yellow('Note: Quarantining is disabled for this repo. Test failures were not quarantined.')
|
|
373
|
+
end
|
|
374
|
+
if @run.lookup_failed?
|
|
375
|
+
puts Colors.yellow('Note: Failed to check quarantining status. Test failures were not quarantined.')
|
|
376
|
+
end
|
|
379
377
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
# trunk-ignore(rubocop/Metrics/AbcSize,rubocop/Metrics/CyclomaticComplexity,rubocop/Metrics/MethodLength)
|
|
397
|
-
def status_and_exception(example)
|
|
398
|
-
result = example.execution_result
|
|
399
|
-
quarantined_exception = example.metadata[:quarantined_exception]
|
|
400
|
-
|
|
401
|
-
# A genuinely skipped example (`skip`/`xit`, or `skip` called from a hook or
|
|
402
|
-
# body) never runs, so it has no real pass/fail outcome. RSpec still reports
|
|
403
|
-
# it with status :pending, so detect skips explicitly -- and up front -- via
|
|
404
|
-
# metadata[:skip] before interpreting any pending pass/fail semantics below.
|
|
405
|
-
return [Status.new('skipped'), nil] if example.metadata[:skip]
|
|
406
|
-
|
|
407
|
-
case result.status
|
|
408
|
-
when :passed
|
|
409
|
-
# A quarantined failure is recorded as :passed by RSpec (see #set_exception),
|
|
410
|
-
# so report it as a failure but carry the original quarantined exception.
|
|
411
|
-
quarantined_exception ? [Status.new('failure'), quarantined_exception] : [Status.new('success'), nil]
|
|
412
|
-
when :failed
|
|
413
|
-
# Includes a pending example whose body unexpectedly passed: RSpec reports it
|
|
414
|
-
# as :failed with a PendingExampleFixedError (on example.exception) and breaks
|
|
415
|
-
# the build (the pending expectation was violated), so it is a failure here too.
|
|
416
|
-
[Status.new('failure'), example.exception || quarantined_exception]
|
|
417
|
-
when :pending
|
|
418
|
-
# A quarantined fixed-pending is left :pending but pending_fixed? -- still a
|
|
419
|
-
# failure. Otherwise the pending "should fail" expectation was met -> success.
|
|
420
|
-
if result.pending_fixed?
|
|
421
|
-
[Status.new('failure'), quarantined_exception || example.exception]
|
|
422
|
-
else
|
|
423
|
-
[Status.new('success'), nil]
|
|
424
|
-
end
|
|
425
|
-
else
|
|
426
|
-
[Status.new(result.status.to_s), example.exception || quarantined_exception]
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
end
|
|
378
|
+
if ENV['TRUNK_LOCAL_UPLOAD_DIR']
|
|
379
|
+
saved = @run.report.try_save(ENV['TRUNK_LOCAL_UPLOAD_DIR'])
|
|
380
|
+
if saved
|
|
381
|
+
puts Colors.green('Local Flaky tests report generated')
|
|
382
|
+
else
|
|
383
|
+
puts Colors.red('Failed to generate local flaky tests report')
|
|
384
|
+
end
|
|
385
|
+
else
|
|
386
|
+
published = @run.report.publish
|
|
387
|
+
if published
|
|
388
|
+
puts Colors.green('Flaky tests report upload complete')
|
|
389
|
+
else
|
|
390
|
+
puts Colors.red('Failed to publish flaky tests report')
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
430
394
|
|
|
431
|
-
|
|
432
|
-
|
|
395
|
+
# trunk-ignore(rubocop/Metrics/CyclomaticComplexity,rubocop/Metrics/AbcSize,rubocop/Metrics/MethodLength)
|
|
396
|
+
def add_test_case(example)
|
|
397
|
+
status, exception = status_and_exception(example)
|
|
398
|
+
failure_message = ''
|
|
399
|
+
backtrace = ''
|
|
400
|
+
if exception
|
|
401
|
+
failure_message = FailureFormatter.message(exception, example).strip
|
|
402
|
+
backtrace = FailureFormatter.backtrace(exception, example).strip
|
|
403
|
+
end
|
|
404
|
+
failure_message = failure_message[0...MAX_TEXT_FIELD_SIZE] if failure_message.length > MAX_TEXT_FIELD_SIZE
|
|
405
|
+
backtrace = backtrace[0...MAX_TEXT_FIELD_SIZE] if backtrace.length > MAX_TEXT_FIELD_SIZE
|
|
406
|
+
# TODO: should we use concatenated string or alias when auto-generated description?
|
|
407
|
+
name = example.full_description
|
|
408
|
+
file, classname = Trunk.file_and_classname(example)
|
|
409
|
+
line = example.metadata[:line_number]
|
|
410
|
+
started_at = example.execution_result.started_at.to_i
|
|
411
|
+
finished_at = example.execution_result.finished_at.to_i
|
|
412
|
+
|
|
413
|
+
attempt_number = example.metadata[:retry_attempts] || example.metadata[:trunk_attempt_number] || 0
|
|
414
|
+
# set the status to failure, but mark it as quarantined
|
|
415
|
+
is_quarantined = example.metadata[:trunk_quarantined_exception] ? true : false
|
|
416
|
+
@run.report.add_test(example.trunk_id, name, classname, file, Trunk.parent_name(example), line, status,
|
|
417
|
+
attempt_number, started_at, finished_at, failure_message || '', backtrace || '',
|
|
418
|
+
is_quarantined)
|
|
419
|
+
end
|
|
433
420
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
421
|
+
# Determine the Trunk status to report for an example, plus the exception (if
|
|
422
|
+
# any) whose message/backtrace should be recorded for it.
|
|
423
|
+
#
|
|
424
|
+
# `pending` examples need care because RSpec expresses their outcome as the
|
|
425
|
+
# inverse of the body's pass/fail. A pending example's contract is "this is
|
|
426
|
+
# expected to fail", so:
|
|
427
|
+
# - body still fails -> expectation met -> RSpec status :pending, build green
|
|
428
|
+
# - body now passes -> expectation violated -> RSpec status :failed
|
|
429
|
+
# (PendingExampleFixedError), build red
|
|
430
|
+
# We report the outcome RSpec actually decided, which also matches the build
|
|
431
|
+
# result:
|
|
432
|
+
# - skip / xit (never ran) -> skipped
|
|
433
|
+
# - pending, body still failing -> success (expectation met)
|
|
434
|
+
# - pending, body now passing -> failure (PendingExampleFixedError)
|
|
435
|
+
# RSpec's own build pass/fail behavior is left untouched (see #set_exception).
|
|
436
|
+
#
|
|
437
|
+
# trunk-ignore(rubocop/Metrics/AbcSize,rubocop/Metrics/CyclomaticComplexity,rubocop/Metrics/MethodLength)
|
|
438
|
+
def status_and_exception(example)
|
|
439
|
+
result = example.execution_result
|
|
440
|
+
quarantined_exception = example.metadata[:trunk_quarantined_exception]
|
|
441
|
+
|
|
442
|
+
# A genuinely skipped example (`skip`/`xit`, or `skip` called from a hook or
|
|
443
|
+
# body) never runs, so it has no real pass/fail outcome. RSpec still reports
|
|
444
|
+
# it with status :pending, so detect skips explicitly -- and up front -- via
|
|
445
|
+
# metadata[:skip] before interpreting any pending pass/fail semantics below.
|
|
446
|
+
return [Status.new('skipped'), nil] if example.metadata[:skip]
|
|
447
|
+
|
|
448
|
+
case result.status
|
|
449
|
+
when :passed
|
|
450
|
+
# A quarantined failure is recorded as :passed by RSpec (see #set_exception),
|
|
451
|
+
# so report it as a failure but carry the original quarantined exception.
|
|
452
|
+
quarantined_exception ? [Status.new('failure'), quarantined_exception] : [Status.new('success'), nil]
|
|
453
|
+
when :failed
|
|
454
|
+
# Includes a pending example whose body unexpectedly passed: RSpec reports it
|
|
455
|
+
# as :failed with a PendingExampleFixedError (on example.exception) and breaks
|
|
456
|
+
# the build (the pending expectation was violated), so it is a failure here too.
|
|
457
|
+
[Status.new('failure'), example.exception || quarantined_exception]
|
|
458
|
+
when :pending
|
|
459
|
+
# A quarantined fixed-pending is left :pending but pending_fixed? -- still a
|
|
460
|
+
# failure. Otherwise the pending "should fail" expectation was met -> success.
|
|
461
|
+
if result.pending_fixed?
|
|
462
|
+
[Status.new('failure'), quarantined_exception || example.exception]
|
|
463
|
+
else
|
|
464
|
+
[Status.new('success'), nil]
|
|
465
|
+
end
|
|
466
|
+
else
|
|
467
|
+
[Status.new(result.status.to_s), example.exception || quarantined_exception]
|
|
468
|
+
end
|
|
469
|
+
end
|
|
437
470
|
end
|
|
438
471
|
end
|
|
439
|
-
|
|
440
|
-
c.reporter.register_listener TrunkAnalyticsListener.new, :example_finished, :close
|
|
441
472
|
end
|
|
442
473
|
|
|
443
474
|
RSpec::Trunk.setup
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec_trunk_flaky_tests
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: aarch64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Trunk Technologies, Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec-core
|