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.
@@ -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 adds the
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
- # Add formatter when in CI
17
- config.add_formatter(Mergify::RSpec::Formatter) if Utils.in_ci?
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
- ci = Mergify::RSpec.ci_insights
41
- fd = ci&.flaky_detector
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
- # Mark as flaky detection candidate (even if too slow to rerun)
57
- example.metadata[:mergify_flaky_detection] = true
58
- example.metadata[:mergify_new_test] = true if fd.mode == 'new'
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
- fd.set_test_deadline(example.id)
61
- next if fd.test_too_slow?(example.id)
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
- distinct_outcomes = Set.new
64
- distinct_outcomes.add(example.execution_result.status) if example.execution_result.status
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
- rerun_count = 0
67
- # Use a null reporter to prevent reruns from being reported as
68
- # individual failures. The final outcome is set after the loop.
69
- null_reporter = ::RSpec::Core::NullReporter
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
- until example.metadata[:is_last_rerun]
72
- example.metadata[:is_last_rerun] = fd.last_rerun_for_test?(example.id)
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
- # Reset example state for rerun
75
- example.instance_variable_set(:@exception, nil)
76
- if example.example_group_instance
77
- memoized = example.example_group_instance.instance_variable_get(:@__memoized)
78
- if memoized.respond_to?(:clear)
79
- memoized.clear
80
- elsif memoized
81
- # RSpec >= 3.12 uses ThreadsafeMemoized which wraps an internal hash
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
- example.run(example.example_group_instance, null_reporter)
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
- # Feed rerun metrics so budget/deadline tracking stays accurate
90
- rerun_time = example.execution_result.run_time || 0.0
91
- rerun_status = example.execution_result.status
92
- fd.fill_metrics_from_report(example.id, 'call', rerun_time, rerun_status)
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
- distinct_outcomes.add(rerun_status)
95
- rerun_count += 1
96
- end
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
- is_flaky = distinct_outcomes.include?(:passed) &&
99
- distinct_outcomes.include?(:failed)
100
- example.metadata[:mergify_flaky] = true if is_flaky
101
- example.metadata[:mergify_rerun_count] = rerun_count
102
- end
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
- # Quarantine: override failed quarantined test results
105
- config.after(:each) do |example|
106
- next unless example.metadata[:mergify_quarantined] && example.exception
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
- example.instance_variable_set(:@exception, nil)
109
- example.execution_result.status = :pending
110
- example.execution_result.pending_message = 'Test is quarantined from Mergify Test Insights'
111
- end
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
- remaining_tests = [remaining_tests_count, 1].max
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
- def remaining_tests_count
211
- @tests_to_process.count { |id| !@metrics.key?(id) || @metrics[id].deadline.nil? }
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
- require 'opentelemetry-sdk'
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 creates OpenTelemetry spans for Mergify Test Insights and
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
- ::RSpec::Core::Formatters.register self,
14
- :start,
15
- :example_started,
16
- :example_finished,
17
- :example_pending,
18
- :stop
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&.tracer
26
-
27
- extract_distributed_trace_context
32
+ return unless @ci_insights&.recorder
28
33
 
29
- @session_span = @ci_insights.tracer.start_span(
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&.tracer && @session_span
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.tracer.start_span(
48
+ span = @ci_insights.recorder.start_span(
46
49
  example.id,
47
- with_parent: parent_context,
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.status = OpenTelemetry::Trace::Status.ok
74
+ span.ok!
71
75
  end
72
76
 
73
- span.finish
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
- span.finish
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
- def extract_distributed_trace_context
96
- traceparent = ENV.fetch('MERGIFY_TRACEPARENT', nil)
97
- @parent_context = if traceparent
98
- propagator = OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator.new
99
- propagator.extract({ 'traceparent' => traceparent })
100
- end
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.status = OpenTelemetry::Trace::Status.error(exception.message)
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
- @session_span.status = if @has_error
143
- OpenTelemetry::Trace::Status.error('One or more tests failed')
144
- else
145
- OpenTelemetry::Trace::Status.ok
146
- end
147
- @session_span.finish
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
- def flush_and_shutdown # rubocop:disable Metrics/MethodLength
191
- return unless @ci_insights&.tracer_provider
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
- begin
194
- @ci_insights.tracer_provider.force_flush
195
- rescue StandardError => e
196
- print_export_error(e)
197
- end
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
- begin
200
- @ci_insights.tracer_provider.shutdown
201
- rescue StandardError => e
202
- output.puts "Error while shutting down the tracer: #{e.message}"
203
- end
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)
@@ -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
- begin
36
- # Precompiled gems ship lib/mergify/rspec/<ruby>/mergify_ci.<dlext>.
37
- require_relative "#{RUBY_VERSION.to_f}/mergify_ci"
38
- rescue LoadError
39
- begin
40
- # Source gem, and any local `rake compile`.
41
- require_relative 'mergify_ci'
42
- rescue LoadError => e
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 << " Unused quarantined tests (#{unused.size}):"
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