rspec-mergify 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -41,7 +41,7 @@ For detailed documentation, see the [official guide](https://docs.mergify.com/ci
41
41
 
42
42
  ### Prerequisites
43
43
 
44
- - Ruby >= 3.1 (`.ruby-version` pins to 3.4.4 — use [rbenv](https://github.com/rbenv/rbenv) or [mise](https://mise.jdx.dev/) to install it)
44
+ - Ruby >= 3.1 (`.ruby-version` pins to 4.0.6 — use [rbenv](https://github.com/rbenv/rbenv) or [mise](https://mise.jdx.dev/) to install it)
45
45
  - Bundler
46
46
 
47
47
  ### Setup
@@ -65,4 +65,4 @@ bundle exec rubocop
65
65
 
66
66
  ## License
67
67
 
68
- GPL-3.0-only
68
+ Apache-2.0 — see the [LICENSE](../../LICENSE) at the repository root.
@@ -3,30 +3,25 @@
3
3
  require 'securerandom'
4
4
  require 'opentelemetry-sdk'
5
5
  require_relative 'utils'
6
+ require_relative 'native'
6
7
  require_relative 'synchronous_batch_span_processor'
7
- require_relative 'resources/ci'
8
- require_relative 'resources/git'
9
- require_relative 'resources/github_actions'
10
- require_relative 'resources/jenkins'
11
- require_relative 'resources/buildkite'
12
- require_relative 'resources/mergify'
13
8
  require_relative 'resources/rspec'
14
9
 
15
10
  module Mergify
16
11
  module RSpec
17
12
  # Central orchestrator for Mergify Test Insights: sets up OpenTelemetry tracing,
18
13
  # manages the tracer provider, and coordinates flaky detection and quarantine.
19
- # rubocop:disable Metrics/ClassLength
14
+ # rubocop:disable-next Metrics/ClassLength
20
15
  class CIInsights
21
16
  attr_reader :token, :repo_name, :api_url, :test_run_id,
22
17
  :tracer_provider, :tracer, :exporter,
23
18
  :branch_name,
24
19
  :flaky_detector, :flaky_detector_error_message, :quarantined_tests
25
20
 
26
- # rubocop:disable Metrics/MethodLength
21
+ # rubocop:disable-next Metrics/MethodLength
27
22
  def initialize
28
23
  @token = ENV.fetch('MERGIFY_TOKEN', nil)
29
- @repo_name = Utils.repository_name
24
+ @repo_name = Native.detect_repository_name
30
25
  @api_url = ENV.fetch('MERGIFY_API_URL', 'https://api.mergify.com')
31
26
  @test_run_id = SecureRandom.hex(8)
32
27
  @tracer_provider = nil
@@ -39,7 +34,6 @@ module Mergify
39
34
 
40
35
  setup_tracing if Utils.in_ci?
41
36
  end
42
- # rubocop:enable Metrics/MethodLength
43
37
 
44
38
  def mark_test_as_quarantined_if_needed(example_id) # rubocop:disable Naming/PredicateMethod
45
39
  return false unless @quarantined_tests&.include?(example_id)
@@ -96,24 +90,20 @@ module Mergify
96
90
  [processor, exp]
97
91
  end
98
92
 
99
- # rubocop:disable Metrics/MethodLength
93
+ # The cicd.* and vcs.* attributes come from the Rust core, which every
94
+ # Mergify test client shares, so a provider gains them everywhere at once.
95
+ # What stays here is what only Ruby knows: the test framework, and the id
96
+ # this run invented for itself.
100
97
  def build_resource
101
98
  resources = [
102
- Resources::CI.detect,
103
- Resources::Git.detect,
104
- Resources::GitHubActions.detect,
105
- Resources::Jenkins.detect,
106
- Resources::Buildkite.detect,
107
- Resources::Mergify.detect,
108
- Resources::RSpec.detect
99
+ OpenTelemetry::SDK::Resources::Resource.create(Native.detect_attributes),
100
+ Resources::RSpec.detect,
101
+ OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
109
102
  ]
110
- base = resources.reduce(OpenTelemetry::SDK::Resources::Resource.create({})) do |merged, r|
103
+ resources.reduce(OpenTelemetry::SDK::Resources::Resource.create({})) do |merged, r|
111
104
  merged.merge(r)
112
105
  end
113
- run_id_resource = OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
114
- base.merge(run_id_resource)
115
106
  end
116
- # rubocop:enable Metrics/MethodLength
117
107
 
118
108
  def extract_branch_name(resource)
119
109
  attrs = resource.attribute_enumerator.to_h
@@ -121,7 +111,7 @@ module Mergify
121
111
  @base_branch_name || attrs['vcs.ref.head.name']
122
112
  end
123
113
 
124
- # rubocop:disable Metrics/MethodLength
114
+ # rubocop:disable-next Metrics/MethodLength
125
115
  def create_otlp_exporter(endpoint)
126
116
  require 'opentelemetry-exporter-otlp'
127
117
  original_env = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', nil)
@@ -140,9 +130,8 @@ module Mergify
140
130
  end
141
131
  end
142
132
  end
143
- # rubocop:enable Metrics/MethodLength
144
133
 
145
- # rubocop:disable Metrics/MethodLength
134
+ # rubocop:disable-next Metrics/MethodLength
146
135
  def load_flaky_detector
147
136
  return unless @token && @repo_name
148
137
 
@@ -161,7 +150,6 @@ module Mergify
161
150
  rescue StandardError => e
162
151
  @flaky_detector_error_message = "Could not load flaky detector: #{e.message}"
163
152
  end
164
- # rubocop:enable Metrics/MethodLength
165
153
 
166
154
  def load_quarantine
167
155
  return unless @token && @repo_name && @branch_name
@@ -175,6 +163,5 @@ module Mergify
175
163
  )
176
164
  end
177
165
  end
178
- # rubocop:enable Metrics/ClassLength
179
166
  end
180
167
  end
@@ -10,7 +10,7 @@ module Mergify
10
10
  module_function
11
11
 
12
12
  # rubocop:disable Metrics/MethodLength,Metrics/BlockLength,Metrics/AbcSize
13
- # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
13
+ # rubocop:disable-next Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
14
14
  def setup!
15
15
  ::RSpec.configure do |config|
16
16
  # Add formatter when in CI
@@ -111,7 +111,6 @@ module Mergify
111
111
  end
112
112
  end
113
113
  end
114
- # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
115
114
  # rubocop:enable Metrics/MethodLength,Metrics/BlockLength,Metrics/AbcSize
116
115
  end
117
116
  end
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'json'
5
- require 'uri'
6
3
  require 'set'
7
4
  require_relative 'utils'
5
+ require_relative 'native'
6
+ require_relative 'version'
8
7
 
9
8
  module Mergify
10
9
  module RSpec
@@ -15,7 +14,7 @@ module Mergify
15
14
  class FlakyDetectionDisabledError < StandardError; end
16
15
 
17
16
  # Manages intelligent test rerunning with budget constraints for flaky detection.
18
- # rubocop:disable Metrics/ClassLength
17
+ # rubocop:disable-next Metrics/ClassLength
19
18
  class FlakyDetector
20
19
  # Per-test tracking metrics.
21
20
  class TestMetrics
@@ -74,38 +73,28 @@ module Mergify
74
73
  @tests_to_process = []
75
74
  @budget = 0.0
76
75
 
77
- fetch_context
78
- validate!
76
+ @context = fetch_context
77
+ raise FlakyDetectionDisabledError unless Native::Budget.should_run(@context, @mode)
79
78
  end
80
79
 
81
- # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
80
+ # Which tests this session reruns, and how long it may spend doing it,
81
+ # both come from the shared budget engine -- so a Ruby suite and a Python
82
+ # one facing the same context spend the same time.
83
+ #
84
+ # The engine sizes the budget from the existing tests *in this session*,
85
+ # where this class counted every existing test the context knew about. A
86
+ # session running part of a suite was handed the whole suite's budget; it
87
+ # now gets its own.
82
88
  def prepare_for_session(test_ids)
83
- existing = Set.new(@context[:existing_test_names])
84
- unhealthy = Set.new(@context[:unhealthy_test_names])
89
+ plan = Native::Budget.compute(@context, @mode, test_ids, [])
85
90
 
86
- @tests_to_process =
87
- if @mode == 'new'
88
- test_ids.reject { |id| existing.include?(id) }
89
- else
90
- test_ids.select { |id| unhealthy.include?(id) }
91
- end
92
-
93
- budget_ratio = if @mode == 'new'
94
- @context[:budget_ratio_for_new_tests]
95
- else
96
- @context[:budget_ratio_for_unhealthy_tests]
97
- end
98
-
99
- mean_duration_s = @context[:existing_tests_mean_duration_ms] / 1000.0
100
- existing_count = @context[:existing_test_names].size
101
- min_budget_s = @context[:min_budget_duration_ms] / 1000.0
102
-
103
- ratio_budget = budget_ratio * mean_duration_s * existing_count
104
- @budget = [ratio_budget, min_budget_s].max
91
+ @tests_to_process = plan['tests_to_process']
92
+ # The engine works in milliseconds; everything downstream compares
93
+ # against RSpec's durations, which are seconds.
94
+ @budget = plan['available_budget_ms'] / 1000.0
105
95
  end
106
- # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
107
96
 
108
- # rubocop:disable Metrics/MethodLength
97
+ # rubocop:disable-next Metrics/MethodLength
109
98
  def fill_metrics_from_report(test_id, phase, duration, status)
110
99
  if status == :skipped
111
100
  @metrics.delete(test_id)
@@ -114,7 +103,7 @@ module Mergify
114
103
 
115
104
  return unless @tests_to_process.include?(test_id)
116
105
 
117
- if test_id.length > @context[:max_test_name_length]
106
+ if test_id.length > @context['max_test_name_length']
118
107
  @over_length_tests.add(test_id)
119
108
  return
120
109
  end
@@ -125,7 +114,6 @@ module Mergify
125
114
  @metrics[test_id] ||= TestMetrics.new
126
115
  @metrics[test_id].fill_from_report(phase, duration, status)
127
116
  end
128
- # rubocop:enable Metrics/MethodLength
129
117
 
130
118
  def rerunning_test?(test_id)
131
119
  @metrics.key?(test_id) && @metrics[test_id].rerun_count >= 1
@@ -155,7 +143,7 @@ module Mergify
155
143
  return false unless @metrics.key?(test_id)
156
144
 
157
145
  metrics = @metrics[test_id]
158
- min_exec = @context[:min_test_execution_count]
146
+ min_exec = @context['min_test_execution_count']
159
147
  (metrics.initial_duration * min_exec) > metrics.remaining_time
160
148
  end
161
149
 
@@ -163,14 +151,14 @@ module Mergify
163
151
  return false unless @metrics.key?(test_id)
164
152
 
165
153
  metrics = @metrics[test_id]
166
- metrics.will_exceed_deadline? || metrics.rerun_count >= @context[:max_test_execution_count]
154
+ metrics.will_exceed_deadline? || metrics.rerun_count >= @context['max_test_execution_count']
167
155
  end
168
156
 
169
157
  def test_metrics(test_id)
170
158
  @metrics[test_id]
171
159
  end
172
160
 
173
- # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
161
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
174
162
  def make_report
175
163
  lines = []
176
164
  lines << 'Mergify Flaky Detection Report'
@@ -195,60 +183,19 @@ module Mergify
195
183
 
196
184
  lines.join("\n")
197
185
  end
198
- # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
199
186
 
200
187
  private
201
188
 
202
- # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
189
+ # A nil context means the repository has not opted into flaky detection,
190
+ # which is the expected default rather than a failure.
203
191
  def fetch_context
204
- owner, repo = Utils.split_full_repo_name(@full_repository_name)
205
- uri = URI("#{@url}/v1/ci/#{owner}/repositories/#{repo}/flaky-detection-context")
206
-
207
- http = Net::HTTP.new(uri.host, uri.port)
208
- http.use_ssl = uri.scheme == 'https'
209
- http.open_timeout = 10
210
- http.read_timeout = 10
211
-
212
- request = Net::HTTP::Get.new(uri)
213
- request['Authorization'] = "Bearer #{@token}"
214
-
215
- response = http.request(request)
216
- case response.code.to_i
217
- when 200
218
- parse_context(response.body)
219
- when 404
220
- # A 404 means the repository has not opted into flaky detection; this
221
- # is the expected default, not an error.
222
- raise FlakyDetectionDisabledError
223
- else
224
- raise "Mergify API returned HTTP #{response.code}"
225
- end
226
- end
227
- # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
228
-
229
- # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
230
- def parse_context(body)
231
- data = JSON.parse(body, symbolize_names: true)
232
- @context = {
233
- budget_ratio_for_new_tests: data[:budget_ratio_for_new_tests].to_f,
234
- budget_ratio_for_unhealthy_tests: data[:budget_ratio_for_unhealthy_tests].to_f,
235
- existing_test_names: Array(data[:existing_test_names]),
236
- existing_tests_mean_duration_ms: data[:existing_tests_mean_duration_ms].to_f,
237
- unhealthy_test_names: Array(data[:unhealthy_test_names]),
238
- max_test_execution_count: data[:max_test_execution_count].to_i,
239
- max_test_name_length: data[:max_test_name_length].to_i,
240
- min_budget_duration_ms: data[:min_budget_duration_ms].to_f,
241
- min_test_execution_count: data[:min_test_execution_count].to_i
242
- }
243
- end
244
- # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
192
+ raise FlakyDetectionDisabledError unless Native.available?
245
193
 
246
- def validate!
247
- return unless @mode == 'new' && @context[:existing_test_names].empty?
194
+ owner, repo = Utils.split_full_repo_name(@full_repository_name)
195
+ context = Native::Client.new(@url, @token, owner, repo, VERSION).fetch_flaky_context
196
+ raise FlakyDetectionDisabledError if context.nil?
248
197
 
249
- # Without a baseline, `new` mode would treat every test as new and rerun
250
- # the whole suite. Skip instead of surfacing an error.
251
- raise FlakyDetectionDisabledError
198
+ context
252
199
  end
253
200
 
254
201
  def remaining_budget
@@ -264,6 +211,5 @@ module Mergify
264
211
  @tests_to_process.count { |id| !@metrics.key?(id) || @metrics[id].deadline.nil? }
265
212
  end
266
213
  end
267
- # rubocop:enable Metrics/ClassLength
268
214
  end
269
215
  end
@@ -8,7 +8,7 @@ module Mergify
8
8
  # RSpec formatter that creates OpenTelemetry spans for Mergify Test Insights and
9
9
  # prints a terminal report. It is purely observational and does not modify
10
10
  # test execution.
11
- # rubocop:disable Metrics/ClassLength
11
+ # rubocop:disable-next Metrics/ClassLength
12
12
  class Formatter < ::RSpec::Core::Formatters::BaseFormatter
13
13
  ::RSpec::Core::Formatters.register self,
14
14
  :start,
@@ -17,7 +17,7 @@ module Mergify
17
17
  :example_pending,
18
18
  :stop
19
19
 
20
- # rubocop:disable Metrics/MethodLength
20
+ # rubocop:disable-next Metrics/MethodLength
21
21
  def start(notification)
22
22
  super
23
23
 
@@ -34,7 +34,6 @@ module Mergify
34
34
  @has_error = false
35
35
  @example_spans = {}
36
36
  end
37
- # rubocop:enable Metrics/MethodLength
38
37
 
39
38
  def example_started(notification)
40
39
  return unless @ci_insights&.tracer && @session_span
@@ -51,7 +50,7 @@ module Mergify
51
50
  @example_spans[example.id] = span
52
51
  end
53
52
 
54
- # rubocop:disable Metrics/MethodLength
53
+ # rubocop:disable-next Metrics/MethodLength
55
54
  def example_finished(notification)
56
55
  return unless @example_spans
57
56
 
@@ -73,7 +72,6 @@ module Mergify
73
72
 
74
73
  span.finish
75
74
  end
76
- # rubocop:enable Metrics/MethodLength
77
75
 
78
76
  def example_pending(notification)
79
77
  return unless @example_spans
@@ -149,7 +147,7 @@ module Mergify
149
147
  @session_span.finish
150
148
  end
151
149
 
152
- # rubocop:disable Metrics/MethodLength
150
+ # rubocop:disable-next Metrics/MethodLength
153
151
  def print_report
154
152
  output.puts ''
155
153
  output.puts '--- Mergify CI ---'
@@ -165,7 +163,6 @@ module Mergify
165
163
  output.puts "MERGIFY_TEST_RUN_ID=#{@ci_insights.test_run_id}"
166
164
  output.puts '------------------'
167
165
  end
168
- # rubocop:enable Metrics/MethodLength
169
166
 
170
167
  def print_configuration_warnings
171
168
  output.puts 'WARNING: MERGIFY_TOKEN is not set. Traces will not be sent to Mergify.' unless @ci_insights.token
@@ -217,6 +214,5 @@ module Mergify
217
214
  output.puts 'Documentation: https://docs.mergify.com/ci-insights/test-frameworks/'
218
215
  end
219
216
  end
220
- # rubocop:enable Metrics/ClassLength
221
217
  end
222
218
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mergify
4
+ module RSpec
5
+ # The Rust extension over mergify-ci-core: CI detection shared with the
6
+ # pytest and TypeScript clients.
7
+ #
8
+ # Loading is best-effort by design. A precompiled gem carries one extension
9
+ # per Ruby under `<ruby>/`, the source gem compiles one alongside this file,
10
+ # and a platform we publish neither for gets neither. Detection is one input
11
+ # to telemetry, not the point of the gem, so an absent extension degrades
12
+ # what we can report rather than breaking the suite under test -- the same
13
+ # fail-open posture the napi binding takes when a platform has no prebuilt
14
+ # binary. `load_error` keeps the reason, for callers that want to say so.
15
+ module Native
16
+ # Raised when a Mergify API call fails outright.
17
+ #
18
+ # Distinct from StandardError on purpose: callers degrade on an API
19
+ # failure, and a bare rescue there would swallow genuine bugs in the
20
+ # binding as though the backend were down.
21
+ class ApiError < StandardError; end
22
+
23
+ class << self
24
+ # The LoadError that prevented the extension loading, or nil.
25
+ attr_accessor :load_error
26
+
27
+ def available?
28
+ load_error.nil?
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
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
45
+ end
46
+
47
+ unless Mergify::RSpec::Native.available?
48
+ # Stand in for the extension so callers can just ask, and get the same answer
49
+ # they would get from a machine that is not in CI. Branching on availability
50
+ # at every call site would only spread the same nil back through the caller.
51
+ module Mergify
52
+ module RSpec
53
+ module Native
54
+ class << self
55
+ def detect_provider
56
+ nil
57
+ end
58
+
59
+ def detect_repository_name
60
+ nil
61
+ end
62
+
63
+ def detect_attributes
64
+ {}
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'json'
5
- require 'uri'
6
3
  require 'set'
7
4
  require_relative 'utils'
5
+ require_relative 'native'
6
+ require_relative 'version'
8
7
 
9
8
  module Mergify
10
9
  module RSpec
11
10
  # Fetches quarantined test names from the Mergify API and tracks which are used.
11
+ #
12
+ # The fetch itself -- pagination, the RFC 8288 `next` links, the status
13
+ # codes that mean "not subscribed" rather than "broken" -- belongs to the
14
+ # shared Rust client now, so every Mergify test client reads a quarantine
15
+ # list the same way. What stays here is what RSpec cares about: which of
16
+ # those tests this session actually ran, and the report at the end.
12
17
  class Quarantine
13
18
  attr_reader :quarantined_tests, :init_error_msg
14
19
 
@@ -19,10 +24,7 @@ module Mergify
19
24
  @used_tests = Set.new
20
25
  @init_error_msg = nil
21
26
 
22
- owner, repo = Utils.split_full_repo_name(repo_name)
23
- fetch_quarantined_tests(api_url, token, owner, repo, branch_name)
24
- rescue Utils::InvalidRepositoryFullNameError => e
25
- @init_error_msg = e.message
27
+ fetch(api_url, token, branch_name)
26
28
  end
27
29
 
28
30
  def include?(example_id)
@@ -33,7 +35,7 @@ module Mergify
33
35
  @used_tests.add(example_id)
34
36
  end
35
37
 
36
- # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
38
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
37
39
  def report
38
40
  used, unused = @quarantined_tests.partition { |t| @used_tests.include?(t) }
39
41
 
@@ -50,85 +52,24 @@ module Mergify
50
52
  unused.each { |t| lines << " - #{t}" }
51
53
  lines.join("\n")
52
54
  end
53
- # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
54
55
 
55
56
  private
56
57
 
57
- def fetch_quarantined_tests(api_url, token, owner, repo, branch_name)
58
- uri = URI("#{api_url}/v1/ci/#{owner}/repositories/#{repo}/quarantines")
59
- uri.query = URI.encode_www_form(branch: branch_name, per_page: 100)
60
- collected = walk_paginated_quarantines(uri, token)
61
- @quarantined_tests = collected if collected
62
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError => e
63
- @init_error_msg = "Failed to connect to Mergify API: #{e.message}"
64
- rescue JSON::ParserError => e
65
- @init_error_msg = "Mergify API returned a malformed quarantine list: #{e.message}"
66
- end
67
-
68
- # Follows the RFC 5988 `next` link until exhausted. Returns the full list
69
- # on success, or `nil` when the run was aborted (subscription missing or
70
- # an error already recorded in @init_error_msg).
71
- # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
72
- def walk_paginated_quarantines(uri, token)
73
- collected = []
74
- # Guard against a server returning a `next` link that loops back to a
75
- # URL we have already fetched.
76
- seen = Set.new
77
- while uri
78
- if seen.include?(uri.to_s)
79
- @init_error_msg = 'Mergify API returned a cyclic `next` link, aborting.'
80
- return nil
81
- end
82
- seen.add(uri.to_s)
83
-
84
- response = perform_request(uri, token)
85
- case response.code.to_i
86
- when 200
87
- data = JSON.parse(response.body)
88
- collected.concat(data.fetch('quarantined_tests', []).map { |t| t['test_name'] })
89
- next_url = parse_next_link(response['Link'])
90
- uri = next_url ? URI(next_url) : nil
91
- when 402
92
- return nil
93
- else
94
- @init_error_msg = "Mergify API returned HTTP #{response.code}"
95
- return nil
96
- end
58
+ # A nil list means the repository has no quarantine subscription, which is
59
+ # not an error: the session simply quarantines nothing. Anything that went
60
+ # genuinely wrong is recorded and the suite carries on -- this plugin has
61
+ # never let the backend fail a test run.
62
+ def fetch(api_url, token, branch_name)
63
+ unless Native.available?
64
+ @init_error_msg = "Mergify native extension unavailable: #{Native.load_error}"
65
+ return
97
66
  end
98
- collected
99
- end
100
- # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
101
-
102
- def perform_request(uri, token)
103
- http = Net::HTTP.new(uri.host, uri.port)
104
- http.use_ssl = uri.scheme == 'https'
105
- http.open_timeout = 10
106
- http.read_timeout = 10
107
-
108
- request = Net::HTTP::Get.new(uri)
109
- request['Authorization'] = "Bearer #{token}"
110
- http.request(request)
111
- end
112
67
 
113
- # Parses RFC 8288 Link headers tolerantly: accepts both quoted
114
- # (`rel="next"`) and token (`rel=next`) forms, and matches when `next`
115
- # is one of several space-separated rel-types (`rel="next prev"`).
116
- def parse_next_link(link_header)
117
- return nil if link_header.nil? || link_header.empty?
118
-
119
- link_header.split(',').each do |part|
120
- match = part.strip.match(/\A<([^>]+)>\s*;\s*(.+)\z/)
121
- next unless match && next_rel?(match[2])
122
-
123
- return match[1]
124
- end
125
- nil
126
- end
127
-
128
- def next_rel?(params)
129
- params.scan(/rel\s*=\s*(?:"([^"]+)"|([^\s;,]+))/i).any? do |quoted, token|
130
- (quoted || token).split.include?('next')
131
- end
68
+ owner, repo = Utils.split_full_repo_name(@repo_name)
69
+ client = Native::Client.new(api_url, token, owner, repo, VERSION)
70
+ @quarantined_tests = client.fetch_quarantine(branch_name) || []
71
+ rescue Utils::InvalidRepositoryFullNameError, Native::ApiError => e
72
+ @init_error_msg = e.message
132
73
  end
133
74
  end
134
75
  end