rspec-mergify 0.2.0 → 0.4.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.
@@ -1,20 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'opentelemetry-sdk'
4
3
  require 'rspec/core/version'
5
4
 
6
5
  module Mergify
7
6
  module RSpec
8
7
  module Resources
9
- # Detects OpenTelemetry Resource attributes for RSpec.
8
+ # The resource attributes only Ruby knows: which framework ran the suite,
9
+ # and in which language.
10
10
  module RSpec
11
11
  module_function
12
12
 
13
13
  def detect
14
- OpenTelemetry::SDK::Resources::Resource.create(
14
+ {
15
15
  'test.framework' => 'rspec',
16
- 'test.framework.version' => ::RSpec::Core::Version::STRING
17
- )
16
+ 'test.framework.version' => ::RSpec::Core::Version::STRING,
17
+ # Mergify takes a test's language from here when the span does not
18
+ # name one. The OpenTelemetry SDK sets this on its default resource,
19
+ # but this gem never used that resource, so nothing had ever sent it.
20
+ 'telemetry.sdk.language' => 'ruby'
21
+ }
18
22
  end
19
23
  end
20
24
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_selection'
4
+
5
+ module Mergify
6
+ module RSpec
7
+ # What the session concluded about each example, folded as RSpec reports
8
+ # it, and written to Mergify when the session ends.
9
+ #
10
+ # Test Selection answers a merge-queue rerun from its predecessor's
11
+ # verdict: which examples failed, whether anything ran at all. The verdict
12
+ # is sent in one request before the trace upload, so the answer is there
13
+ # seconds after the session ends rather than whenever trace ingestion gets
14
+ # to it (INC-2434).
15
+ #
16
+ # One status per example, its FINAL one -- the status RSpec reported once
17
+ # flaky detection's reruns were over, which is what decided the exit code.
18
+ # A verdict that disagreed with the exit code would either replay examples
19
+ # that did not gate, or skip the one that did.
20
+ class SessionVerdict
21
+ # A failure the quarantine absorbed is `quarantined_failed`: it did not
22
+ # gate the job, so a rerun must not replay it, but it did run and fail,
23
+ # so it is not green either.
24
+ PRECEDENCE = { 'passed' => 0, 'skipped' => 1, 'quarantined_failed' => 2, 'failed' => 3 }.freeze
25
+
26
+ # How sending went, for the report. `sent` is false both when nothing
27
+ # had to be sent and when the request failed; `error` tells them apart.
28
+ Result = Struct.new(:sent, :truncated, :error, keyword_init: true) do
29
+ # Said only when the verdict did not reach Mergify whole. Wording
30
+ # validated by Alexandre on 2026-09-15 (MRGFY-9313), pytest-mergify's
31
+ # verbatim; a change here is a product decision.
32
+ def report
33
+ if error
34
+ "#{TestSelection.wrap("Mergify couldn't record this run's results. If this merge-queue batch is " \
35
+ 'retried, this job will run its full test suite.')}\nError: #{error}\n"
36
+ elsif truncated
37
+ "#{TestSelection.wrap("Mergify recorded this run's counts but not its failing tests. If this " \
38
+ 'merge-queue batch is retried, this job will run its full test suite.')}\n"
39
+ end
40
+ end
41
+ end
42
+
43
+ def initialize
44
+ @final = {}
45
+ @runtime_seconds = 0.0
46
+ end
47
+
48
+ def record(example_id, status, run_time)
49
+ @runtime_seconds += run_time.to_f
50
+ previous = @final[example_id]
51
+ @final[example_id] = status if previous.nil? || PRECEDENCE.fetch(status) > PRECEDENCE.fetch(previous)
52
+ end
53
+
54
+ def total_test_runtime_ms
55
+ (@runtime_seconds * 1000).to_i
56
+ end
57
+
58
+ # The engine's own definitions: `executed` counts every example that
59
+ # reached a status, a skipped one included, and `failed` includes the
60
+ # quarantined failures.
61
+ def counts
62
+ statuses = @final.values
63
+ {
64
+ 'executed_count' => statuses.size,
65
+ 'passed_count' => statuses.count('passed'),
66
+ 'failed_count' => statuses.count { |status| %w[failed quarantined_failed].include?(status) },
67
+ 'skipped_count' => statuses.count('skipped')
68
+ }
69
+ end
70
+
71
+ def failing_tests
72
+ ids_with('failed')
73
+ end
74
+
75
+ def quarantined_failing_tests
76
+ ids_with('quarantined_failed')
77
+ end
78
+
79
+ private
80
+
81
+ def ids_with(status)
82
+ @final.filter_map { |id, final| id if final == status }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require_relative 'utils'
5
+
6
+ module Mergify
7
+ module RSpec
8
+ # Raised by `TestSelection#resolve` when Mergify refuses to choose a
9
+ # selection for this run, carrying the explanation to show. The caller
10
+ # stops the run with it.
11
+ class TestSelectionRefused < StandardError; end
12
+
13
+ # Whether this run should execute only part of what it collected.
14
+ #
15
+ # A merge-queue rerun only needs to replay the examples that failed on the
16
+ # previous attempt of the same job. Mergify decides that server-side from
17
+ # the run's own identity AND from the fingerprint of what the run
18
+ # collected, which is why the request leaves only once RSpec has loaded
19
+ # and filtered its examples. The answer is one of:
20
+ #
21
+ # * `full` -- run everything;
22
+ # * `subset` -- run only `tests`;
23
+ # * `empty` -- run nothing: the previous attempt ran these examples and
24
+ # they passed, so the run exits green having executed none;
25
+ # * `refused` -- Mergify holds several candidate sessions for this job and
26
+ # will not guess between them, so the run FAILS with its explanation.
27
+ #
28
+ # Every error, and every answer outside that list, runs the whole suite:
29
+ # the feature can remove work, never correctness. pytest-mergify's
30
+ # `TestSelection`, answer for answer and sentence for sentence.
31
+ #
32
+ # When an answer cannot be honoured the run executes everything and says
33
+ # so in `not_applied_reason`, never by rewriting `selection` or `reason`:
34
+ # those two carry Mergify's word.
35
+ # rubocop:disable-next Metrics/ClassLength
36
+ class TestSelection
37
+ ENABLE_ENV = 'MERGIFY_TEST_SELECTION_ENABLE'
38
+
39
+ CLIENT_NAME = 'rspec-mergify'
40
+ DOCS_URL = 'https://docs.mergify.com/ci-insights/test-frameworks/rspec/'
41
+
42
+ # What a refusal says when the server sent no wording of its own. The
43
+ # copy belongs to the server; this is a fallback, not the message.
44
+ FALLBACK_REFUSAL_MESSAGE = <<~MESSAGE.chomp
45
+ Mergify Test Selection stopped this run.
46
+
47
+ Several runs of this job report to Mergify under the same name, and they run the same tests — so Mergify cannot tell which one this run repeats, and it will not guess which tests to skip.
48
+
49
+ If this job runs more than once (a build matrix, for example), give each run its own name with MERGIFY_TEST_JOB_NAME:
50
+ #{DOCS_URL}
51
+
52
+ If this job only runs once, this is unexpected — please contact Mergify support.
53
+ MESSAGE
54
+
55
+ NOT_APPLIED_SENTENCE =
56
+ "Mergify's answer didn't match the tests this run collected, so the full suite ran."
57
+
58
+ # One sentence per reason the full suite ran. pytest-mergify's
59
+ # `_FULL_RUN_SENTENCES` verbatim (validated by Alexandre on 2026-09-11,
60
+ # MRGFY-8978), naming this gem where they name a client. A change here is
61
+ # a product decision.
62
+ FULL_RUN_SENTENCES = {
63
+ 'no_predecessor' => 'First attempt of this batch, so the full suite ran.',
64
+ 'not_a_merge_queue_run' => "This job isn't part of a merge queue run, so the full suite ran.",
65
+ 'stale_run' => 'The batch branch was updated while this job was running, so the full suite ran.',
66
+ 'no_matching_test_session' =>
67
+ "The previous attempt didn't run this exact set of tests, so the full suite ran.",
68
+ 'matched_test_session_ran_no_test' => 'The previous attempt executed no tests, so the full suite ran.',
69
+ 'predecessor_unknown' => "Mergify couldn't tell which previous run to start from, so the full suite ran.",
70
+ 'indeterminate_test_session' =>
71
+ "Mergify couldn't tell which previous run to start from, so the full suite ran.",
72
+ 'matched_test_session_partially_processed' =>
73
+ "Mergify didn't have the complete results of the previous attempt, so the full suite ran.",
74
+ 'matched_test_session_dropped_cases' =>
75
+ "Mergify didn't have the complete results of the previous attempt, so the full suite ran.",
76
+ 'matched_test_session_declaration_unreadable' =>
77
+ "Mergify didn't have the complete results of the previous attempt, so the full suite ran.",
78
+ 'matched_test_session_incomplete' =>
79
+ 'The previous attempt stopped before running all of its tests, so the full suite ran.',
80
+ 'matched_test_session_failures_truncated' =>
81
+ 'The previous attempt had too many failures for Mergify to list, so the full suite ran.',
82
+ 'no_collection_fingerprint' =>
83
+ "This version of #{CLIENT_NAME} doesn't report what it collected, so the full suite ran. " \
84
+ 'Upgrade it to let Mergify reduce reruns.',
85
+ 'feature_disabled' => "Test selection isn't enabled for this organization yet, so the full suite ran.",
86
+ 'not_requested' => "Test selection isn't available for this repository, so the full suite ran.",
87
+ 'unrecognised_selection' =>
88
+ "Mergify answered in a way this version of #{CLIENT_NAME} doesn't understand, so the full suite " \
89
+ 'ran. Upgrade it to let Mergify reduce reruns.',
90
+ 'subset_served_without_tests' => NOT_APPLIED_SENTENCE,
91
+ 'subset_matched_no_collected_test' => NOT_APPLIED_SENTENCE,
92
+ 'subset_partly_absent_from_collection' => NOT_APPLIED_SENTENCE
93
+ }.freeze
94
+
95
+ # A newer engine may serve a reason this gem predates. The block must
96
+ # still read as a full run, and must never show the raw identifier.
97
+ UNKNOWN_REASON_SENTENCE = 'Mergify served the full suite.'
98
+
99
+ HEADER = '✂️ Test selection'
100
+
101
+ # How many re-executed examples the subset block lists before counting
102
+ # the rest.
103
+ LISTED_TESTS_MAX = 10
104
+
105
+ # The width the wording was validated at.
106
+ WRAP_WIDTH = 80
107
+
108
+ KNOWN_ANSWERS = %w[full subset empty refused].freeze
109
+
110
+ attr_reader :selection, :reason, :tests, :message, :init_error_msg,
111
+ :not_applied_reason, :kept_count, :deselected_count, :kept_tests
112
+
113
+ class << self
114
+ # Whether this job asked for test selection. Opt-in and per job: a job
115
+ # that has not opted in asks nothing, which is what lets Mergify tell
116
+ # a repository that never opted in from one that did (MRGFY-9172).
117
+ # Trimmed, and anything unrecognised is off -- the direction that runs
118
+ # the whole suite.
119
+ def enabled?
120
+ Utils::TRUTHY_STRINGS.include?(ENV.fetch(ENABLE_ENV, '').strip.downcase)
121
+ end
122
+
123
+ # What Mergify answered, as the binding hands it over.
124
+ def served(answer)
125
+ new(selection: answer['selection'], reason: answer['reason'], tests: answer['tests'] || [],
126
+ message: answer['message'], served: true)
127
+ end
128
+
129
+ # A run Mergify did not answer: the repository has no such feature, or
130
+ # the request failed. Both run everything, and neither was offered
131
+ # anything, so neither is echoed as an answer.
132
+ def unanswered(init_error_msg: nil)
133
+ new(selection: 'full', reason: 'not_requested', tests: [], message: nil,
134
+ served: false, init_error_msg: init_error_msg)
135
+ end
136
+ end
137
+
138
+ # rubocop:disable-next Metrics/ParameterLists,Metrics/MethodLength
139
+ def initialize(selection:, reason:, tests:, message:, served:, init_error_msg: nil)
140
+ @selection = selection
141
+ @reason = reason
142
+ @tests = tests
143
+ @message = message
144
+ @served = served
145
+ @init_error_msg = init_error_msg
146
+ @not_applied_reason = nil
147
+ @kept_count = nil
148
+ @deselected_count = 0
149
+ @kept_tests = []
150
+
151
+ # A subset is only honoured with a non-empty list, and an answer this
152
+ # gem predates is never acted on: both run everything, and say so.
153
+ if @selection == 'subset'
154
+ @not_applied_reason = 'subset_served_without_tests' if @tests.empty?
155
+ else
156
+ @not_applied_reason = 'unrecognised_selection' unless KNOWN_ANSWERS.include?(@selection)
157
+ @tests = []
158
+ end
159
+ end
160
+
161
+ def served?
162
+ @served
163
+ end
164
+
165
+ def refused?
166
+ @not_applied_reason.nil? && @selection == 'refused'
167
+ end
168
+
169
+ # Decide what the answer leaves of this collection to run: the ids to
170
+ # keep, or nil for all of them. Raises `TestSelectionRefused` on a
171
+ # refusal, carrying the server's explanation.
172
+ #
173
+ # Matching is by exact example id, the identity this gem uploads. A
174
+ # subset is honoured all or not at all: one served id this run did not
175
+ # collect declines the whole answer and runs everything, rather than a
176
+ # reduced run over an arbitrary part of what was asked for.
177
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
178
+ def resolve(ids)
179
+ return nil unless @not_applied_reason.nil?
180
+
181
+ raise TestSelectionRefused, @message.to_s.empty? ? FALLBACK_REFUSAL_MESSAGE : @message if refused?
182
+
183
+ if @selection == 'empty'
184
+ # A collection already empty is left alone: the run is then empty for
185
+ # a reason of its own, and announcing a skip over it would mislead.
186
+ return nil if ids.empty?
187
+
188
+ @deselected_count = ids.size
189
+ return Set.new
190
+ end
191
+
192
+ return nil unless @selection == 'subset'
193
+
194
+ subset = @tests.to_set
195
+ kept = ids.select { |id| subset.include?(id) }
196
+ matched = kept.to_set
197
+ # Identities, not counts, so a repeated id can never stand in for a
198
+ # missing one.
199
+ if matched != subset
200
+ @not_applied_reason =
201
+ matched.empty? ? 'subset_matched_no_collected_test' : 'subset_partly_absent_from_collection'
202
+ return nil
203
+ end
204
+
205
+ @kept_count = kept.size
206
+ @deselected_count = ids.size - kept.size
207
+ @kept_tests = kept
208
+ subset
209
+ end
210
+
211
+ # The block printed in the gem's "Mergify CI" report: what reduced this
212
+ # run, whether it was deliberate, and whether its green can be trusted.
213
+ # Prose, and no identifier from the API ever reaches it.
214
+ # rubocop:disable-next Metrics/MethodLength
215
+ def report
216
+ if @init_error_msg
217
+ # The error text on its own line: it is what support will ask for,
218
+ # and it usually carries a URL that wrapping would split.
219
+ return block("Mergify couldn't be asked whether this run could be reduced, so the full suite ran.") +
220
+ "Error: #{@init_error_msg}\n"
221
+ end
222
+
223
+ if refused?
224
+ # The server's explanation was printed when the run stopped; the
225
+ # block only says where to look.
226
+ return block('Mergify stopped this run before any test ran; its explanation is in the error above.')
227
+ end
228
+
229
+ return block(FULL_RUN_SENTENCES.fetch(@not_applied_reason, UNKNOWN_REASON_SENTENCE)) if @not_applied_reason
230
+ return empty_block if @selection == 'empty'
231
+ return subset_block if @selection == 'subset' && @kept_count
232
+
233
+ block(FULL_RUN_SENTENCES.fetch(@reason, UNKNOWN_REASON_SENTENCE))
234
+ end
235
+
236
+ private
237
+
238
+ def empty_block
239
+ skipped = @deselected_count
240
+ # Nothing was collected: the block is the title alone, rather than a
241
+ # paragraph about skipping "all 0 tests".
242
+ return "#{HEADER}\n" if skipped.zero?
243
+
244
+ passed, them =
245
+ skipped == 1 ? ['its only test passed back then', 'it'] : ["all #{skipped} tests passed back then", 'them']
246
+ block("The code under test hasn't changed since the previous attempt of this job, and #{passed}. " \
247
+ "Mergify skipped #{them}: the job is green, and no test was executed.")
248
+ end
249
+
250
+ # rubocop:disable-next Metrics/MethodLength
251
+ def subset_block
252
+ failed = @kept_count
253
+ skipped = @deselected_count
254
+ sentence =
255
+ if skipped.zero?
256
+ which, them =
257
+ failed == 1 ? ['its only test failed', 'it'] : ["all #{failed} of its tests failed", 'all of them']
258
+ "The code under test hasn't changed since the previous attempt of this job, where #{which}. " \
259
+ "Mergify re-executed #{them}:"
260
+ else
261
+ those = failed == 1 ? 'that one' : "those #{failed}"
262
+ "The code under test hasn't changed since the previous attempt of this job, where #{failed} of its " \
263
+ "#{count_tests(failed + skipped)} failed. Mergify re-executed only #{those} and skipped the " \
264
+ "#{skipped} that had already passed:"
265
+ end
266
+
267
+ lines = @kept_tests.first(LISTED_TESTS_MAX).map { |id| " #{id}" }
268
+ remaining = @kept_tests.size - LISTED_TESTS_MAX
269
+ lines << " … and #{remaining} more" if remaining.positive?
270
+ "#{block(sentence)}\n#{lines.join("\n")}\n"
271
+ end
272
+
273
+ def count_tests(count)
274
+ count == 1 ? '1 test' : "#{count} tests"
275
+ end
276
+
277
+ def block(text)
278
+ "#{HEADER}\n\n#{TestSelection.wrap(text)}\n"
279
+ end
280
+
281
+ class << self
282
+ # Greedy word wrap, the way Python's `textwrap.fill` does it for this
283
+ # prose: words are never split, and a word longer than the width sits
284
+ # on a line of its own.
285
+ def wrap(text, width = WRAP_WIDTH)
286
+ lines = []
287
+ text.split.each do |word|
288
+ if lines.empty? || lines.last.length + 1 + word.length > width
289
+ lines << word.dup
290
+ else
291
+ lines.last << ' ' << word
292
+ end
293
+ end
294
+ lines.join("\n")
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Mergify
6
+ module RSpec
7
+ # The little of OpenTelemetry this plugin actually used.
8
+ #
9
+ # A test run needs identifiers, a start and an end, some attributes and a
10
+ # status -- and the shared Rust client does the rest: the wire format, the
11
+ # compression, the retries, the size limit. The SDK brought samplers,
12
+ # context propagation machinery, batch processors and an exporter registry
13
+ # to do the part that fits here, and its dependency tree landed in every
14
+ # consumer's bundle. So the spans are assembled directly.
15
+ module Trace
16
+ # W3C traceparent: version-traceid-spanid-flags, all lowercase hex.
17
+ TRACEPARENT = /\A00-(?<trace_id>[0-9a-f]{32})-(?<span_id>[0-9a-f]{16})-[0-9a-f]{2}\z/
18
+
19
+ # A span being recorded, and once finished, the record itself.
20
+ class Span
21
+ attr_reader :name, :trace_id, :span_id, :parent_span_id, :attributes
22
+ attr_accessor :status, :status_message
23
+
24
+ def initialize(name:, trace_id:, span_id:, parent_span_id: nil, attributes: {})
25
+ @name = name
26
+ @trace_id = trace_id
27
+ @span_id = span_id
28
+ @parent_span_id = parent_span_id
29
+ @attributes = attributes.transform_keys(&:to_s)
30
+ @status = 'unset'
31
+ @status_message = nil
32
+ @start_unix_nano = Trace.now_unix_nano
33
+ @end_unix_nano = nil
34
+ end
35
+
36
+ # Ids travel as bytes and are read as hex -- in a traceparent, in a
37
+ # backend URL, in a log line.
38
+ def hex_trace_id
39
+ @trace_id.unpack1('H*')
40
+ end
41
+
42
+ def hex_span_id
43
+ @span_id.unpack1('H*')
44
+ end
45
+
46
+ def set_attribute(key, value)
47
+ @attributes[key.to_s] = value
48
+ end
49
+
50
+ def error!(message)
51
+ @status = 'error'
52
+ @status_message = message
53
+ end
54
+
55
+ def ok!
56
+ @status = 'ok'
57
+ end
58
+
59
+ def finish
60
+ @end_unix_nano ||= Trace.now_unix_nano
61
+ self
62
+ end
63
+
64
+ # The shape the binding accepts, which is the wire format's own.
65
+ # rubocop:disable-next Metrics/MethodLength
66
+ def to_h
67
+ {
68
+ 'name' => @name,
69
+ 'trace_id' => @trace_id,
70
+ 'span_id' => @span_id,
71
+ 'parent_span_id' => @parent_span_id,
72
+ 'start_unix_nano' => @start_unix_nano,
73
+ 'end_unix_nano' => @end_unix_nano || Trace.now_unix_nano,
74
+ 'attributes' => @attributes,
75
+ 'status' => @status,
76
+ 'status_message' => @status_message
77
+ }.compact
78
+ end
79
+ end
80
+
81
+ # Collects a run's spans and hands them to the client in one upload.
82
+ #
83
+ # Deliberately not streaming: a suite's spans are worth one request at the
84
+ # end, and exporting mid-run would put HTTP in the middle of the thing
85
+ # being timed. That was already why the gem replaced the SDK's batch
86
+ # processor with its own.
87
+ class Recorder
88
+ attr_reader :resource_attributes, :finished_spans, :trace_id
89
+
90
+ # `traceparent` is the W3C header a caller can hand down to put this
91
+ # run inside a trace it already started; the session span then hangs off
92
+ # that caller's span rather than starting a trace of its own.
93
+ def initialize(resource_attributes: {}, traceparent: nil)
94
+ @resource_attributes = resource_attributes.transform_keys(&:to_s)
95
+ inherited = Trace.parse_traceparent(traceparent)
96
+ @trace_id = inherited ? inherited.first : Trace.generate_trace_id
97
+ @root_parent_span_id = inherited&.last
98
+ @finished_spans = []
99
+ end
100
+
101
+ def start_span(name, parent: nil, attributes: {})
102
+ Span.new(
103
+ name: name,
104
+ trace_id: @trace_id,
105
+ span_id: Trace.generate_span_id,
106
+ parent_span_id: parent&.span_id || @root_parent_span_id,
107
+ attributes: attributes
108
+ )
109
+ end
110
+
111
+ def record(span)
112
+ @finished_spans << span.finish
113
+ span
114
+ end
115
+
116
+ def clear
117
+ @finished_spans = []
118
+ end
119
+ end
120
+
121
+ class << self
122
+ def now_unix_nano
123
+ (Time.now.to_r * 1_000_000_000).to_i
124
+ end
125
+
126
+ def generate_trace_id
127
+ SecureRandom.bytes(16)
128
+ end
129
+
130
+ def generate_span_id
131
+ SecureRandom.bytes(8)
132
+ end
133
+
134
+ # The trace this run belongs to, when a parent handed one down.
135
+ # Returns [trace_id, parent_span_id] as raw bytes, or nil.
136
+ def parse_traceparent(header)
137
+ match = TRACEPARENT.match(header.to_s)
138
+ return nil unless match
139
+
140
+ [[match[:trace_id]].pack('H*'), [match[:span_id]].pack('H*')]
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -45,6 +45,13 @@ module Mergify
45
45
  env_truthy?('CI') || env_truthy?('RSPEC_MERGIFY_ENABLE')
46
46
  end
47
47
 
48
+ # Returns true inside one of the processes parallel_tests or turbo_tests
49
+ # splits a suite across. Both set TEST_ENV_NUMBER, to an empty string for
50
+ # parallel_tests' first worker, so it is the key that counts.
51
+ def parallel_worker?
52
+ ENV.key?('TEST_ENV_NUMBER')
53
+ end
54
+
48
55
  # Split "owner/repo" into [owner, repo].
49
56
  # Raises InvalidRepositoryFullNameError when the format is wrong.
50
57
  def split_full_repo_name(full_repo_name)
@@ -8,6 +8,6 @@ module Mergify
8
8
  # which resolved against whatever repository happened to be the working
9
9
  # directory at load time -- the monorepo's namespaced tags here, the user's
10
10
  # own tags once the gem was installed.
11
- VERSION = '0.2.0'
11
+ VERSION = '0.4.0'
12
12
  end
13
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-mergify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mergify
@@ -9,34 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: opentelemetry-exporter-otlp
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '0.29'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '0.29'
26
- - !ruby/object:Gem::Dependency
27
- name: opentelemetry-sdk
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.4'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.4'
40
12
  - !ruby/object:Gem::Dependency
41
13
  name: rspec-core
42
14
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +41,9 @@ files:
69
41
  - lib/mergify/rspec/native.rb
70
42
  - lib/mergify/rspec/quarantine.rb
71
43
  - lib/mergify/rspec/resources/rspec.rb
72
- - lib/mergify/rspec/synchronous_batch_span_processor.rb
44
+ - lib/mergify/rspec/session_verdict.rb
45
+ - lib/mergify/rspec/test_selection.rb
46
+ - lib/mergify/rspec/trace.rb
73
47
  - lib/mergify/rspec/utils.rb
74
48
  - lib/mergify/rspec/version.rb
75
49
  - lib/rspec_mergify.rb
@@ -94,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
68
  - !ruby/object:Gem::Version
95
69
  version: '0'
96
70
  requirements: []
97
- rubygems_version: 4.0.16
71
+ rubygems_version: 4.0.20
98
72
  specification_version: 4
99
73
  summary: RSpec plugin for Mergify CI Insights
100
74
  test_files: []
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'opentelemetry-sdk'
4
-
5
- module Mergify
6
- module RSpec
7
- class ExportError < StandardError; end
8
-
9
- # A span processor that queues spans in memory and exports them all in one
10
- # batch when force_flush is called. This avoids HTTP requests during test
11
- # execution.
12
- class SynchronousBatchSpanProcessor < OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor
13
- def initialize(exporter)
14
- super
15
- @queue = []
16
- end
17
-
18
- def on_finish(span)
19
- return unless span.context.trace_flags.sampled?
20
-
21
- @queue << span.to_span_data
22
- end
23
-
24
- def force_flush(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
25
- spans = @queue.dup
26
- @queue.clear
27
- result = @span_exporter.export(spans)
28
- raise ExportError, 'Failed to export traces' unless result == OpenTelemetry::SDK::Trace::Export::SUCCESS
29
-
30
- OpenTelemetry::SDK::Trace::Export::SUCCESS
31
- end
32
- end
33
- end
34
- end