minitest-mergify 2026.10.8
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 +7 -0
- data/LICENSE +31 -0
- data/README.md +129 -0
- data/THIRD_PARTY_LICENSES +11060 -0
- data/lib/mergify/minitest/declarations.rb +41 -0
- data/lib/mergify/minitest/names.rb +128 -0
- data/lib/mergify/minitest/plugin.rb +106 -0
- data/lib/mergify/minitest/recorder.rb +208 -0
- data/lib/mergify/minitest/reruns.rb +97 -0
- data/lib/mergify/minitest/results.rb +65 -0
- data/lib/mergify/minitest/selection.rb +121 -0
- data/lib/mergify/minitest/test_run.rb +161 -0
- data/lib/mergify/minitest/version.rb +9 -0
- data/lib/mergify/minitest.rb +14 -0
- data/lib/minitest/mergify.rb +15 -0
- data/lib/minitest/mergify_plugin.rb +13 -0
- metadata +94 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Mergify
|
|
6
|
+
module Minitest
|
|
7
|
+
# The run's tests, as the session asks Test Selection about them, and the
|
|
8
|
+
# order they run in: on a merge-queue bisection step, Fail First puts
|
|
9
|
+
# first the tests that failed in the step above.
|
|
10
|
+
#
|
|
11
|
+
# Minitest has no collection of its own: every test file is loaded before
|
|
12
|
+
# it runs, so the run's tests are each class's test methods, once
|
|
13
|
+
# Minitest's filters (`-n`, `-e`, Rails' `file:line`) left them, which is
|
|
14
|
+
# what Minitest itself runs. Each class then runs what is narrowed here.
|
|
15
|
+
class Selection
|
|
16
|
+
# A test the run collected: its class, its method, and the name Mergify
|
|
17
|
+
# knows it by.
|
|
18
|
+
Test = Struct.new(:klass, :method_name, :name)
|
|
19
|
+
|
|
20
|
+
# Every test the run collected, as Minitest's filters left them.
|
|
21
|
+
attr_reader :tests
|
|
22
|
+
|
|
23
|
+
def initialize(tests)
|
|
24
|
+
@tests = tests
|
|
25
|
+
@first = Set.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# How many tests the run is to run: every one it collected.
|
|
29
|
+
def to_run
|
|
30
|
+
@tests.size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The tests `runnables` hold once Minitest's filters, in `options`, left
|
|
34
|
+
# them, each named by `names`.
|
|
35
|
+
def self.collect(runnables, names, options)
|
|
36
|
+
tests = runnables.flat_map do |klass|
|
|
37
|
+
filtered(klass, options).filter_map do |method|
|
|
38
|
+
file = klass.instance_method(method).source_location&.first
|
|
39
|
+
Test.new(klass, method, names.of(klass.name, method, file).name) if file
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
new(tests)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Minitest 6 names its filters `include` and `exclude`, and filters a
|
|
46
|
+
# class's tests in `filter_runnable_methods`; Minitest 5 does the same
|
|
47
|
+
# inline, `include` under the name `filter`.
|
|
48
|
+
def self.filtered(klass, options)
|
|
49
|
+
return klass.filter_runnable_methods(options) if klass.respond_to?(:filter_runnable_methods)
|
|
50
|
+
|
|
51
|
+
include = pattern(options[:filter])
|
|
52
|
+
exclude = pattern(options[:exclude])
|
|
53
|
+
klass.runnable_methods
|
|
54
|
+
.select { |method| !include || matches?(include, klass, method) }
|
|
55
|
+
.reject { |method| exclude && matches?(exclude, klass, method) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether `filter` takes the test, as Minitest asks it: by its method,
|
|
59
|
+
# or by `Class#method`. With `===`, which a Regexp, a String and Rails'
|
|
60
|
+
# `file:line` filter each answer their own way.
|
|
61
|
+
def self.matches?(filter, klass, method)
|
|
62
|
+
filter === method || filter === "#{klass}##{method}" # rubocop:disable Style/CaseEquality
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A filter given as `/.../` is a regular expression, as Minitest reads it.
|
|
66
|
+
def self.pattern(filter)
|
|
67
|
+
filter.is_a?(String) && filter =~ %r{/(.*)/} ? Regexp.new(Regexp.last_match(1)) : filter
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Hand the session what the run collected, and put first what Fail
|
|
71
|
+
# First puts first, when Mergify answered the run is a bisection step.
|
|
72
|
+
def apply(test_run)
|
|
73
|
+
test_run.select(@tests.map(&:name))
|
|
74
|
+
fail_first = test_run.fail_first
|
|
75
|
+
put_first(fail_first) if fail_first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The names of the tests the run is to run: every one it collected.
|
|
79
|
+
def names_to_run
|
|
80
|
+
@tests.to_set(&:name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The tests of `klass`, of `methods`, that run now.
|
|
84
|
+
def narrow(klass, methods)
|
|
85
|
+
case @pass
|
|
86
|
+
when :first then methods.select { |method| @first.include?([klass, method]) }
|
|
87
|
+
when :rest then methods.reject { |method| @first.include?([klass, method]) }
|
|
88
|
+
else methods
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Run the suites Minitest runs in `run_suites`: the tests put first,
|
|
93
|
+
# then, unless the recorder says to stop once all of them recorded, the
|
|
94
|
+
# rest.
|
|
95
|
+
def run(recorder, &run_suites)
|
|
96
|
+
return run_suites.call if @first.empty?
|
|
97
|
+
|
|
98
|
+
@pass = :first
|
|
99
|
+
run_suites.call
|
|
100
|
+
# They may run in Minitest's threads or Rails' workers: their results
|
|
101
|
+
# come back after the suites were handed over.
|
|
102
|
+
recorder.wait_for(@first_names)
|
|
103
|
+
return if recorder.stopping?
|
|
104
|
+
|
|
105
|
+
@pass = :rest
|
|
106
|
+
run_suites.call
|
|
107
|
+
ensure
|
|
108
|
+
@pass = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def put_first(fail_first)
|
|
114
|
+
names = fail_first.put_first(@tests.map { |test| [test.name, test.name] }).to_set
|
|
115
|
+
first = @tests.select { |test| names.include?(test.name) }
|
|
116
|
+
@first = first.to_set { |test| [test.klass, test.method_name] }
|
|
117
|
+
@first_names = names
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest'
|
|
4
|
+
require 'mergify/ci/core'
|
|
5
|
+
require_relative 'version'
|
|
6
|
+
|
|
7
|
+
module Mergify
|
|
8
|
+
module Minitest
|
|
9
|
+
# The run's session: what this process reads once (the CI context, the
|
|
10
|
+
# trace a caller handed down, the run's id), and what the run reports.
|
|
11
|
+
class TestRun
|
|
12
|
+
# What Minitest is, as the session takes it: what it calls a test; what
|
|
13
|
+
# it can do about Fail First; and Minitest itself, which the trace
|
|
14
|
+
# names.
|
|
15
|
+
FRAMEWORK = {
|
|
16
|
+
test_noun: 'test',
|
|
17
|
+
# Minitest runs tests, the ones put first in a pass of their own that
|
|
18
|
+
# the main process waits for, wherever its workers run them: it sees
|
|
19
|
+
# every result and stops the run.
|
|
20
|
+
fail_first: { unit: 'test', waits_for_all: true, workers_stop_the_run: false },
|
|
21
|
+
# Flaky detection reruns a test after the suite, as many times as
|
|
22
|
+
# every other one it reruns, and nothing retries a failure.
|
|
23
|
+
reruns: { plans_repeats: true, retries_failures: false },
|
|
24
|
+
framework: { name: 'minitest', version: ::Minitest::VERSION, language: 'ruby' }
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
CI_ENABLE_ENV = 'MERGIFY_CI_ENABLE'
|
|
28
|
+
CI_DEBUG_ENV = 'MERGIFY_CI_DEBUG'
|
|
29
|
+
|
|
30
|
+
attr_reader :session
|
|
31
|
+
|
|
32
|
+
# `doubles` has the session play Mergify in memory, answering and
|
|
33
|
+
# keeping what it would send (the session's test doubles, by port, as a
|
|
34
|
+
# Hash: `trace`, `record`). It is for the gem's own specs, which install
|
|
35
|
+
# their TestRun before the run starts. The one the plugin builds takes
|
|
36
|
+
# none, and the gem reads no switch for them.
|
|
37
|
+
def initialize(doubles: nil)
|
|
38
|
+
@session = Mergify::CI::Core::Native::Session.start(
|
|
39
|
+
VERSION, output, doubles, enabled?, client: 'minitest-mergify', **FRAMEWORK
|
|
40
|
+
)
|
|
41
|
+
# What a run that reports loads before its tests: the quarantine list
|
|
42
|
+
# for the branch they target. Without one (no branch, a failed fetch,
|
|
43
|
+
# no token) nothing is quarantined, and its block says why.
|
|
44
|
+
@quarantine = @session.load_before_tests if reports?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Whether the run reports to Mergify at all: in CI, or turned on with
|
|
48
|
+
# MERGIFY_CI_ENABLE. The session decides.
|
|
49
|
+
def reports?
|
|
50
|
+
@session.reports?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Hand the session what this run is about to execute, by name, once
|
|
54
|
+
# Minitest applied its filters: in a job that opted in, it asks Mergify
|
|
55
|
+
# whether the run is a merge-queue bisection step, and the collection's
|
|
56
|
+
# fingerprint travels on the verdict and the trace.
|
|
57
|
+
def select(names)
|
|
58
|
+
@session.select(names)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The run's Fail First, which the session hands out when Mergify
|
|
63
|
+
# answered `failure_first` and the run runs it; nil otherwise.
|
|
64
|
+
def fail_first
|
|
65
|
+
@session.fail_first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Record a test's final status (`passed`, `failed`, `skipped` or
|
|
69
|
+
# `quarantined_failed`) and how long it took, for the session's verdict.
|
|
70
|
+
def record_test(name, status, run_time)
|
|
71
|
+
@session.record_test(name, status, run_time.to_f)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# How many distinct tests reached a final status.
|
|
75
|
+
def executed_count
|
|
76
|
+
@session.executed_count
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Write what this session concluded to Mergify, before the trace goes:
|
|
80
|
+
# the session decides whether there is a verdict to send -- only a run
|
|
81
|
+
# that asked for a selection has one -- builds it from what it recorded
|
|
82
|
+
# and from how the run ended (`not_run` tests it was to run never ran;
|
|
83
|
+
# `told_to_stop`, Fail First stopped it), and sends it. A verdict that
|
|
84
|
+
# did not land is a line of the summary.
|
|
85
|
+
def report_verdict(not_run:, told_to_stop:)
|
|
86
|
+
@session.end_run(not_run, told_to_stop)
|
|
87
|
+
@session.report_verdict(false)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether flaky detection runs in this run: Mergify answered, and the
|
|
91
|
+
# repository opted in.
|
|
92
|
+
def flaky_detection_runs?
|
|
93
|
+
@session.flaky_detection_runs?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether flaky detection found a new test flaky, which fails the run.
|
|
97
|
+
def flaky_detection_fails_the_run?
|
|
98
|
+
@session.flaky_detection_fails_the_run?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Prepare flaky detection for the tests the run ran, by name, each also
|
|
102
|
+
# known by its former names (`former_names`, a list by name), which
|
|
103
|
+
# Mergify's lists may still hold it under: the run's reruns, or nil
|
|
104
|
+
# when nothing is to rerun. Which tests it reruns, and how many times, are the
|
|
105
|
+
# session's.
|
|
106
|
+
def prepare_reruns(names, former_names)
|
|
107
|
+
@session.state_former_names(former_names)
|
|
108
|
+
@session.prepare_reruns(names)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# An attempt at a test, ended with `outcome` (`'passed'` or `'failed'`)
|
|
112
|
+
# after `duration_ms`: its first one, then each rerun.
|
|
113
|
+
def record_attempt(name, outcome, duration_ms)
|
|
114
|
+
@session.record_attempt(name, outcome, duration_ms)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Account for a test that ended with `outcome` (`'passed'`, `'failed'`
|
|
118
|
+
# or `'skipped'`), under each of its `names`, as `{ 'quarantined' =>
|
|
119
|
+
# bool, 'absorbed' => bool }`: an absorbed failure fails nothing.
|
|
120
|
+
def record_quarantine(names, outcome)
|
|
121
|
+
return { 'quarantined' => false, 'absorbed' => false } unless @quarantine
|
|
122
|
+
|
|
123
|
+
@quarantine.record(names, outcome)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Hand the session a test once it finished (see
|
|
127
|
+
# `Mergify::CI::Core::Native::Session#record_case`): its span joins the
|
|
128
|
+
# run's trace.
|
|
129
|
+
def record_case(test_case)
|
|
130
|
+
@session.record_case(test_case)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Have the session send the run's trace, once, at the end, under a
|
|
134
|
+
# session span that fails with the run. An upload that did not land is
|
|
135
|
+
# a line of the summary, never a failure of the suite.
|
|
136
|
+
def report_trace(failed:)
|
|
137
|
+
@session.report_trace(failed) if reports?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# What the session says once the run is over, each line a `[severity,
|
|
141
|
+
# text]` pair. A process parallel_tests started ran a share of the
|
|
142
|
+
# suite only: it sets TEST_ENV_NUMBER, to an empty string for its first.
|
|
143
|
+
def summary
|
|
144
|
+
@session.summary(ENV.key?('TEST_ENV_NUMBER'))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Where the session sends the trace: shown in debug mode, sent
|
|
150
|
+
# otherwise -- to Mergify, or to the test doubles a spec handed over.
|
|
151
|
+
def output
|
|
152
|
+
Mergify::CI::Core::Utils.env_truthy?(CI_DEBUG_ENV) ? 'debug' : 'send'
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The gem's enable switch: the run then reports outside CI too.
|
|
156
|
+
def enabled?
|
|
157
|
+
Mergify::CI::Core::Utils.env_truthy?(CI_ENABLE_ENV)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'minitest/version'
|
|
4
|
+
|
|
5
|
+
module Mergify
|
|
6
|
+
module Minitest
|
|
7
|
+
class << self
|
|
8
|
+
# The run's TestRun: the plugin builds it, unless one is installed
|
|
9
|
+
# before the run starts, as the gem's own specs install theirs, with
|
|
10
|
+
# the session's test doubles.
|
|
11
|
+
attr_accessor :test_run
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest'
|
|
4
|
+
require 'minitest/spec'
|
|
5
|
+
require_relative 'mergify_plugin'
|
|
6
|
+
require_relative '../mergify/minitest/declarations'
|
|
7
|
+
|
|
8
|
+
# What a suite requires to report to Mergify, before its tests are defined.
|
|
9
|
+
# Minitest 6 loads only the plugins it is asked for, so this registers ours;
|
|
10
|
+
# Minitest 5 registers every installed plugin by itself, ours again, and the
|
|
11
|
+
# plugin sets itself up once. Either way, only a suite that requires it gives
|
|
12
|
+
# a test with no body the place it is declared (see Declarations); requiring
|
|
13
|
+
# the spec DSL for that does what `minitest/autorun` does anyway.
|
|
14
|
+
Minitest.extensions << 'mergify' unless Minitest.extensions.include?('mergify')
|
|
15
|
+
Mergify::Minitest::Declarations.install
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mergify/minitest/plugin'
|
|
4
|
+
|
|
5
|
+
# Minitest's plugin entry point. Minitest 5 finds this file in every installed
|
|
6
|
+
# gem and loads it by itself; Minitest 6 loads it when the suite requires
|
|
7
|
+
# `minitest/mergify`.
|
|
8
|
+
module Minitest
|
|
9
|
+
# Called once Minitest has parsed its options, before any test runs.
|
|
10
|
+
def self.plugin_mergify_init(options)
|
|
11
|
+
Mergify::Minitest::Plugin.init(options)
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: minitest-mergify
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2026.10.8
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mergify
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.19'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '7'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '5.19'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '7'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: mergify-ci-core
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - '='
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2026.10.8
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - '='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 2026.10.8
|
|
46
|
+
description: 'Minitest integration for Mergify Test Engine: the run reported to CI
|
|
47
|
+
Insights, test quarantine, Fail First on merge-queue bisection steps, and flaky
|
|
48
|
+
test detection.'
|
|
49
|
+
email:
|
|
50
|
+
- support@mergify.com
|
|
51
|
+
executables: []
|
|
52
|
+
extensions: []
|
|
53
|
+
extra_rdoc_files: []
|
|
54
|
+
files:
|
|
55
|
+
- LICENSE
|
|
56
|
+
- README.md
|
|
57
|
+
- THIRD_PARTY_LICENSES
|
|
58
|
+
- lib/mergify/minitest.rb
|
|
59
|
+
- lib/mergify/minitest/declarations.rb
|
|
60
|
+
- lib/mergify/minitest/names.rb
|
|
61
|
+
- lib/mergify/minitest/plugin.rb
|
|
62
|
+
- lib/mergify/minitest/recorder.rb
|
|
63
|
+
- lib/mergify/minitest/reruns.rb
|
|
64
|
+
- lib/mergify/minitest/results.rb
|
|
65
|
+
- lib/mergify/minitest/selection.rb
|
|
66
|
+
- lib/mergify/minitest/test_run.rb
|
|
67
|
+
- lib/mergify/minitest/version.rb
|
|
68
|
+
- lib/minitest/mergify.rb
|
|
69
|
+
- lib/minitest/mergify_plugin.rb
|
|
70
|
+
homepage: https://docs.mergify.com/test-engine/test-frameworks/minitest/
|
|
71
|
+
licenses:
|
|
72
|
+
- LicenseRef-Mergify-Proprietary
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://docs.mergify.com/test-engine/test-frameworks/minitest/
|
|
75
|
+
bug_tracker_uri: https://github.com/Mergifyio/mergify-cli/issues
|
|
76
|
+
rubygems_mfa_required: 'true'
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '3.1'
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 4.0.20
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Minitest plugin for Mergify Test Engine
|
|
94
|
+
test_files: []
|