rspec-mergify 0.2.0-aarch64-linux → 0.4.0-aarch64-linux
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/README.md +66 -0
- data/lib/mergify/rspec/3.1/mergify_ci.so +0 -0
- data/lib/mergify/rspec/3.2/mergify_ci.so +0 -0
- data/lib/mergify/rspec/3.3/mergify_ci.so +0 -0
- data/lib/mergify/rspec/3.4/mergify_ci.so +0 -0
- data/lib/mergify/rspec/4.0/mergify_ci.so +0 -0
- data/lib/mergify/rspec/ci_insights.rb +246 -76
- data/lib/mergify/rspec/configuration.rb +160 -65
- data/lib/mergify/rspec/flaky_detection.rb +16 -10
- data/lib/mergify/rspec/formatter.rb +90 -47
- data/lib/mergify/rspec/native.rb +31 -10
- data/lib/mergify/rspec/quarantine.rb +7 -1
- data/lib/mergify/rspec/resources/rspec.rb +9 -5
- data/lib/mergify/rspec/session_verdict.rb +86 -0
- data/lib/mergify/rspec/test_selection.rb +299 -0
- data/lib/mergify/rspec/trace.rb +145 -0
- data/lib/mergify/rspec/utils.rb +7 -0
- data/lib/mergify/rspec/version.rb +1 -1
- metadata +5 -31
- data/lib/mergify/rspec/synchronous_batch_span_processor.rb +0 -34
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'set'
|
|
4
|
+
require_relative 'test_selection'
|
|
4
5
|
|
|
5
6
|
module Mergify
|
|
6
7
|
module RSpec
|
|
7
|
-
# Registers RSpec hooks for quarantine and flaky detection, and
|
|
8
|
+
# Registers RSpec hooks for quarantine and flaky detection, and attaches the
|
|
8
9
|
# Mergify Test Insights formatter when running inside CI.
|
|
10
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
9
11
|
module Configuration
|
|
12
|
+
QUARANTINE_MESSAGE = 'Test is quarantined from Mergify Test Insights'
|
|
13
|
+
|
|
10
14
|
module_function
|
|
11
15
|
|
|
12
|
-
# rubocop:disable Metrics/MethodLength,Metrics/BlockLength,Metrics/AbcSize
|
|
16
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/BlockLength,Metrics/AbcSize
|
|
13
17
|
# rubocop:disable-next Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
14
18
|
def setup!
|
|
15
19
|
::RSpec.configure do |config|
|
|
16
|
-
#
|
|
17
|
-
|
|
20
|
+
# Registered before the formatter's hook so that it runs after it --
|
|
21
|
+
# each `prepend_before` goes to the front: the formatter has to be
|
|
22
|
+
# attached already for the run's report and verdict to see what the
|
|
23
|
+
# selection did.
|
|
24
|
+
config.prepend_before(:suite) { Configuration.select_examples } if Utils.in_ci?
|
|
25
|
+
|
|
26
|
+
# Attached to the reporter once the suite starts, rather than added with
|
|
27
|
+
# `add_formatter`: RSpec only sets up its default formatter when no
|
|
28
|
+
# other was added, so adding this one took the progress output and the
|
|
29
|
+
# summary line away from every suite that had not chosen a format.
|
|
30
|
+
config.prepend_before(:suite) { Configuration.attach_formatter(config) } if Utils.in_ci?
|
|
18
31
|
|
|
19
32
|
# Flaky detection: prepare session with all example IDs
|
|
20
33
|
config.before(:suite) do
|
|
@@ -37,81 +50,163 @@ module Mergify
|
|
|
37
50
|
|
|
38
51
|
# Flaky detection: rerun tests within budget
|
|
39
52
|
config.around(:each) do |example|
|
|
40
|
-
|
|
41
|
-
fd
|
|
42
|
-
|
|
43
|
-
example.run
|
|
44
|
-
|
|
45
|
-
# Feed metrics from the initial run so the detector can evaluate
|
|
46
|
-
if fd
|
|
47
|
-
run_time = example.execution_result.run_time || 0.0
|
|
48
|
-
status = example.execution_result.status
|
|
49
|
-
fd.fill_metrics_from_report(example.id, 'setup', 0.0, status)
|
|
50
|
-
fd.fill_metrics_from_report(example.id, 'call', run_time, status)
|
|
51
|
-
fd.fill_metrics_from_report(example.id, 'teardown', 0.0, status)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
next unless fd&.rerunning_test?(example.id)
|
|
53
|
+
fd = Mergify::RSpec.ci_insights&.flaky_detector
|
|
54
|
+
fd ? Configuration.run_detecting_flakiness(example, fd) : example.run
|
|
55
|
+
end
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
# Quarantine: override failed quarantined test results. The failure is
|
|
58
|
+
# noted for flaky detection first, which would otherwise take a
|
|
59
|
+
# quarantined attempt that failed for one that passed.
|
|
60
|
+
config.after(:each) do |example|
|
|
61
|
+
next unless example.metadata[:mergify_quarantined] && example.exception
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
example.metadata[:mergify_quarantined_failure] = true
|
|
64
|
+
example.instance_variable_set(:@exception, nil)
|
|
65
|
+
example.execution_result.status = :pending
|
|
66
|
+
example.execution_result.pending_message = QUARANTINE_MESSAGE
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
# Fingerprint what this process is about to run, ask Mergify whether part
|
|
72
|
+
# of it is enough, and remove the rest.
|
|
73
|
+
#
|
|
74
|
+
# A `before(:suite)` hook, because that is the first moment RSpec has both
|
|
75
|
+
# loaded the spec files and applied its filters, and the last one before
|
|
76
|
+
# an example group starts. What each group runs is read from
|
|
77
|
+
# `RSpec.world.filtered_examples`, which RSpec memoized while counting the
|
|
78
|
+
# run; narrowing those lists is what deselects.
|
|
79
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
80
|
+
def select_examples
|
|
81
|
+
ci = Mergify::RSpec.ci_insights
|
|
82
|
+
return unless ci
|
|
83
|
+
|
|
84
|
+
groups = ::RSpec.world.example_groups.flat_map(&:descendants)
|
|
85
|
+
ids = groups.flat_map { |group| ::RSpec.world.filtered_examples[group] }.map(&:id)
|
|
86
|
+
ci.on_examples_collected(ids)
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
keep = ci.test_selection&.resolve(ids)
|
|
90
|
+
rescue TestSelectionRefused => e
|
|
91
|
+
ci.on_selection_resolved(0)
|
|
92
|
+
return refuse(e.message)
|
|
93
|
+
end
|
|
65
94
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
deselect(groups, keep) if keep
|
|
96
|
+
ci.on_selection_resolved(keep ? ids.count { |id| keep.include?(id) } : ids.size)
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
# This runs on every CI run, opted in or not, and RSpec would answer a
|
|
99
|
+
# raise here by running nothing: a fault of this gem must cost the run
|
|
100
|
+
# its reduction, never its tests.
|
|
101
|
+
::RSpec.configuration.error_stream.puts(
|
|
102
|
+
"Mergify Test Selection failed, so the full suite runs: #{e.class}: #{e.message}"
|
|
103
|
+
)
|
|
104
|
+
end
|
|
70
105
|
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
# Stop the run before any example, the way RSpec stops one whose suite
|
|
107
|
+
# hook failed -- nothing runs, the exit code is red, and the summary
|
|
108
|
+
# counts an error outside of examples -- but with the server's
|
|
109
|
+
# explanation alone. Raised from the hook, the same failure is printed
|
|
110
|
+
# under a "Failure/Error:" header pointing at a line of this gem, which
|
|
111
|
+
# only buries the explanation.
|
|
112
|
+
def refuse(message)
|
|
113
|
+
::RSpec.configuration.error_stream.puts("\n#{message}\n")
|
|
114
|
+
::RSpec.world.wants_to_quit = true
|
|
115
|
+
reporter = ::RSpec.configuration.reporter
|
|
116
|
+
count = reporter.instance_variable_get(:@non_example_exception_count)
|
|
117
|
+
reporter.instance_variable_set(:@non_example_exception_count, count + 1) if count.is_a?(Integer)
|
|
118
|
+
end
|
|
73
119
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
inner = memoized.instance_variable_get(:@memoized)
|
|
83
|
-
inner&.clear
|
|
84
|
-
end
|
|
85
|
-
end
|
|
120
|
+
# A group left with no example runs none of its `before(:context)`
|
|
121
|
+
# hooks: RSpec decides that from these same lists, once the group starts.
|
|
122
|
+
def deselect(groups, keep)
|
|
123
|
+
groups.each do |group|
|
|
124
|
+
::RSpec.world.filtered_examples[group] =
|
|
125
|
+
::RSpec.world.filtered_examples[group].select { |example| keep.include?(example.id) }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
# The reporter has sent `start` by the time suite hooks run, so the
|
|
130
|
+
# formatter is handed the same notification directly.
|
|
131
|
+
def attach_formatter(config)
|
|
132
|
+
formatter = Formatter.new(config.output_stream)
|
|
133
|
+
formatter.start(::RSpec::Core::Notifications::StartNotification.new(::RSpec.world.example_count, 0))
|
|
134
|
+
config.reporter.register_listener(formatter, *Formatter::NOTIFICATIONS)
|
|
135
|
+
end
|
|
88
136
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
137
|
+
# `example` is the Procsy an around hook is handed, and the example has no
|
|
138
|
+
# result yet: RSpec records its status and run time in `Example#finish`,
|
|
139
|
+
# once every around hook has returned. So each attempt is timed here, and
|
|
140
|
+
# its outcome read off the exception it left behind.
|
|
141
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
142
|
+
def run_detecting_flakiness(example, detector)
|
|
143
|
+
status, duration = run_attempt(example)
|
|
144
|
+
detector.fill_metrics_from_report(example.id, 'setup', 0.0, status)
|
|
145
|
+
detector.fill_metrics_from_report(example.id, 'call', duration, status)
|
|
146
|
+
detector.fill_metrics_from_report(example.id, 'teardown', 0.0, status)
|
|
147
|
+
return unless detector.rerunning_test?(example.id)
|
|
148
|
+
|
|
149
|
+
# Mark as flaky detection candidate (even if too slow to rerun)
|
|
150
|
+
example.metadata[:mergify_flaky_detection] = true
|
|
151
|
+
example.metadata[:mergify_new_test] = true if detector.mode == 'new'
|
|
152
|
+
|
|
153
|
+
detector.set_test_deadline(example.id)
|
|
154
|
+
return if detector.test_too_slow?(example.id)
|
|
155
|
+
|
|
156
|
+
rerun(example, detector, status)
|
|
157
|
+
end
|
|
93
158
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
# Which attempt decides the result is pytest-mergify's rule: in 'new' mode
|
|
160
|
+
# any failure fails the test, so a new flaky test cannot be merged; in
|
|
161
|
+
# 'unhealthy' mode the reruns only learn, and the first attempt stands.
|
|
162
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
|
|
163
|
+
def rerun(example, detector, initial_status)
|
|
164
|
+
initial_exception = example.exception
|
|
165
|
+
first_failure = initial_exception
|
|
166
|
+
outcomes = Set[initial_status]
|
|
167
|
+
rerun_count = 0
|
|
168
|
+
|
|
169
|
+
until example.metadata[:is_last_rerun]
|
|
170
|
+
example.metadata[:is_last_rerun] = detector.last_rerun_for_test?(example.id)
|
|
171
|
+
reset_for_rerun(example)
|
|
172
|
+
|
|
173
|
+
status, duration = run_attempt(example)
|
|
174
|
+
detector.fill_metrics_from_report(example.id, 'call', duration, status)
|
|
175
|
+
outcomes << status
|
|
176
|
+
first_failure ||= example.exception
|
|
177
|
+
rerun_count += 1
|
|
178
|
+
end
|
|
97
179
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
180
|
+
example.metadata[:mergify_flaky] = true if outcomes.include?(:passed) && outcomes.include?(:failed)
|
|
181
|
+
example.metadata[:mergify_rerun_count] = rerun_count
|
|
182
|
+
final_exception = detector.mode == 'new' ? first_failure : initial_exception
|
|
183
|
+
example.example.instance_variable_set(:@exception, final_exception)
|
|
184
|
+
end
|
|
103
185
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
186
|
+
# Runs what the around hook wraps -- the before and after hooks included
|
|
187
|
+
# -- and answers its outcome and duration in seconds.
|
|
188
|
+
def run_attempt(example)
|
|
189
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
190
|
+
example.run
|
|
191
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
192
|
+
failed = example.exception || example.metadata.delete(:mergify_quarantined_failure)
|
|
193
|
+
[failed ? :failed : :passed, duration]
|
|
194
|
+
end
|
|
107
195
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
196
|
+
# The exception lives on the example itself, not on the Procsy: setting
|
|
197
|
+
# it there would leave RSpec to fold every attempt's failure into one.
|
|
198
|
+
def reset_for_rerun(example)
|
|
199
|
+
example.example.instance_variable_set(:@exception, nil)
|
|
200
|
+
return unless example.example_group_instance
|
|
201
|
+
|
|
202
|
+
memoized = example.example_group_instance.instance_variable_get(:@__memoized)
|
|
203
|
+
if memoized.respond_to?(:clear)
|
|
204
|
+
memoized.clear
|
|
205
|
+
elsif memoized
|
|
206
|
+
# RSpec >= 3.12 uses ThreadsafeMemoized which wraps an internal hash
|
|
207
|
+
memoized.instance_variable_get(:@memoized)&.clear
|
|
112
208
|
end
|
|
113
209
|
end
|
|
114
|
-
# rubocop:enable Metrics/MethodLength,Metrics/BlockLength,Metrics/AbcSize
|
|
115
210
|
end
|
|
116
211
|
end
|
|
117
212
|
end
|
|
@@ -126,8 +126,7 @@ module Mergify
|
|
|
126
126
|
def set_test_deadline(test_id, timeout: nil)
|
|
127
127
|
return unless @metrics.key?(test_id)
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
per_test_budget = remaining_budget / remaining_tests
|
|
129
|
+
per_test_budget = next_test_share
|
|
131
130
|
|
|
132
131
|
allocated =
|
|
133
132
|
if timeout
|
|
@@ -147,11 +146,14 @@ module Mergify
|
|
|
147
146
|
(metrics.initial_duration * min_exec) > metrics.remaining_time
|
|
148
147
|
end
|
|
149
148
|
|
|
149
|
+
# Asked before the rerun it guards. `rerun_count` counts executions, the
|
|
150
|
+
# initial one included, so the rerun about to start is the last one when
|
|
151
|
+
# it is the one reaching the cap.
|
|
150
152
|
def last_rerun_for_test?(test_id)
|
|
151
153
|
return false unless @metrics.key?(test_id)
|
|
152
154
|
|
|
153
155
|
metrics = @metrics[test_id]
|
|
154
|
-
metrics.will_exceed_deadline? || metrics.rerun_count >= @context['max_test_execution_count']
|
|
156
|
+
metrics.will_exceed_deadline? || metrics.rerun_count + 1 >= @context['max_test_execution_count']
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
def test_metrics(test_id)
|
|
@@ -198,17 +200,21 @@ module Mergify
|
|
|
198
200
|
context
|
|
199
201
|
end
|
|
200
202
|
|
|
201
|
-
def remaining_budget
|
|
202
|
-
used = budget_used
|
|
203
|
-
[@budget - used, 0.0].max
|
|
204
|
-
end
|
|
205
|
-
|
|
206
203
|
def budget_used
|
|
207
204
|
@metrics.sum { |_, m| m.total_duration }
|
|
208
205
|
end
|
|
209
206
|
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
# What is left of the budget, divided over the tests still waiting for a
|
|
208
|
+
# share of it. The engine works in milliseconds; RSpec durations are
|
|
209
|
+
# seconds.
|
|
210
|
+
def next_test_share
|
|
211
|
+
Native::Budget.dynamic_share_ms(
|
|
212
|
+
@budget * 1000, budget_used * 1000, @tests_to_process.size, processed_tests_count
|
|
213
|
+
) / 1000.0
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def processed_tests_count
|
|
217
|
+
@tests_to_process.count { |id| @metrics.key?(id) && !@metrics[id].deadline.nil? }
|
|
212
218
|
end
|
|
213
219
|
end
|
|
214
220
|
end
|
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rspec/core/formatters/base_formatter'
|
|
4
|
-
|
|
4
|
+
require_relative 'trace'
|
|
5
|
+
require_relative 'configuration'
|
|
6
|
+
|
|
7
|
+
require 'mergify/rspec/native'
|
|
5
8
|
|
|
6
9
|
module Mergify
|
|
7
10
|
module RSpec
|
|
8
|
-
# RSpec formatter that
|
|
11
|
+
# RSpec formatter that records spans for Mergify Test Insights and
|
|
9
12
|
# prints a terminal report. It is purely observational and does not modify
|
|
10
13
|
# test execution.
|
|
11
14
|
# rubocop:disable-next Metrics/ClassLength
|
|
12
15
|
class Formatter < ::RSpec::Core::Formatters::BaseFormatter
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# What it listens to once attached, `start` excepted: it is attached after
|
|
17
|
+
# that notification, and handed it directly.
|
|
18
|
+
NOTIFICATIONS = %i[example_started example_finished example_pending stop].freeze
|
|
19
|
+
|
|
20
|
+
# @mergifyio/vitest's sentence, for the same withheld verdict.
|
|
21
|
+
WITHHELD_VERDICT = TestSelection.wrap(
|
|
22
|
+
"Mergify wasn't sent this run's results: it failed outside any example. If this merge-queue " \
|
|
23
|
+
'batch is retried, this job will run its full test suite.'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
::RSpec::Core::Formatters.register self, :start, *NOTIFICATIONS
|
|
19
27
|
|
|
20
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
21
28
|
def start(notification)
|
|
22
29
|
super
|
|
23
30
|
|
|
24
31
|
@ci_insights = Mergify::RSpec.ci_insights
|
|
25
|
-
return unless @ci_insights&.
|
|
26
|
-
|
|
27
|
-
extract_distributed_trace_context
|
|
32
|
+
return unless @ci_insights&.recorder
|
|
28
33
|
|
|
29
|
-
@session_span = @ci_insights.
|
|
34
|
+
@session_span = @ci_insights.recorder.start_span(
|
|
30
35
|
'rspec session start',
|
|
31
|
-
with_parent: @parent_context,
|
|
32
36
|
attributes: { 'test.scope' => 'session' }
|
|
33
37
|
)
|
|
34
38
|
@has_error = false
|
|
@@ -36,21 +40,20 @@ module Mergify
|
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def example_started(notification)
|
|
39
|
-
return unless @ci_insights&.
|
|
43
|
+
return unless @ci_insights&.recorder && @session_span
|
|
40
44
|
|
|
41
45
|
example = notification.example
|
|
42
|
-
parent_context = OpenTelemetry::Trace.context_with_span(@session_span)
|
|
43
46
|
quarantined = @ci_insights.mark_test_as_quarantined_if_needed(example.id)
|
|
44
47
|
|
|
45
|
-
span = @ci_insights.
|
|
48
|
+
span = @ci_insights.recorder.start_span(
|
|
46
49
|
example.id,
|
|
47
|
-
|
|
50
|
+
parent: @session_span,
|
|
48
51
|
attributes: build_example_attributes(example, quarantined)
|
|
49
52
|
)
|
|
50
53
|
@example_spans[example.id] = span
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
56
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
|
|
54
57
|
def example_finished(notification)
|
|
55
58
|
return unless @example_spans
|
|
56
59
|
|
|
@@ -59,6 +62,7 @@ module Mergify
|
|
|
59
62
|
return unless span
|
|
60
63
|
|
|
61
64
|
result = example.execution_result
|
|
65
|
+
@ci_insights.session_verdict.record(example.id, verdict_status(example), result.run_time)
|
|
62
66
|
status = result.status.to_s
|
|
63
67
|
span.set_attribute('test.case.result.status', status)
|
|
64
68
|
set_flaky_attributes(span, example)
|
|
@@ -67,10 +71,10 @@ module Mergify
|
|
|
67
71
|
set_error_attributes(span, result.exception)
|
|
68
72
|
@has_error = true
|
|
69
73
|
else
|
|
70
|
-
span.
|
|
74
|
+
span.ok!
|
|
71
75
|
end
|
|
72
76
|
|
|
73
|
-
span
|
|
77
|
+
@ci_insights.recorder.record(span)
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def example_pending(notification)
|
|
@@ -81,23 +85,48 @@ module Mergify
|
|
|
81
85
|
return unless span
|
|
82
86
|
|
|
83
87
|
span.set_attribute('test.case.result.status', 'skipped')
|
|
84
|
-
|
|
88
|
+
@ci_insights.session_verdict.record(example.id, verdict_status(example), example.execution_result.run_time)
|
|
89
|
+
@ci_insights.recorder.record(span)
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
def stop(_notification)
|
|
88
93
|
finish_session_span
|
|
94
|
+
# The verdict first, on purpose: it is what the next merge-queue rerun
|
|
95
|
+
# of this job is answered from, and it must never wait behind the
|
|
96
|
+
# trace upload's timeout and retries.
|
|
97
|
+
send_session_verdict
|
|
89
98
|
print_report
|
|
90
99
|
flush_and_shutdown
|
|
91
100
|
end
|
|
92
101
|
|
|
93
102
|
private
|
|
94
103
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
# The status RSpec reported, which is the one that decided the exit
|
|
105
|
+
# code. A pending example is a skip, unless it is a failure the
|
|
106
|
+
# quarantine absorbed.
|
|
107
|
+
def verdict_status(example)
|
|
108
|
+
result = example.execution_result
|
|
109
|
+
case result.status
|
|
110
|
+
when :passed then 'passed'
|
|
111
|
+
when :failed then 'failed'
|
|
112
|
+
else
|
|
113
|
+
quarantined = example.metadata[:mergify_quarantined] &&
|
|
114
|
+
result.pending_message == Configuration::QUARANTINE_MESSAGE
|
|
115
|
+
quarantined ? 'quarantined_failed' : 'skipped'
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def send_session_verdict
|
|
120
|
+
return unless @ci_insights&.recorder
|
|
121
|
+
|
|
122
|
+
@ci_insights.send_session_verdict(failed_outside_examples: failed_outside_examples?)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# RSpec flags a failure no example carries -- a failing `after(:context)`
|
|
126
|
+
# or suite hook. A load error flags it too, but stops the run before the
|
|
127
|
+
# formatter is ever attached.
|
|
128
|
+
def failed_outside_examples?
|
|
129
|
+
::RSpec.world.non_example_failure ? true : false
|
|
101
130
|
end
|
|
102
131
|
|
|
103
132
|
def build_example_attributes(example, quarantined)
|
|
@@ -133,18 +162,18 @@ module Mergify
|
|
|
133
162
|
span.set_attribute('exception.type', exception.class.to_s)
|
|
134
163
|
span.set_attribute('exception.message', exception.message)
|
|
135
164
|
span.set_attribute('exception.stacktrace', exception.backtrace&.join("\n") || '')
|
|
136
|
-
span.
|
|
165
|
+
span.error!(exception.message)
|
|
137
166
|
end
|
|
138
167
|
|
|
139
168
|
def finish_session_span
|
|
140
169
|
return unless @session_span
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@session_span
|
|
171
|
+
if @has_error
|
|
172
|
+
@session_span.error!('One or more tests failed')
|
|
173
|
+
else
|
|
174
|
+
@session_span.ok!
|
|
175
|
+
end
|
|
176
|
+
@ci_insights.recorder.record(@session_span)
|
|
148
177
|
end
|
|
149
178
|
|
|
150
179
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -160,6 +189,7 @@ module Mergify
|
|
|
160
189
|
print_configuration_warnings
|
|
161
190
|
print_flaky_report
|
|
162
191
|
print_quarantine_report
|
|
192
|
+
print_test_selection_report
|
|
163
193
|
output.puts "MERGIFY_TEST_RUN_ID=#{@ci_insights.test_run_id}"
|
|
164
194
|
output.puts '------------------'
|
|
165
195
|
end
|
|
@@ -167,12 +197,21 @@ module Mergify
|
|
|
167
197
|
def print_configuration_warnings
|
|
168
198
|
output.puts 'WARNING: MERGIFY_TOKEN is not set. Traces will not be sent to Mergify.' unless @ci_insights.token
|
|
169
199
|
|
|
200
|
+
return print_native_warning unless Mergify::RSpec::Native.available?
|
|
170
201
|
return if @ci_insights.repo_name
|
|
171
202
|
|
|
172
203
|
output.puts 'WARNING: Could not detect repository name. ' \
|
|
173
204
|
'Please set GITHUB_REPOSITORY or configure a git remote.'
|
|
174
205
|
end
|
|
175
206
|
|
|
207
|
+
# The extension is what detects CI, so when it did not load there is no
|
|
208
|
+
# repository name either, and blaming that would send people hunting for a
|
|
209
|
+
# git remote that is fine. Say what actually happened instead.
|
|
210
|
+
def print_native_warning
|
|
211
|
+
output.puts 'WARNING: the Mergify native extension could not be loaded, so this run reports nothing: ' \
|
|
212
|
+
"#{Mergify::RSpec::Native.load_error&.message}"
|
|
213
|
+
end
|
|
214
|
+
|
|
176
215
|
def print_flaky_report
|
|
177
216
|
return unless @ci_insights.flaky_detector.respond_to?(:make_report)
|
|
178
217
|
|
|
@@ -187,20 +226,24 @@ module Mergify
|
|
|
187
226
|
output.puts report if report
|
|
188
227
|
end
|
|
189
228
|
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
# One block whatever happened, once the job asked for a selection.
|
|
230
|
+
def print_test_selection_report
|
|
231
|
+
selection = @ci_insights.test_selection
|
|
232
|
+
return unless selection
|
|
192
233
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
output.puts selection.report
|
|
235
|
+
output.puts WITHHELD_VERDICT if @ci_insights.session_verdict_withheld
|
|
236
|
+
verdict_report = @ci_insights.session_verdict_result.report
|
|
237
|
+
output.puts verdict_report if verdict_report
|
|
238
|
+
end
|
|
198
239
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
# One upload, at the end. There is nothing to shut down any more: the
|
|
241
|
+
# recorder holds spans in memory and the client owns the connection.
|
|
242
|
+
def flush_and_shutdown
|
|
243
|
+
return unless @ci_insights&.recorder
|
|
244
|
+
|
|
245
|
+
error = @ci_insights.flush
|
|
246
|
+
print_export_error(StandardError.new(error)) if error
|
|
204
247
|
end
|
|
205
248
|
|
|
206
249
|
def print_export_error(error)
|
data/lib/mergify/rspec/native.rb
CHANGED
|
@@ -27,21 +27,42 @@ module Mergify
|
|
|
27
27
|
def available?
|
|
28
28
|
load_error.nil?
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
# Which of the two failed requires actually explains the absence.
|
|
32
|
+
#
|
|
33
|
+
# A precompiled gem ships lib/mergify/rspec/<ruby>/mergify_ci.<dlext>,
|
|
34
|
+
# so when one is packed for this Ruby that require is the real attempt:
|
|
35
|
+
# it fails with something like "version `GLIBC_2.29' not found", and the
|
|
36
|
+
# fallback that follows only ever adds "cannot load such file", which
|
|
37
|
+
# sends the reader looking for the wrong thing. With no extension for
|
|
38
|
+
# this Ruby -- the source gem, or a platform we publish no gem for --
|
|
39
|
+
# it is the other way round.
|
|
40
|
+
# Require a file next to this one, returning the LoadError instead of
|
|
41
|
+
# raising it. Two attempts read better as values than as nested rescues.
|
|
42
|
+
def attempt_require(path)
|
|
43
|
+
require_relative path
|
|
44
|
+
nil
|
|
45
|
+
rescue LoadError => e
|
|
46
|
+
e
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_failure_reason(versioned, fallback)
|
|
50
|
+
packed = Dir.glob(File.join(__dir__, RUBY_VERSION.to_f.to_s, 'mergify_ci.*')).any?
|
|
51
|
+
packed ? versioned : fallback
|
|
52
|
+
end
|
|
30
53
|
end
|
|
31
54
|
end
|
|
32
55
|
end
|
|
33
56
|
end
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Mergify::RSpec::Native.load_error = e
|
|
44
|
-
end
|
|
58
|
+
# Precompiled gems ship lib/mergify/rspec/<ruby>/mergify_ci.<dlext>; the source
|
|
59
|
+
# gem and any local `rake compile` put one beside this file instead.
|
|
60
|
+
versioned_error = Mergify::RSpec::Native.attempt_require("#{RUBY_VERSION.to_f}/mergify_ci")
|
|
61
|
+
fallback_error = versioned_error && Mergify::RSpec::Native.attempt_require('mergify_ci')
|
|
62
|
+
|
|
63
|
+
if fallback_error
|
|
64
|
+
Mergify::RSpec::Native.load_error =
|
|
65
|
+
Mergify::RSpec::Native.load_failure_reason(versioned_error, fallback_error)
|
|
45
66
|
end
|
|
46
67
|
|
|
47
68
|
unless Mergify::RSpec::Native.available?
|
|
@@ -48,13 +48,19 @@ module Mergify
|
|
|
48
48
|
lines << " Quarantined tests run (#{used.size}):"
|
|
49
49
|
used.each { |t| lines << " - #{t}" }
|
|
50
50
|
lines << ''
|
|
51
|
-
lines << "
|
|
51
|
+
lines << " #{unused_label} (#{unused.size}):"
|
|
52
52
|
unused.each { |t| lines << " - #{t}" }
|
|
53
53
|
lines.join("\n")
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
|
+
# A parallel worker runs a share of the suite, so a quarantined test it
|
|
59
|
+
# did not run was most likely run by another worker, not left unused.
|
|
60
|
+
def unused_label
|
|
61
|
+
Utils.parallel_worker? ? 'Quarantined tests not run by this worker' : 'Unused quarantined tests'
|
|
62
|
+
end
|
|
63
|
+
|
|
58
64
|
# A nil list means the repository has no quarantine subscription, which is
|
|
59
65
|
# not an error: the session simply quarantines nothing. Anything that went
|
|
60
66
|
# genuinely wrong is recorded and the suite carries on -- this plugin has
|