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.
- checksums.yaml +4 -4
- data/LICENSE +174 -672
- data/README.md +2 -2
- data/lib/mergify/rspec/ci_insights.rb +14 -27
- data/lib/mergify/rspec/configuration.rb +1 -2
- data/lib/mergify/rspec/flaky_detection.rb +30 -84
- data/lib/mergify/rspec/formatter.rb +4 -8
- data/lib/mergify/rspec/native.rb +70 -0
- data/lib/mergify/rspec/quarantine.rb +23 -82
- data/lib/mergify/rspec/utils.rb +11 -95
- data/lib/mergify/rspec/version.rb +7 -12
- metadata +7 -12
- data/lib/mergify/rspec/resources/buildkite.rb +0 -54
- data/lib/mergify/rspec/resources/ci.rb +0 -24
- data/lib/mergify/rspec/resources/git.rb +0 -35
- data/lib/mergify/rspec/resources/github_actions.rb +0 -60
- data/lib/mergify/rspec/resources/jenkins.rb +0 -55
- data/lib/mergify/rspec/resources/mergify.rb +0 -24
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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::
|
|
103
|
-
Resources::
|
|
104
|
-
Resources::
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
@context = fetch_context
|
|
77
|
+
raise FlakyDetectionDisabledError unless Native::Budget.should_run(@context, @mode)
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
#
|
|
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
|
-
|
|
84
|
-
unhealthy = Set.new(@context[:unhealthy_test_names])
|
|
89
|
+
plan = Native::Budget.compute(@context, @mode, test_ids, [])
|
|
85
90
|
|
|
86
|
-
@tests_to_process =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|