rspec-mergify 2026.10.8 → 2026.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -4
- data/THIRD_PARTY_LICENSES +20 -20
- data/lib/mergify/rspec/configuration.rb +60 -15
- data/lib/mergify/rspec/test_retry.rb +46 -0
- data/lib/mergify/rspec/test_run.rb +14 -10
- data/lib/mergify/rspec/tries.rb +21 -0
- data/lib/mergify/rspec/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: addf160bfa2fcbe5d11558f76b497bdae4342d27ad69b4caadc7659854dedfde
|
|
4
|
+
data.tar.gz: 96d6a5c574bd767230faf016689753e8ad2f8eac4e9e68c3644cff8fe9fba47e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74839e7f18bcec97b58f5801c4c651f61dfe3f522188442f10609937ef1de454c832ef4776f3c6b7146415770ca9d6554e6b3b138742345a27f9ca17850956b5
|
|
7
|
+
data.tar.gz: 447fa60ca2e3f431980cb8294ea39c718b5969d263d1eb6af262dda3fd26f29c6941fbdf7e86bca736bef607b0a85563d1b8a557425dada761230edaacbb0f69
|
data/README.md
CHANGED
|
@@ -6,6 +6,7 @@ RSpec plugin for [Mergify Test Engine](https://docs.mergify.com/test-engine/).
|
|
|
6
6
|
|
|
7
7
|
- **Test tracing** — Sends OpenTelemetry traces for every test to Mergify's API
|
|
8
8
|
- **Flaky test detection** — Intelligently reruns tests to detect flakiness with budget constraints
|
|
9
|
+
- **Test retry** — On the default branch, runs a failing example again, once rspec-retry's tries failed, within a time budget Mergify sets
|
|
9
10
|
- **Test quarantine** — Quarantines failing tests so they don't block CI
|
|
10
11
|
- **Test selection** — Runs first the examples a merge-queue bisection step failed in the step above, and stops once one fails again
|
|
11
12
|
|
|
@@ -25,12 +26,12 @@ The gem runs on `mergify-ci-core`, which Bundler installs with it. That gem ship
|
|
|
25
26
|
|
|
26
27
|
| Platform | Requirement |
|
|
27
28
|
|---|---|
|
|
28
|
-
| Linux x86_64 / aarch64 (glibc) | **glibc 2.
|
|
29
|
-
| Linux x86_64 / aarch64 (musl) | Alpine
|
|
29
|
+
| Linux x86_64 / aarch64 (glibc) | **glibc 2.28 or newer** — Debian 10+, Ubuntu 20.04+, RHEL 8+ and its rebuilds, Amazon Linux 2023 |
|
|
30
|
+
| Linux x86_64 / aarch64 (musl) | Alpine |
|
|
30
31
|
| macOS arm64 / x86_64 | — |
|
|
31
32
|
| Windows x64 (UCRT) | — |
|
|
32
33
|
|
|
33
|
-
Anywhere else, and on glibc older than 2.
|
|
34
|
+
Anywhere else, and on glibc older than 2.28 — Amazon Linux 2, CentOS 7, Ubuntu 18.04 — the extension cannot load. The gem still installs and your suite still runs, but nothing is reported to Mergify. Pin the last pure-Ruby release on those systems:
|
|
34
35
|
|
|
35
36
|
```ruby
|
|
36
37
|
gem 'rspec-mergify', '0.1.4'
|
|
@@ -53,6 +54,7 @@ The plugin activates automatically when running in CI (detected via the `CI` env
|
|
|
53
54
|
| `MERGIFY_TRACEPARENT` | W3C distributed trace context | — |
|
|
54
55
|
| `MERGIFY_TEST_JOB_NAME` | Mergify test job name | — |
|
|
55
56
|
| `MERGIFY_FAIL_FIRST_ENABLE` | Set to `false` to never run first what a merge-queue bisection step of this job failed before | `true` |
|
|
57
|
+
| `MERGIFY_TEST_RETRY_ENABLE` | Set to `false` to have this job retry no failing test, whatever the repository sets | `true` |
|
|
56
58
|
|
|
57
59
|
The names these replace, `RSPEC_MERGIFY_ENABLE` and `RSPEC_MERGIFY_DEBUG`, are
|
|
58
60
|
deprecated: they still work when the new name is not set, and the summary says
|
|
@@ -143,7 +145,7 @@ test.
|
|
|
143
145
|
|
|
144
146
|
[parallel_tests](https://github.com/grosser/parallel_tests) and [turbo_tests](https://github.com/serpapi/turbo_tests) need no setup. Each worker is a separate RSpec process, and each reports to Mergify as its own session:
|
|
145
147
|
|
|
146
|
-
- flaky detection sizes each worker's rerun budget from the tests that worker runs;
|
|
148
|
+
- flaky detection sizes each worker's rerun budget from the tests that worker runs, and test retry gives each worker a budget of its own;
|
|
147
149
|
- each worker prints its own Mergify report, so a report covers only that worker's tests.
|
|
148
150
|
|
|
149
151
|
Suites split across CI jobs (knapsack, `circleci tests split`, Buildkite `parallelism`) behave the same way, one session per job.
|
data/THIRD_PARTY_LICENSES
CHANGED
|
@@ -8,8 +8,8 @@ The source code of each component is available from the repository listed
|
|
|
8
8
|
next to it, as the Mozilla Public License 2.0 requires for the components
|
|
9
9
|
under it.
|
|
10
10
|
|
|
11
|
-
Apache License 2.0 (
|
|
12
|
-
MIT License (
|
|
11
|
+
Apache License 2.0 (229)
|
|
12
|
+
MIT License (74)
|
|
13
13
|
Unicode License v3 (19)
|
|
14
14
|
ISC License (5)
|
|
15
15
|
BSD 3-Clause "New" or "Revised" License (4)
|
|
@@ -23,7 +23,7 @@ Apache License 2.0 (Apache-2.0)
|
|
|
23
23
|
|
|
24
24
|
Used by:
|
|
25
25
|
- cmov 0.5.4 (https://github.com/RustCrypto/utils)
|
|
26
|
-
- ctutils 0.4.
|
|
26
|
+
- ctutils 0.4.6 (https://github.com/RustCrypto/utils)
|
|
27
27
|
- encode_unicode 1.0.0 (https://github.com/tormol/encode_unicode)
|
|
28
28
|
- encoding_rs 0.8.42 (https://github.com/hsivonen/encoding_rs)
|
|
29
29
|
- multiversion_no_op 1.0.0 (https://github.com/hsivonen/multiversion_no_op)
|
|
@@ -474,11 +474,7 @@ Used by:
|
|
|
474
474
|
- windows-link 0.2.1 (https://github.com/microsoft/windows-rs)
|
|
475
475
|
- windows-result 0.4.1 (https://github.com/microsoft/windows-rs)
|
|
476
476
|
- windows-strings 0.5.1 (https://github.com/microsoft/windows-rs)
|
|
477
|
-
- windows-sys 0.52.0 (https://github.com/microsoft/windows-rs)
|
|
478
477
|
- windows-sys 0.61.2 (https://github.com/microsoft/windows-rs)
|
|
479
|
-
- windows-targets 0.52.6 (https://github.com/microsoft/windows-rs)
|
|
480
|
-
- windows_x86_64_gnu 0.52.6 (https://github.com/microsoft/windows-rs)
|
|
481
|
-
- windows_x86_64_msvc 0.52.6 (https://github.com/microsoft/windows-rs)
|
|
482
478
|
|
|
483
479
|
Apache License
|
|
484
480
|
Version 2.0, January 2004
|
|
@@ -2571,14 +2567,14 @@ limitations under the License.
|
|
|
2571
2567
|
Apache License 2.0 (Apache-2.0)
|
|
2572
2568
|
|
|
2573
2569
|
Used by:
|
|
2574
|
-
- futures-channel 0.3.
|
|
2575
|
-
- futures-core 0.3.
|
|
2570
|
+
- futures-channel 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2571
|
+
- futures-core 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2576
2572
|
- futures-executor 0.3.33 (https://github.com/rust-lang/futures-rs)
|
|
2577
|
-
- futures-io 0.3.
|
|
2578
|
-
- futures-macro 0.3.
|
|
2579
|
-
- futures-sink 0.3.
|
|
2580
|
-
- futures-task 0.3.
|
|
2581
|
-
- futures-util 0.3.
|
|
2573
|
+
- futures-io 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2574
|
+
- futures-macro 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2575
|
+
- futures-sink 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2576
|
+
- futures-task 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2577
|
+
- futures-util 0.3.34 (https://github.com/rust-lang/futures-rs)
|
|
2582
2578
|
- futures 0.3.33 (https://github.com/rust-lang/futures-rs)
|
|
2583
2579
|
|
|
2584
2580
|
Apache License
|
|
@@ -4882,6 +4878,7 @@ Apache License 2.0 (Apache-2.0)
|
|
|
4882
4878
|
Used by:
|
|
4883
4879
|
- ahash 0.8.12 (https://github.com/tkaitchuck/ahash)
|
|
4884
4880
|
- async-channel 2.5.0 (https://github.com/smol-rs/async-channel)
|
|
4881
|
+
- async-compression 0.4.51 (https://github.com/Nullus157/async-compression)
|
|
4885
4882
|
- async-executor 1.14.0 (https://github.com/smol-rs/async-executor)
|
|
4886
4883
|
- async-io 2.6.0 (https://github.com/smol-rs/async-io)
|
|
4887
4884
|
- async-lock 3.4.2 (https://github.com/smol-rs/async-lock)
|
|
@@ -4894,6 +4891,8 @@ Used by:
|
|
|
4894
4891
|
- bitflags 2.13.1 (https://github.com/bitflags/bitflags)
|
|
4895
4892
|
- blocking 1.7.0 (https://github.com/smol-rs/blocking)
|
|
4896
4893
|
- cfg-if 1.0.4 (https://github.com/rust-lang/cfg-if)
|
|
4894
|
+
- compression-codecs 0.4.46 (https://github.com/Nullus157/async-compression)
|
|
4895
|
+
- compression-core 0.4.34 (https://github.com/Nullus157/async-compression)
|
|
4897
4896
|
- concurrent-queue 2.5.0 (https://github.com/smol-rs/concurrent-queue)
|
|
4898
4897
|
- core-foundation-sys 0.8.7 (https://github.com/servo/core-foundation-rs)
|
|
4899
4898
|
- core-foundation 0.10.1 (https://github.com/servo/core-foundation-rs)
|
|
@@ -4951,7 +4950,7 @@ Used by:
|
|
|
4951
4950
|
- secret-service 5.2.0 (https://github.com/hwchen/secret-service-rs.git)
|
|
4952
4951
|
- security-framework-sys 2.17.0 (https://github.com/kornelski/rust-security-framework)
|
|
4953
4952
|
- security-framework 3.7.0 (https://github.com/kornelski/rust-security-framework)
|
|
4954
|
-
- self-replace 1.5.
|
|
4953
|
+
- self-replace 1.5.1 (https://github.com/mitsuhiko/self-replace)
|
|
4955
4954
|
- signal-hook-registry 1.4.8 (https://github.com/vorner/signal-hook)
|
|
4956
4955
|
- smallvec 1.15.2 (https://github.com/servo/rust-smallvec)
|
|
4957
4956
|
- socket2 0.6.5 (https://github.com/rust-lang/socket2)
|
|
@@ -8791,7 +8790,7 @@ THIS SOFTWARE.
|
|
|
8791
8790
|
ISC License (ISC)
|
|
8792
8791
|
|
|
8793
8792
|
Used by:
|
|
8794
|
-
- rustls-webpki 0.103.
|
|
8793
|
+
- rustls-webpki 0.103.16 (https://github.com/rustls/webpki)
|
|
8795
8794
|
|
|
8796
8795
|
Except as otherwise noted, this project is licensed under the following
|
|
8797
8796
|
(ISC-style) terms:
|
|
@@ -9826,10 +9825,10 @@ SOFTWARE.
|
|
|
9826
9825
|
MIT License (MIT)
|
|
9827
9826
|
|
|
9828
9827
|
Used by:
|
|
9829
|
-
- jsonschema-regex 0.60.
|
|
9830
|
-
- jsonschema-value 0.60.
|
|
9831
|
-
- jsonschema 0.60.
|
|
9832
|
-
- referencing 0.60.
|
|
9828
|
+
- jsonschema-regex 0.60.2 (https://github.com/Stranger6667/jsonschema)
|
|
9829
|
+
- jsonschema-value 0.60.2 (https://github.com/Stranger6667/jsonschema)
|
|
9830
|
+
- jsonschema 0.60.2 (https://github.com/Stranger6667/jsonschema)
|
|
9831
|
+
- referencing 0.60.2 (https://github.com/Stranger6667/jsonschema)
|
|
9833
9832
|
|
|
9834
9833
|
MIT License
|
|
9835
9834
|
|
|
@@ -10083,6 +10082,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
10083
10082
|
MIT License (MIT)
|
|
10084
10083
|
|
|
10085
10084
|
Used by:
|
|
10085
|
+
- tokio-util 0.7.19 (https://github.com/tokio-rs/tokio)
|
|
10086
10086
|
- tokio 1.54.1 (https://github.com/tokio-rs/tokio)
|
|
10087
10087
|
|
|
10088
10088
|
MIT License
|
|
@@ -146,44 +146,87 @@ module Mergify
|
|
|
146
146
|
# engine around `example.run`, and its outcome read off the exception it
|
|
147
147
|
# left behind. Its first run is the example's first attempt, and the
|
|
148
148
|
# tries a retry plugin makes after it, inside that run; flaky detection's
|
|
149
|
-
# reruns follow, each a try alone.
|
|
149
|
+
# reruns follow, or test retry's, each a try alone.
|
|
150
150
|
def run_example(example, test_run)
|
|
151
151
|
name = test_run.names.of(example)
|
|
152
152
|
detector = test_run.flaky_detector
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
retrier = test_run.test_retrier
|
|
154
|
+
retrier = nil unless retrier&.answers_for?(name)
|
|
155
|
+
tries = run_first(example, name, test_run, detector, retrier)
|
|
156
|
+
if detector&.rerunning_test?(name)
|
|
157
|
+
detect_flakiness(example, name, detector, retrier, tries)
|
|
158
|
+
elsif retrier
|
|
159
|
+
retry_failure(example, name, retrier, tries)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
155
162
|
|
|
163
|
+
# Flaky detection reruns the example, within its budget. Where test
|
|
164
|
+
# retry answers for the example, the reruns answer its failure too.
|
|
165
|
+
def detect_flakiness(example, name, detector, retrier, tries)
|
|
156
166
|
initial_exception = example.exception
|
|
157
167
|
first_failure = tries.failure || initial_exception
|
|
158
|
-
first_failure = rerun(example, name, detector, first_failure) if detector.start_reruns(name)
|
|
168
|
+
first_failure = rerun(example, name, detector, retrier, first_failure) if detector.start_reruns(name)
|
|
159
169
|
|
|
160
170
|
# Which attempt decides the result is the session's rule: in 'new'
|
|
161
171
|
# mode any failure fails the test, the retry plugin's tries included,
|
|
162
172
|
# so a new flaky test cannot be merged; in 'unhealthy' mode the reruns
|
|
163
|
-
# only learn, and the example's own result stands
|
|
173
|
+
# only learn, and the example's own result stands, unless test retry
|
|
174
|
+
# says a rerun rescued it.
|
|
164
175
|
final_exception = detector.rerun_failure_fails_the_test? ? first_failure : initial_exception
|
|
176
|
+
final_exception = nil if retrier&.rescued?(name)
|
|
165
177
|
example.example.instance_variable_set(:@exception, final_exception)
|
|
166
178
|
end
|
|
167
179
|
|
|
180
|
+
# Test retry runs the failed example again, each time the session grants
|
|
181
|
+
# one more attempt, until one passes: the example then passes, as a pass
|
|
182
|
+
# of a retry plugin's would. Otherwise its first run's failure stands.
|
|
183
|
+
# Each attempt is one try, as flaky detection's reruns are.
|
|
184
|
+
def retry_failure(example, name, retrier, tries)
|
|
185
|
+
failure = example.exception
|
|
186
|
+
return unless failure
|
|
187
|
+
|
|
188
|
+
without_retries(example) { run_retries(example, name, retrier, tries.last_ms) }
|
|
189
|
+
example.example.instance_variable_set(:@exception, retrier.rescued?(name) ? nil : failure)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Each attempt the session grants the example, once the one before it
|
|
193
|
+
# failed after `attempt_ms`.
|
|
194
|
+
def run_retries(example, name, retrier, attempt_ms)
|
|
195
|
+
while retrier.grant?(name, attempt_ms)
|
|
196
|
+
reset_for_rerun(example)
|
|
197
|
+
attempt_ms = run_tries(example).last_ms
|
|
198
|
+
retrier.record(name, example.exception ? :failed : :passed, attempt_ms)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
168
202
|
# The first run of the example named `name`: its first attempt, timed by
|
|
169
203
|
# the engine up to the second try where a retry plugin made one, then
|
|
170
204
|
# each try after it, handed to the session once the first reached flaky
|
|
171
|
-
# detection. What the first attempt was is kept for its
|
|
172
|
-
|
|
205
|
+
# detection and test retry. What the first attempt was is kept for its
|
|
206
|
+
# span.
|
|
207
|
+
def run_first(example, name, test_run, detector, retrier)
|
|
173
208
|
detector&.attempt_started(name)
|
|
174
209
|
tries = run_tries(example) { detector&.attempt_stopped(name) }
|
|
175
|
-
|
|
210
|
+
first_attempt_ended(example, name, tries, detector, retrier)
|
|
176
211
|
tries.later.each { |try| test_run.record_retry(name, try.status, try.duration_ms) }
|
|
177
212
|
example.metadata[:mergify_first_attempt] = { try: tries.first, ended_at: tries.first_ended_at } if tries.first
|
|
178
213
|
tries
|
|
179
214
|
end
|
|
180
215
|
|
|
216
|
+
# The example's first attempt is over: flaky detection and test retry
|
|
217
|
+
# take it before the tries after it.
|
|
218
|
+
def first_attempt_ended(example, name, tries, detector, retrier)
|
|
219
|
+
status = first_failed?(example, tries) ? :failed : :passed
|
|
220
|
+
detector&.attempt_ended(name, status)
|
|
221
|
+
retrier&.record(name, status, tries.first_ms)
|
|
222
|
+
end
|
|
223
|
+
|
|
181
224
|
# Run the example once, noting each try a retry plugin makes of it; the
|
|
182
225
|
# block is called as a second one starts.
|
|
183
226
|
def run_tries(example, &)
|
|
184
227
|
tries = Tries.new(&)
|
|
185
228
|
example.metadata[:mergify_tries] = tries
|
|
186
|
-
example.run
|
|
229
|
+
tries.time { example.run }
|
|
187
230
|
tries
|
|
188
231
|
ensure
|
|
189
232
|
example.metadata.delete(:mergify_tries)
|
|
@@ -215,16 +258,16 @@ module Mergify
|
|
|
215
258
|
end
|
|
216
259
|
|
|
217
260
|
# Which attempt decides the result is the session's rule (see
|
|
218
|
-
# `
|
|
261
|
+
# `detect_flakiness`): answers the first failure, the reruns' included.
|
|
219
262
|
# Each rerun is one try: a retry plugin's tries would hide its failure
|
|
220
263
|
# behind the one that passed.
|
|
221
|
-
def rerun(example, name, detector, first_failure)
|
|
264
|
+
def rerun(example, name, detector, retrier, first_failure)
|
|
222
265
|
without_retries(example) do
|
|
223
266
|
until example.metadata[:is_last_rerun]
|
|
224
267
|
example.metadata[:is_last_rerun] = detector.last_rerun_for_test?(name)
|
|
225
268
|
reset_for_rerun(example)
|
|
226
269
|
|
|
227
|
-
run_attempt(example, name, detector)
|
|
270
|
+
run_attempt(example, name, detector, retrier)
|
|
228
271
|
first_failure ||= example.exception
|
|
229
272
|
end
|
|
230
273
|
end
|
|
@@ -260,13 +303,15 @@ module Mergify
|
|
|
260
303
|
end
|
|
261
304
|
|
|
262
305
|
# Runs what the around hook wraps -- the before and after hooks included
|
|
263
|
-
# -- timed by the session, and answers its outcome
|
|
264
|
-
|
|
306
|
+
# -- timed by the session, and answers its outcome; test retry, where it
|
|
307
|
+
# answers for the example, is told of it too.
|
|
308
|
+
def run_attempt(example, name, detector, retrier)
|
|
265
309
|
detector.attempt_started(name)
|
|
266
|
-
example
|
|
310
|
+
attempt = run_tries(example)
|
|
267
311
|
failed = example.exception || example.metadata.delete(:mergify_quarantined_failure)
|
|
268
312
|
status = failed ? :failed : :passed
|
|
269
313
|
detector.attempt_ended(name, status)
|
|
314
|
+
retrier&.record(name, status, attempt.last_ms)
|
|
270
315
|
status
|
|
271
316
|
end
|
|
272
317
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mergify
|
|
4
|
+
module RSpec
|
|
5
|
+
# Test retry for one RSpec run, as its session prepared it.
|
|
6
|
+
#
|
|
7
|
+
# Which failures it answers for, whether an example runs once more, and
|
|
8
|
+
# whether a retry rescued it are the session's: on a push to the default
|
|
9
|
+
# branch, every failing example but the ones the quarantine holds and the
|
|
10
|
+
# ones Mergify classifies as broken, up to the repository's retries, within
|
|
11
|
+
# one budget for the run. What is left here is how RSpec runs an attempt,
|
|
12
|
+
# which `Configuration#retry_failure` does.
|
|
13
|
+
class TestRetrier
|
|
14
|
+
# `reruns` is the session's.
|
|
15
|
+
def initialize(reruns)
|
|
16
|
+
@reruns = reruns
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Whether a failure of the example named `name` is test retry's to
|
|
20
|
+
# answer for: false where it does not run.
|
|
21
|
+
def answers_for?(name)
|
|
22
|
+
@reruns.retry_answers_for?(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# An attempt at the example ended with `status` (`:passed` or
|
|
26
|
+
# `:failed`) after `duration_ms`: its first, a rerun of flaky
|
|
27
|
+
# detection's, or one of retry's. Its first reaches flaky detection
|
|
28
|
+
# through its phases, so test retry is told apart.
|
|
29
|
+
def record(name, status, duration_ms)
|
|
30
|
+
@reruns.record_retry_attempt(name, status.to_s, duration_ms)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Whether the example, whose attempt just failed after `attempt_ms`,
|
|
34
|
+
# runs once more. Asked once that attempt is recorded, and once the
|
|
35
|
+
# retry plugin's tries of it are over.
|
|
36
|
+
def grant?(name, attempt_ms)
|
|
37
|
+
@reruns.grant_retry(name, attempt_ms)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether an attempt after the example's first passed, rescuing it.
|
|
41
|
+
def rescued?(name)
|
|
42
|
+
@reruns.rescued?(name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'utils'
|
|
4
4
|
require 'mergify/ci/core'
|
|
5
5
|
require_relative 'flaky_detection'
|
|
6
|
+
require_relative 'test_retry'
|
|
6
7
|
require_relative 'names'
|
|
7
8
|
require 'json'
|
|
8
9
|
require 'rspec/core/version'
|
|
@@ -22,14 +23,14 @@ module Mergify
|
|
|
22
23
|
# RSpec runs examples, in one process that can wait for every one put
|
|
23
24
|
# first.
|
|
24
25
|
fail_first: { unit: 'test', waits_for_all: true, workers_stop_the_run: false },
|
|
25
|
-
# RSpec reruns an example as it goes, and retries
|
|
26
|
-
# process that plans the run's
|
|
27
|
-
reruns: { plans_repeats: false, retries_failures:
|
|
26
|
+
# RSpec reruns an example as it goes, and retries a failed one in
|
|
27
|
+
# place, in one process that plans the run's budgets once.
|
|
28
|
+
reruns: { plans_repeats: false, retries_failures: true },
|
|
28
29
|
framework: { name: 'rspec', version: ::RSpec::Core::Version::STRING, language: 'ruby' }
|
|
29
30
|
}.freeze
|
|
30
31
|
|
|
31
32
|
attr_reader :session, :repo_name, :test_run_id,
|
|
32
|
-
:flaky_detector, :quarantined_tests,
|
|
33
|
+
:flaky_detector, :test_retrier, :quarantined_tests,
|
|
33
34
|
:captured_session_verdict, :names
|
|
34
35
|
|
|
35
36
|
# `doubles` has the session play Mergify in memory, answering and keeping
|
|
@@ -51,6 +52,7 @@ module Mergify
|
|
|
51
52
|
@test_run_id = @session.run_id
|
|
52
53
|
@reruns = nil
|
|
53
54
|
@flaky_detector = nil
|
|
55
|
+
@test_retrier = nil
|
|
54
56
|
@quarantined_tests = nil
|
|
55
57
|
@captured_session_verdict = nil
|
|
56
58
|
@examples_to_run = nil
|
|
@@ -150,15 +152,17 @@ module Mergify
|
|
|
150
152
|
@session.summary(Utils.parallel_worker?)
|
|
151
153
|
end
|
|
152
154
|
|
|
153
|
-
# Prepare flaky detection for the examples this run is
|
|
154
|
-
# their names, each one Mergify's lists may still hold
|
|
155
|
-
# names (`former_names`, a list by name). Which of them
|
|
156
|
-
# it runs at all, are the session's.
|
|
155
|
+
# Prepare flaky detection and test retry for the examples this run is
|
|
156
|
+
# about to run, by their names, each one Mergify's lists may still hold
|
|
157
|
+
# under its former names (`former_names`, a list by name). Which of them
|
|
158
|
+
# each reruns, and whether it runs at all, are the session's. No example
|
|
159
|
+
# opts out of test retry: RSpec's metadata has no key of Mergify's.
|
|
157
160
|
def prepare_reruns(names, former_names)
|
|
158
161
|
@session.state_former_names(former_names)
|
|
159
162
|
@reruns = @session.prepare_reruns(names)
|
|
160
163
|
detection = @reruns&.detection
|
|
161
164
|
@flaky_detector = detection && FlakyDetector.new(@reruns, detection)
|
|
165
|
+
@test_retrier = @reruns && TestRetrier.new(@reruns)
|
|
162
166
|
end
|
|
163
167
|
|
|
164
168
|
# Account for the real outcome of the example known by `keys` against
|
|
@@ -184,8 +188,8 @@ module Mergify
|
|
|
184
188
|
end
|
|
185
189
|
|
|
186
190
|
# What a run that reports loads before its examples: the session loads
|
|
187
|
-
# the quarantine list for the branch they target
|
|
188
|
-
#
|
|
191
|
+
# the quarantine list for the branch they target and the reruns'
|
|
192
|
+
# contexts, all at once. Without a list (no branch, a failed fetch, no
|
|
189
193
|
# token) nothing is quarantined, and its block says why.
|
|
190
194
|
def set_up
|
|
191
195
|
require_relative 'quarantine'
|
data/lib/mergify/rspec/tries.rb
CHANGED
|
@@ -25,6 +25,15 @@ module Mergify
|
|
|
25
25
|
@on_second = on_second
|
|
26
26
|
@started_ms = nil
|
|
27
27
|
@first_ended_at = nil
|
|
28
|
+
@run_ms = 0.0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Run the block, the run of the example its tries are made in, timed.
|
|
32
|
+
def time
|
|
33
|
+
started_ms = now_ms
|
|
34
|
+
yield
|
|
35
|
+
ensure
|
|
36
|
+
@run_ms = now_ms - started_ms
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
def start
|
|
@@ -53,6 +62,18 @@ module Mergify
|
|
|
53
62
|
@ended.drop(1)
|
|
54
63
|
end
|
|
55
64
|
|
|
65
|
+
# How long the first try took; the whole run where no try started (a
|
|
66
|
+
# skipped example, an `around` hook that raised before running it).
|
|
67
|
+
def first_ms
|
|
68
|
+
first ? first.duration_ms : @run_ms
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# How long the last try took, as `first_ms`: what the next attempt is
|
|
72
|
+
# expected to take.
|
|
73
|
+
def last_ms
|
|
74
|
+
@ended.empty? ? @run_ms : @ended.last.duration_ms
|
|
75
|
+
end
|
|
76
|
+
|
|
56
77
|
# The first failure a try left on the example, the quarantine's aside.
|
|
57
78
|
def failure
|
|
58
79
|
@ended.find(&:exception)&.exception
|
|
@@ -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 = '2026.10.
|
|
11
|
+
VERSION = '2026.10.10'
|
|
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: 2026.10.
|
|
4
|
+
version: 2026.10.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mergify
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 2026.10.
|
|
32
|
+
version: 2026.10.10
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 2026.10.
|
|
39
|
+
version: 2026.10.10
|
|
40
40
|
description: 'RSpec integration for Mergify Test Engine: OpenTelemetry tracing, flaky
|
|
41
41
|
test detection, and test quarantine.'
|
|
42
42
|
email:
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/mergify/rspec/formatter.rb
|
|
57
57
|
- lib/mergify/rspec/names.rb
|
|
58
58
|
- lib/mergify/rspec/quarantine.rb
|
|
59
|
+
- lib/mergify/rspec/test_retry.rb
|
|
59
60
|
- lib/mergify/rspec/test_run.rb
|
|
60
61
|
- lib/mergify/rspec/tries.rb
|
|
61
62
|
- lib/mergify/rspec/utils.rb
|