mutineer 1.3.0 → 1.5.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/CHANGELOG.md +317 -0
- data/README.md +200 -28
- data/lib/mutineer/changed_lines.rb +57 -2
- data/lib/mutineer/cli.rb +142 -92
- data/lib/mutineer/config.rb +215 -56
- data/lib/mutineer/coverage_map.rb +161 -14
- data/lib/mutineer/daemon_backend.rb +3 -0
- data/lib/mutineer/daemon_client.rb +170 -13
- data/lib/mutineer/daemon_server.rb +2 -1
- data/lib/mutineer/external_backend.rb +2 -2
- data/lib/mutineer/isolation.rb +91 -6
- data/lib/mutineer/kill_channel.rb +284 -0
- data/lib/mutineer/kill_matrix.rb +88 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
- data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
- data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +14 -68
- data/lib/mutineer/minitest_integration.rb +27 -10
- data/lib/mutineer/mutant_id.rb +5 -4
- data/lib/mutineer/mutator_registry.rb +12 -3
- data/lib/mutineer/mutators/array_literal.rb +0 -7
- data/lib/mutineer/mutators/base.rb +63 -4
- data/lib/mutineer/mutators/chain_link.rb +0 -7
- data/lib/mutineer/mutators/collection_method.rb +1 -1
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/operand_removal.rb +0 -7
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/return_nil.rb +5 -11
- data/lib/mutineer/mutators/string_literal.rb +1 -1
- data/lib/mutineer/pairing.rb +222 -15
- data/lib/mutineer/parser.rb +12 -3
- data/lib/mutineer/project.rb +7 -0
- data/lib/mutineer/rails_worker_db.rb +1 -1
- data/lib/mutineer/reporter.rb +288 -52
- data/lib/mutineer/result.rb +37 -1
- data/lib/mutineer/runner.rb +133 -16
- data/lib/mutineer/statement_lines.rb +143 -0
- data/lib/mutineer/test_runners/minitest.rb +4 -2
- data/lib/mutineer/test_runners/rspec.rb +243 -3
- data/lib/mutineer/test_runners.rb +5 -3
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +4 -2
- data/lib/mutineer.rb +3 -0
- metadata +10 -3
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
# The line protocol a `--matrix` mutant child uses to tell its parent how its
|
|
7
|
+
# tests went. One JSON array per line:
|
|
8
|
+
#
|
|
9
|
+
# ["start"] the recorder is armed; once, first
|
|
10
|
+
# ["pass", file, name, id] a test passed against the mutant
|
|
11
|
+
# ["kill", file, name, id] a test failed or errored: it killed it
|
|
12
|
+
# ["parallel"] the run reached its parallel tests; once
|
|
13
|
+
# ["skip"] what runs next, a run without --matrix
|
|
14
|
+
# skips (see below)
|
|
15
|
+
# ["unskip"] the matching end of a ["skip"]
|
|
16
|
+
# ["cleanup"] every test ran; suite hooks follow; once
|
|
17
|
+
# ["lost"] a test the recorder could not describe
|
|
18
|
+
# ["end"] the suite returned with every test seen;
|
|
19
|
+
# once, last
|
|
20
|
+
#
|
|
21
|
+
# `id` tells two tests apart when `[file, name]` does not, and it is what
|
|
22
|
+
# identifies the test across mutants (an RSpec example id such as
|
|
23
|
+
# `./spec/calc_spec.rb[1:2]`; the name can change with the mutant). For
|
|
24
|
+
# Minitest it equals the name. A test line is written when the test is
|
|
25
|
+
# recorded, so a child killed at the timeout has already sent every kill it
|
|
26
|
+
# saw. A skipped test sends nothing.
|
|
27
|
+
#
|
|
28
|
+
# Minitest runs its serial test classes first and its `parallelize_me!`
|
|
29
|
+
# classes after them, and the stop at the first failure of a run without
|
|
30
|
+
# `--matrix` skips everything after a serial kill. `parallel` marks where the
|
|
31
|
+
# parallel tests begin, so the parent can tell a kill that the stop would have
|
|
32
|
+
# followed from one it could not. `cleanup` marks where the tests end and the
|
|
33
|
+
# suite's own hooks begin (RSpec `after(:suite)`). It decides no verdict
|
|
34
|
+
# (the `skip` regions do); it only marks a later test line as out of order.
|
|
35
|
+
#
|
|
36
|
+
# After the first serial kill, a run without `--matrix` skips every later
|
|
37
|
+
# test and every later test class or example group, but it still runs the
|
|
38
|
+
# rest of the code around the failing test: the remainder of its Minitest
|
|
39
|
+
# class `run` wrapper, the `after(:all)` hooks of its RSpec groups, and the
|
|
40
|
+
# suite hooks. The recorder writes `skip` as it enters something that run
|
|
41
|
+
# would skip and `unskip` as that returns, so a child that exits, crashes or
|
|
42
|
+
# hangs inside a skipped region is one the plain run would have stopped
|
|
43
|
+
# before (see {Isolation.finish}). The pairs nest.
|
|
44
|
+
#
|
|
45
|
+
# A row is complete only with `start` and `end`, in order, and no lost line: a
|
|
46
|
+
# test that exits the process, a crash, an interrupted run or a recorder that
|
|
47
|
+
# never armed leaves one out. A stream out of order (a duplicate marker, a
|
|
48
|
+
# test line before `start` or after `end`) is invalid, and the parent trusts
|
|
49
|
+
# nothing from it.
|
|
50
|
+
#
|
|
51
|
+
# Writing never raises: a recorder that broke the test run would change the
|
|
52
|
+
# verdict, and the exit status decides the verdict. A lost line leaves the row
|
|
53
|
+
# incomplete. After one write to a channel fails, nothing more is written to
|
|
54
|
+
# it, so a later `end` can never make a row with a missing line look complete.
|
|
55
|
+
#
|
|
56
|
+
# Stdlib-only, so the app-side daemon can load the files that require it.
|
|
57
|
+
module KillChannel
|
|
58
|
+
# Event for a test that passed against the mutant.
|
|
59
|
+
PASS = "pass"
|
|
60
|
+
|
|
61
|
+
# Event for a test that failed or errored against the mutant: it killed it.
|
|
62
|
+
KILL = "kill"
|
|
63
|
+
|
|
64
|
+
# Event that opens a recorded run.
|
|
65
|
+
START = "start"
|
|
66
|
+
|
|
67
|
+
# Event that marks the first parallel test of a run.
|
|
68
|
+
PARALLEL = "parallel"
|
|
69
|
+
|
|
70
|
+
# Event that marks the end of the tests and the start of the suite's cleanup.
|
|
71
|
+
CLEANUP = "cleanup"
|
|
72
|
+
|
|
73
|
+
# Event that opens a region a run without `--matrix` would skip.
|
|
74
|
+
SKIP = "skip"
|
|
75
|
+
|
|
76
|
+
# Event that closes the latest open {SKIP} region.
|
|
77
|
+
UNSKIP = "unskip"
|
|
78
|
+
|
|
79
|
+
# Event for a test the recorder could not describe.
|
|
80
|
+
LOST = "lost"
|
|
81
|
+
|
|
82
|
+
# Event that closes a run whose suite returned with every test seen.
|
|
83
|
+
FINISH = "end"
|
|
84
|
+
|
|
85
|
+
# Serializes writes from tests that record on several threads.
|
|
86
|
+
LOCK = Mutex.new
|
|
87
|
+
|
|
88
|
+
# What one child sent. `killed` and `ran` are sorted, unique
|
|
89
|
+
# `[file, name, id]` tests (`ran` includes the killers); `lost` counts lines
|
|
90
|
+
# that could not be read, including a partial last line. `parallel` and
|
|
91
|
+
# `cleanup` say the marker arrived, and `serial_kill` that a test killed
|
|
92
|
+
# before the `parallel` marker. `skipping` counts the {SKIP} regions still
|
|
93
|
+
# open when the stream ended. `invalid` is set by any line out of order.
|
|
94
|
+
Report = Struct.new(:killed, :ran, :started, :finished, :parallel, :cleanup, :serial_kill, :skipping, :invalid,
|
|
95
|
+
:lost, keyword_init: true)
|
|
96
|
+
|
|
97
|
+
# Writes one test line.
|
|
98
|
+
#
|
|
99
|
+
# @param io [IO] the write end of the channel.
|
|
100
|
+
# @param event [String] {PASS} or {KILL}.
|
|
101
|
+
# @param file [String] the file that defines the test.
|
|
102
|
+
# @param name [String] the test's display name, e.g. `CalculatorTest#test_add`.
|
|
103
|
+
# @param id [String] what tells the test apart from others with its file and
|
|
104
|
+
# name; defaults to the name.
|
|
105
|
+
# @return [void]
|
|
106
|
+
def self.write(io, event, file, name, id = name)
|
|
107
|
+
emit(io, [event, utf8(file), utf8(name), utf8(id)])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Writes the `start` line.
|
|
111
|
+
#
|
|
112
|
+
# @param io [IO] the write end of the channel.
|
|
113
|
+
# @return [void]
|
|
114
|
+
def self.write_start(io)
|
|
115
|
+
emit(io, [START])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Writes the `parallel` line.
|
|
119
|
+
#
|
|
120
|
+
# @param io [IO] the write end of the channel.
|
|
121
|
+
# @return [void]
|
|
122
|
+
def self.write_parallel(io)
|
|
123
|
+
emit(io, [PARALLEL])
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Writes the `skip` line.
|
|
127
|
+
#
|
|
128
|
+
# @param io [IO] the write end of the channel.
|
|
129
|
+
# @return [void]
|
|
130
|
+
def self.write_skip(io)
|
|
131
|
+
emit(io, [SKIP])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Writes the `unskip` line.
|
|
135
|
+
#
|
|
136
|
+
# @param io [IO] the write end of the channel.
|
|
137
|
+
# @return [void]
|
|
138
|
+
def self.write_unskip(io)
|
|
139
|
+
emit(io, [UNSKIP])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Writes the `cleanup` line.
|
|
143
|
+
#
|
|
144
|
+
# @param io [IO] the write end of the channel.
|
|
145
|
+
# @return [void]
|
|
146
|
+
def self.write_cleanup(io)
|
|
147
|
+
emit(io, [CLEANUP])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Writes the `lost` line, for a test the recorder could not describe.
|
|
151
|
+
#
|
|
152
|
+
# @param io [IO] the write end of the channel.
|
|
153
|
+
# @return [void]
|
|
154
|
+
def self.write_lost(io)
|
|
155
|
+
emit(io, [LOST])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Writes the `end` line.
|
|
159
|
+
#
|
|
160
|
+
# @param io [IO] the write end of the channel.
|
|
161
|
+
# @return [void]
|
|
162
|
+
def self.write_end(io)
|
|
163
|
+
emit(io, [FINISH])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Reads every complete line of `buffer`.
|
|
167
|
+
#
|
|
168
|
+
# @param buffer [String] bytes read from the channel.
|
|
169
|
+
# @return [Report]
|
|
170
|
+
def self.parse(buffer)
|
|
171
|
+
report = Report.new(killed: [], ran: [], started: false, finished: false, parallel: false, cleanup: false,
|
|
172
|
+
serial_kill: false, skipping: 0, invalid: false, lost: 0)
|
|
173
|
+
buffer.dup.force_encoding(Encoding::UTF_8).each_line do |line|
|
|
174
|
+
fields = line.end_with?("\n") ? parse_line(line) : nil
|
|
175
|
+
fields ? apply(report, fields) : report.lost += 1
|
|
176
|
+
end
|
|
177
|
+
report.killed = report.killed.uniq.sort
|
|
178
|
+
report.ran = report.ran.uniq.sort
|
|
179
|
+
report
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Adds one parsed line to `report`. A line out of order marks the report
|
|
183
|
+
# invalid; the line still counts, since what it names happened.
|
|
184
|
+
#
|
|
185
|
+
# @api private
|
|
186
|
+
# @param report [Report] the report being built.
|
|
187
|
+
# @param fields [Array<String>] a valid line's fields.
|
|
188
|
+
# @return [void]
|
|
189
|
+
def self.apply(report, fields)
|
|
190
|
+
case fields[0]
|
|
191
|
+
when START
|
|
192
|
+
report.invalid = true if report.started || report.ran.any? || report.parallel || report.cleanup || report.finished
|
|
193
|
+
report.started = true
|
|
194
|
+
when PARALLEL
|
|
195
|
+
report.invalid = true unless open?(report) && !report.parallel && !report.cleanup
|
|
196
|
+
report.parallel = true
|
|
197
|
+
when SKIP
|
|
198
|
+
report.invalid = true unless open?(report) && !report.cleanup
|
|
199
|
+
report.skipping += 1
|
|
200
|
+
when UNSKIP
|
|
201
|
+
report.invalid = true unless open?(report) && !report.cleanup && report.skipping.positive?
|
|
202
|
+
report.skipping -= 1 if report.skipping.positive?
|
|
203
|
+
when CLEANUP
|
|
204
|
+
report.invalid = true unless open?(report) && !report.cleanup
|
|
205
|
+
report.cleanup = true
|
|
206
|
+
when LOST then report.lost += 1
|
|
207
|
+
when FINISH
|
|
208
|
+
report.invalid = true unless open?(report) && !report.finished
|
|
209
|
+
report.finished = true
|
|
210
|
+
else
|
|
211
|
+
report.invalid = true unless open?(report) && !report.cleanup
|
|
212
|
+
test = fields[1, 3]
|
|
213
|
+
report.ran << test
|
|
214
|
+
return unless fields[0] == KILL
|
|
215
|
+
|
|
216
|
+
report.killed << test
|
|
217
|
+
report.serial_kill = true unless report.parallel
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# True between `start` and `end`.
|
|
222
|
+
#
|
|
223
|
+
# @api private
|
|
224
|
+
# @param report [Report] the report being built.
|
|
225
|
+
# @return [Boolean]
|
|
226
|
+
def self.open?(report)
|
|
227
|
+
report.started && !report.finished
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Parses one line into its fields, or nil when it is not a valid line.
|
|
231
|
+
#
|
|
232
|
+
# @api private
|
|
233
|
+
# @param line [String] one newline-terminated line.
|
|
234
|
+
# @return [Array<String>, nil]
|
|
235
|
+
def self.parse_line(line)
|
|
236
|
+
fields = JSON.parse(line)
|
|
237
|
+
return unless fields.is_a?(Array) && fields.all?(String)
|
|
238
|
+
|
|
239
|
+
valid =
|
|
240
|
+
case fields[0]
|
|
241
|
+
when PASS, KILL then fields.size == 4
|
|
242
|
+
when START, PARALLEL, SKIP, UNSKIP, CLEANUP, LOST, FINISH then fields.size == 1
|
|
243
|
+
end
|
|
244
|
+
fields if valid
|
|
245
|
+
rescue JSON::ParserError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Writes one line. Never raises. After a write to `io` fails, writes
|
|
250
|
+
# nothing more to it.
|
|
251
|
+
#
|
|
252
|
+
# @api private
|
|
253
|
+
# @param io [IO] the write end of the channel.
|
|
254
|
+
# @param fields [Array<String>] the line's fields.
|
|
255
|
+
# @return [void]
|
|
256
|
+
def self.emit(io, fields)
|
|
257
|
+
line = "#{JSON.generate(fields)}\n"
|
|
258
|
+
LOCK.synchronize do
|
|
259
|
+
next if @broken.equal?(io)
|
|
260
|
+
|
|
261
|
+
begin
|
|
262
|
+
io.write(line)
|
|
263
|
+
rescue StandardError
|
|
264
|
+
@broken = io
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
nil
|
|
268
|
+
rescue StandardError
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# A UTF-8 copy of `value`, with invalid bytes replaced, so JSON can encode it.
|
|
273
|
+
#
|
|
274
|
+
# @api private
|
|
275
|
+
# @param value [Object] a file path or test name.
|
|
276
|
+
# @return [String]
|
|
277
|
+
def self.utf8(value)
|
|
278
|
+
str = value.to_s
|
|
279
|
+
str = str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace) unless str.encoding == Encoding::UTF_8
|
|
280
|
+
str.scrub
|
|
281
|
+
end
|
|
282
|
+
private_class_method :apply, :open?, :parse_line, :emit, :utf8
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
# Which tests kill which mutants, read from the {Kills} rows of a `--matrix`
|
|
7
|
+
# run. Pure: it reads results and runs nothing, and it never touches the score.
|
|
8
|
+
#
|
|
9
|
+
# A test is a `[file, name, id]` triple (see {Kills}); file and id identify it,
|
|
10
|
+
# and every row names a test the same way (see {Runner.share_tests}). The tests it knows are
|
|
11
|
+
# the ones that ran against at least one mutant of this run, so every answer
|
|
12
|
+
# is relative to the run's mutants: a test of code outside the run's sources
|
|
13
|
+
# kills nothing here.
|
|
14
|
+
#
|
|
15
|
+
# - A blind test ran in at least one complete row and killed no mutant. A
|
|
16
|
+
# test seen only in incomplete rows is never called blind.
|
|
17
|
+
# - A redundant test killed at least one mutant, and each mutant it killed has
|
|
18
|
+
# another killer. Each test is judged alone: two redundant tests can be the
|
|
19
|
+
# only killers of one mutant, so delete them one at a time. Finding the
|
|
20
|
+
# smallest set of tests that keeps every kill is a set-cover problem, out of
|
|
21
|
+
# scope here.
|
|
22
|
+
#
|
|
23
|
+
# Kills in incomplete rows count, since they did happen. An incomplete row can
|
|
24
|
+
# miss kills, so when {#complete?} is false a blind test may have killed one
|
|
25
|
+
# of those mutants.
|
|
26
|
+
class KillMatrix
|
|
27
|
+
# The results that carry a {Kills} row, in run order.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<Mutineer::Result>]
|
|
30
|
+
attr_reader :rows
|
|
31
|
+
|
|
32
|
+
# @param results [Array<Mutineer::Result>] every result of the run.
|
|
33
|
+
def initialize(results)
|
|
34
|
+
@rows = results.select(&:kills)
|
|
35
|
+
@kill_counts = Hash.new(0)
|
|
36
|
+
@rows.each { |r| r.kills.killed_by.each { |test| @kill_counts[test] += 1 } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Every test that ran against at least one mutant, sorted by file, name and id.
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Array(String, String, String)>]
|
|
42
|
+
def tests
|
|
43
|
+
@tests ||= @rows.flat_map { |r| r.kills.ran + r.kills.killed_by }.uniq.sort
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# True when every row is complete.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def complete?
|
|
50
|
+
@rows.all? { |r| r.kills.complete }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The rows that are not complete.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<Mutineer::Result>]
|
|
56
|
+
def incomplete_rows
|
|
57
|
+
@rows.reject { |r| r.kills.complete }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# How many mutants `test` killed.
|
|
61
|
+
#
|
|
62
|
+
# @param test [Array(String, String, String)] a `[file, name, id]` test.
|
|
63
|
+
# @return [Integer]
|
|
64
|
+
def kill_count(test)
|
|
65
|
+
@kill_counts[test]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Tests that ran in a complete row and killed no mutant.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<Array(String, String, String)>] sorted.
|
|
71
|
+
def blind
|
|
72
|
+
@blind ||= begin
|
|
73
|
+
seen = @rows.select { |r| r.kills.complete }.flat_map { |r| r.kills.ran }.to_set
|
|
74
|
+
tests.select { |test| kill_count(test).zero? && seen.include?(test) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Tests that killed a mutant, where every mutant they killed has another killer.
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<Array(String, String, String)>] sorted.
|
|
81
|
+
def redundant
|
|
82
|
+
@redundant ||= begin
|
|
83
|
+
sole = @rows.filter_map { |r| r.kills.killed_by.first if r.kills.killed_by.size == 1 }.to_set
|
|
84
|
+
tests.select { |test| kill_count(test).positive? && !sole.include?(test) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "outer_reporter"
|
|
4
|
+
require_relative "../kill_channel"
|
|
5
|
+
|
|
6
|
+
module Mutineer
|
|
7
|
+
class MinitestIntegration
|
|
8
|
+
# Sends the outcome of each test to the parent of a `--matrix` mutant run,
|
|
9
|
+
# through {KillChannel}. It never stops the run: every covering test runs,
|
|
10
|
+
# so the parent learns exactly which tests kill the mutant. Child-process
|
|
11
|
+
# only, like MinitestIntegration.
|
|
12
|
+
#
|
|
13
|
+
# Only the outer reporter counts (see {OuterReporter}): a test can build and
|
|
14
|
+
# record on its own reporter, and those results are not the run's.
|
|
15
|
+
module KillRecorder
|
|
16
|
+
extend OuterReporter::Client
|
|
17
|
+
|
|
18
|
+
# Serializes the counting and the writing of results, which Minitest
|
|
19
|
+
# records from several threads in a parallel class.
|
|
20
|
+
LOCK = Mutex.new
|
|
21
|
+
|
|
22
|
+
# Prepended on each loaded test class: one test's run. Serial and parallel
|
|
23
|
+
# runs both call it, in Minitest 5 and 6.
|
|
24
|
+
module MarkSkippedTest
|
|
25
|
+
# Runs the test inside a `skip` region once a serial test has killed.
|
|
26
|
+
#
|
|
27
|
+
# @return [Minitest::Result]
|
|
28
|
+
def run
|
|
29
|
+
KillRecorder.skipping { super }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Prepended on each loaded test class's singleton under Minitest 6: one
|
|
34
|
+
# class's run and each test's class-level run, user wrappers included.
|
|
35
|
+
# A run without `--matrix` skips both after a stop (see
|
|
36
|
+
# StopAtFirstFailure::SkipAfterStop6).
|
|
37
|
+
module MarkSkippedClass6
|
|
38
|
+
# Runs the class inside a `skip` region once a serial test has killed.
|
|
39
|
+
#
|
|
40
|
+
# @param reporter [Minitest::CompositeReporter]
|
|
41
|
+
# @param options [Hash]
|
|
42
|
+
# @return [Object]
|
|
43
|
+
def run_suite(reporter, options = {})
|
|
44
|
+
KillRecorder.class_starting(self, reporter)
|
|
45
|
+
KillRecorder.skipping { super }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Runs one test's class-level step inside a `skip` region once a
|
|
49
|
+
# serial test has killed.
|
|
50
|
+
#
|
|
51
|
+
# @param klass [Class]
|
|
52
|
+
# @param method_name [String]
|
|
53
|
+
# @param reporter [Minitest::CompositeReporter]
|
|
54
|
+
# @return [Object]
|
|
55
|
+
def run(klass, method_name, reporter)
|
|
56
|
+
KillRecorder.skipping { super }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The Minitest 5 form of {MarkSkippedClass6}.
|
|
61
|
+
module MarkSkippedClass5
|
|
62
|
+
# Runs the class inside a `skip` region once a serial test has killed.
|
|
63
|
+
#
|
|
64
|
+
# @param reporter [Minitest::CompositeReporter]
|
|
65
|
+
# @param options [Hash]
|
|
66
|
+
# @return [Object]
|
|
67
|
+
def run(reporter, options = {})
|
|
68
|
+
KillRecorder.class_starting(self, reporter)
|
|
69
|
+
KillRecorder.skipping { super }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Runs one test's class-level step inside a `skip` region once a
|
|
73
|
+
# serial test has killed.
|
|
74
|
+
#
|
|
75
|
+
# @param klass [Class]
|
|
76
|
+
# @param method_name [String]
|
|
77
|
+
# @param reporter [Minitest::CompositeReporter]
|
|
78
|
+
# @return [Object]
|
|
79
|
+
def run_one_method(klass, method_name, reporter)
|
|
80
|
+
KillRecorder.skipping { super }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Prepended on `Minitest::CompositeReporter`.
|
|
85
|
+
module Record
|
|
86
|
+
# Records the result, then sends it to the channel.
|
|
87
|
+
#
|
|
88
|
+
# @param result [Minitest::Result] the result of one test.
|
|
89
|
+
# @return [void]
|
|
90
|
+
def record(result)
|
|
91
|
+
super
|
|
92
|
+
KillRecorder.record(self, result)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class << self
|
|
97
|
+
# The write end of the channel of the armed run.
|
|
98
|
+
#
|
|
99
|
+
# @return [IO, nil]
|
|
100
|
+
attr_accessor :channel
|
|
101
|
+
|
|
102
|
+
# True once the `parallel` line has gone out.
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean, nil]
|
|
105
|
+
attr_accessor :parallel_marked
|
|
106
|
+
|
|
107
|
+
# The loaded test classes of the armed run.
|
|
108
|
+
#
|
|
109
|
+
# @return [Array<Class>, nil]
|
|
110
|
+
attr_accessor :runnables
|
|
111
|
+
|
|
112
|
+
# How many results the outer reporter has recorded, skips included.
|
|
113
|
+
#
|
|
114
|
+
# @return [Integer, nil]
|
|
115
|
+
attr_accessor :seen
|
|
116
|
+
|
|
117
|
+
# True once a serial test killed: from then on, a run without `--matrix`
|
|
118
|
+
# skips each later test and class.
|
|
119
|
+
#
|
|
120
|
+
# @return [Boolean, nil]
|
|
121
|
+
attr_accessor :serial_killed
|
|
122
|
+
|
|
123
|
+
# Installs the hooks, arms the recorder for this process, and writes the
|
|
124
|
+
# channel's `start` line. Call it after the test files load, so the
|
|
125
|
+
# recorder knows the test classes. An unknown Minitest shape arms
|
|
126
|
+
# nothing and writes nothing, so the parent sees no `start` and keeps
|
|
127
|
+
# the row incomplete.
|
|
128
|
+
#
|
|
129
|
+
# @param channel [IO] the write end of the channel.
|
|
130
|
+
# @param runnables [Array<Class>] the loaded test classes.
|
|
131
|
+
# @return [Boolean] false when the Minitest shape is unknown.
|
|
132
|
+
def arm!(channel, runnables)
|
|
133
|
+
outer = OuterReporter.hook_for_loaded_minitest
|
|
134
|
+
return false unless outer
|
|
135
|
+
|
|
136
|
+
OuterReporter.prepend_once(::Minitest::CompositeReporter, Record)
|
|
137
|
+
OuterReporter.prepend_once(::Minitest.singleton_class, outer)
|
|
138
|
+
class_hook = outer.equal?(OuterReporter::Hook6) ? MarkSkippedClass6 : MarkSkippedClass5
|
|
139
|
+
runnables.each do |klass|
|
|
140
|
+
OuterReporter.prepend_once(klass, MarkSkippedTest)
|
|
141
|
+
OuterReporter.prepend_once(klass.singleton_class, class_hook)
|
|
142
|
+
end
|
|
143
|
+
OuterReporter.register(self)
|
|
144
|
+
self.armed_pid = Process.pid
|
|
145
|
+
self.armed_reporter = nil
|
|
146
|
+
self.channel = channel
|
|
147
|
+
self.parallel_marked = false
|
|
148
|
+
# A copy: Minitest's list is live, and a class a test defines during
|
|
149
|
+
# the run joins it without running, which would break the count.
|
|
150
|
+
self.runnables = runnables.dup
|
|
151
|
+
self.seen = 0
|
|
152
|
+
self.serial_killed = false
|
|
153
|
+
KillChannel.write_start(channel)
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Writes the channel's `end` line once the suite has returned and the
|
|
158
|
+
# outer reporter has seen every test the loaded classes run. A return
|
|
159
|
+
# alone proves nothing: Minitest catches an Interrupt in a test and
|
|
160
|
+
# returns after the tests so far. Does nothing unless the recorder is
|
|
161
|
+
# armed in this process.
|
|
162
|
+
#
|
|
163
|
+
# @return [void]
|
|
164
|
+
def finish!
|
|
165
|
+
return unless channel && armed_here?
|
|
166
|
+
|
|
167
|
+
KillChannel.write_end(channel) if seen == planned_tests
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# True when the test class runs its tests in parallel threads
|
|
171
|
+
# (`parallelize_me!`). Minitest runs these after every serial class, and
|
|
172
|
+
# the stop at the first failure cannot skip the tests it already queued.
|
|
173
|
+
# Minitest 6 names the order `run_order`; Minitest 5 names it `test_order`.
|
|
174
|
+
#
|
|
175
|
+
# @param klass [Class] a loaded test class.
|
|
176
|
+
# @return [Boolean]
|
|
177
|
+
def parallel?(klass)
|
|
178
|
+
%i[run_order test_order].any? { |order| klass.respond_to?(order) && klass.public_send(order) == :parallel }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Clears the armed state.
|
|
182
|
+
#
|
|
183
|
+
# @return [void]
|
|
184
|
+
def disarm!
|
|
185
|
+
self.armed_pid = nil
|
|
186
|
+
self.armed_reporter = nil
|
|
187
|
+
self.channel = nil
|
|
188
|
+
self.runnables = nil
|
|
189
|
+
self.serial_killed = false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Writes the `parallel` line when the first parallel class starts on the
|
|
193
|
+
# outer reporter. The class object is known here, so an anonymous class
|
|
194
|
+
# (which Minitest records with no name) counts too. Minitest runs every
|
|
195
|
+
# serial class first, so a kill before this line came from a serial
|
|
196
|
+
# test. A test that runs a class on its own reporter is not the run's
|
|
197
|
+
# parallel phase, so it marks nothing.
|
|
198
|
+
#
|
|
199
|
+
# @param klass [Class] the test class starting its run.
|
|
200
|
+
# @param reporter [Minitest::CompositeReporter] the reporter it runs on.
|
|
201
|
+
# @return [void]
|
|
202
|
+
def class_starting(klass, reporter)
|
|
203
|
+
return unless channel && outer_reporter?(reporter) && parallel?(klass)
|
|
204
|
+
|
|
205
|
+
LOCK.synchronize { mark_parallel }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Runs the block between `skip` and `unskip` when a serial test has
|
|
209
|
+
# killed in this armed process, else just runs it. No `ensure`: a block
|
|
210
|
+
# that never returns (an exit, a crash) must leave its region open, so
|
|
211
|
+
# the parent knows the child ended where a run without `--matrix`
|
|
212
|
+
# would not have been.
|
|
213
|
+
#
|
|
214
|
+
# @yieldreturn [Object]
|
|
215
|
+
# @return [Object] the block's value.
|
|
216
|
+
def skipping
|
|
217
|
+
open = channel && serial_killed && armed_here?
|
|
218
|
+
KillChannel.write_skip(channel) if open
|
|
219
|
+
value = yield
|
|
220
|
+
KillChannel.write_unskip(channel) if open
|
|
221
|
+
value
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Sends one result recorded on `reporter`: a pass, or a kill for a
|
|
225
|
+
# failure or an error. Skips send no test line, though they count as
|
|
226
|
+
# seen, and results on other reporters count for nothing. The
|
|
227
|
+
# `parallel` line goes out when the first parallel class starts (see
|
|
228
|
+
# {.class_starting}), so a kill that precedes it came from a serial test.
|
|
229
|
+
#
|
|
230
|
+
# @param reporter [Minitest::CompositeReporter] the recording reporter.
|
|
231
|
+
# @param result [Minitest::Result] the result of one test.
|
|
232
|
+
# @return [void]
|
|
233
|
+
def record(reporter, result)
|
|
234
|
+
return unless channel && outer_reporter?(reporter)
|
|
235
|
+
|
|
236
|
+
LOCK.synchronize do
|
|
237
|
+
self.seen += 1
|
|
238
|
+
send_result(result)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
# Writes one result's line. A recorder that raised would change the
|
|
245
|
+
# verdict, so a result it cannot describe (a Minitest without
|
|
246
|
+
# `source_location` or `klass` on its results) is a `lost` line, which
|
|
247
|
+
# keeps the row incomplete, as on the RSpec side.
|
|
248
|
+
#
|
|
249
|
+
# @param result [Minitest::Result] the result of one test.
|
|
250
|
+
# @return [void]
|
|
251
|
+
def send_result(result)
|
|
252
|
+
return if result.skipped?
|
|
253
|
+
|
|
254
|
+
event = result.passed? ? KillChannel::PASS : KillChannel::KILL
|
|
255
|
+
self.serial_killed = true if event == KillChannel::KILL && !parallel_marked
|
|
256
|
+
file, line = Array(result.source_location)
|
|
257
|
+
KillChannel.write(channel, event, file, *name_and_id(result, line))
|
|
258
|
+
rescue StandardError
|
|
259
|
+
KillChannel.write_lost(channel)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The test's name and id. `Class#method` is unique within a run, so the
|
|
263
|
+
# name is also the id. An anonymous class has no name (Minitest records
|
|
264
|
+
# nil), so its tests would all merge into `#method`; their id adds the
|
|
265
|
+
# line that defines the method, which no mutant changes. The test's file
|
|
266
|
+
# is already part of its identity, so the id holds no path.
|
|
267
|
+
#
|
|
268
|
+
# @param result [Minitest::Result] the result of one test.
|
|
269
|
+
# @param line [Integer, nil] the line that defines the test method.
|
|
270
|
+
# @return [Array(String, String)] name and id.
|
|
271
|
+
def name_and_id(result, line)
|
|
272
|
+
return ["#{result.klass}##{result.name}"] * 2 unless result.klass.to_s.empty?
|
|
273
|
+
|
|
274
|
+
name = "(anonymous)##{result.name}"
|
|
275
|
+
[name, "#{name}@#{line}"]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Writes the `parallel` line, once.
|
|
279
|
+
#
|
|
280
|
+
# @return [void]
|
|
281
|
+
def mark_parallel
|
|
282
|
+
return if parallel_marked
|
|
283
|
+
|
|
284
|
+
self.parallel_marked = true
|
|
285
|
+
KillChannel.write_parallel(channel)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# How many tests the loaded classes ran, or nil when it cannot be told.
|
|
289
|
+
# Asked after the run, because `runnable_methods` shuffles with the
|
|
290
|
+
# seed that `Minitest.run` sets.
|
|
291
|
+
#
|
|
292
|
+
# @return [Integer, nil]
|
|
293
|
+
def planned_tests
|
|
294
|
+
runnables.sum { |klass| klass.runnable_methods.size }
|
|
295
|
+
rescue StandardError
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|