mutineer-mongoid 2.0.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 +7 -0
- data/CHANGELOG.md +1194 -0
- data/LICENSE +21 -0
- data/README.md +659 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/baseline.rb +185 -0
- data/lib/mutineer/changed_lines.rb +136 -0
- data/lib/mutineer/child_stdout.rb +35 -0
- data/lib/mutineer/cli.rb +681 -0
- data/lib/mutineer/config.rb +372 -0
- data/lib/mutineer/coverage_map.rb +1366 -0
- data/lib/mutineer/daemon_backend.rb +351 -0
- data/lib/mutineer/daemon_client.rb +407 -0
- data/lib/mutineer/daemon_server.rb +351 -0
- data/lib/mutineer/external_backend.rb +288 -0
- data/lib/mutineer/file_swap.rb +194 -0
- data/lib/mutineer/isolation.rb +321 -0
- data/lib/mutineer/job_plan.rb +352 -0
- 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 +145 -0
- data/lib/mutineer/minitest_integration.rb +137 -0
- data/lib/mutineer/mongoid_failures.rb +82 -0
- data/lib/mutineer/mongoid_worker_db.rb +97 -0
- data/lib/mutineer/mutant_id.rb +126 -0
- data/lib/mutineer/mutation.rb +27 -0
- data/lib/mutineer/mutator_registry.rb +109 -0
- data/lib/mutineer/mutators/arithmetic.rb +35 -0
- data/lib/mutineer/mutators/array_literal.rb +44 -0
- data/lib/mutineer/mutators/base.rb +105 -0
- data/lib/mutineer/mutators/boolean_connector.rb +52 -0
- data/lib/mutineer/mutators/boolean_literal.rb +56 -0
- data/lib/mutineer/mutators/chain_link.rb +132 -0
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +35 -0
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/condition_negation.rb +49 -0
- data/lib/mutineer/mutators/literal_mutation.rb +51 -0
- data/lib/mutineer/mutators/negation_removal.rb +44 -0
- data/lib/mutineer/mutators/operand_removal.rb +58 -0
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/range_literal.rb +54 -0
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +72 -0
- data/lib/mutineer/mutators/safe_navigation.rb +76 -0
- data/lib/mutineer/mutators/statement_removal.rb +33 -0
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/pairing.rb +298 -0
- data/lib/mutineer/parser.rb +45 -0
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/project.rb +506 -0
- data/lib/mutineer/project_path.rb +57 -0
- data/lib/mutineer/rails_worker_db.rb +212 -0
- data/lib/mutineer/reporter.rb +926 -0
- data/lib/mutineer/result.rb +240 -0
- data/lib/mutineer/runner.rb +418 -0
- data/lib/mutineer/statement_lines.rb +183 -0
- data/lib/mutineer/subject.rb +38 -0
- data/lib/mutineer/test_runners/minitest.rb +22 -0
- data/lib/mutineer/test_runners/rspec.rb +330 -0
- data/lib/mutineer/test_runners.rb +30 -0
- data/lib/mutineer/version.rb +6 -0
- data/lib/mutineer/worker_pool.rb +147 -0
- data/lib/mutineer.rb +48 -0
- metadata +143 -0
|
@@ -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
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutineer
|
|
4
|
+
class MinitestIntegration
|
|
5
|
+
# Finds the outer reporter of a Minitest run, for the hooks that act only on
|
|
6
|
+
# the run's own results. A test can build a reporter of its own and record
|
|
7
|
+
# on it, or start a nested run, so the first reporter the run hands to its
|
|
8
|
+
# suites is the one that counts. Child-process only, like
|
|
9
|
+
# MinitestIntegration.
|
|
10
|
+
#
|
|
11
|
+
# A client module extends {Client}, registers here, and sets its armed pid.
|
|
12
|
+
# The hook then gives the outer reporter to each client armed in this
|
|
13
|
+
# process. Minitest 5 and 6 need different hook points; an unknown shape
|
|
14
|
+
# gets no hook.
|
|
15
|
+
module OuterReporter
|
|
16
|
+
# Prepended on the `Minitest` singleton class for Minitest 6.
|
|
17
|
+
module Hook6
|
|
18
|
+
# Hands the outer reporter to the armed clients. Only the first call
|
|
19
|
+
# counts, because a test can call this method with its own reporter.
|
|
20
|
+
#
|
|
21
|
+
# @param reporter [Minitest::CompositeReporter] the outer reporter.
|
|
22
|
+
# @param options [Hash] the Minitest options.
|
|
23
|
+
# @return [Object] whatever Minitest returns.
|
|
24
|
+
def run_all_suites(reporter, options)
|
|
25
|
+
OuterReporter.seen(reporter)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prepended on the `Minitest` singleton class for Minitest 5.
|
|
31
|
+
module Hook5
|
|
32
|
+
# The Minitest 5 form of Hook6#run_all_suites.
|
|
33
|
+
#
|
|
34
|
+
# @param reporter [Minitest::CompositeReporter] the outer reporter.
|
|
35
|
+
# @param options [Hash] the Minitest options.
|
|
36
|
+
# @return [Object] whatever Minitest returns.
|
|
37
|
+
def __run(reporter, options)
|
|
38
|
+
OuterReporter.seen(reporter)
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The per-process state a client keeps. Forked workers inherit it, and
|
|
44
|
+
# the pid keeps them from acting on it.
|
|
45
|
+
module Client
|
|
46
|
+
# The pid of the armed process.
|
|
47
|
+
#
|
|
48
|
+
# @return [Integer, nil]
|
|
49
|
+
attr_accessor :armed_pid
|
|
50
|
+
|
|
51
|
+
# The outer reporter of the armed run.
|
|
52
|
+
#
|
|
53
|
+
# @return [Minitest::CompositeReporter, nil]
|
|
54
|
+
attr_accessor :armed_reporter
|
|
55
|
+
|
|
56
|
+
# True when the client is armed in this process.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def armed_here?
|
|
60
|
+
armed_pid == Process.pid
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# True when `reporter` is the outer reporter of the run armed in this
|
|
64
|
+
# process.
|
|
65
|
+
#
|
|
66
|
+
# @param reporter [Minitest::CompositeReporter] the recording reporter.
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def outer_reporter?(reporter)
|
|
69
|
+
armed_here? && reporter.equal?(armed_reporter)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
# The registered clients, in registration order.
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<Module>]
|
|
77
|
+
def clients
|
|
78
|
+
@clients ||= []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Registers a client once.
|
|
82
|
+
#
|
|
83
|
+
# @param client [Module] a module that extends {Client}.
|
|
84
|
+
# @return [void]
|
|
85
|
+
def register(client)
|
|
86
|
+
clients << client unless clients.include?(client)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Gives `reporter` to every client armed in this process that has no
|
|
90
|
+
# outer reporter yet.
|
|
91
|
+
#
|
|
92
|
+
# @param reporter [Minitest::CompositeReporter] a reporter a run uses.
|
|
93
|
+
# @return [void]
|
|
94
|
+
def seen(reporter)
|
|
95
|
+
clients.each do |client|
|
|
96
|
+
client.armed_reporter ||= reporter if client.armed_here?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The outer-reporter hook for the loaded Minitest.
|
|
101
|
+
#
|
|
102
|
+
# @return [Module, nil] {Hook6}, {Hook5}, or nil for an unknown shape.
|
|
103
|
+
def hook_for_loaded_minitest
|
|
104
|
+
runnable = ::Minitest::Runnable
|
|
105
|
+
if ::Minitest.respond_to?(:run_all_suites) && runnable.respond_to?(:run_suite)
|
|
106
|
+
Hook6
|
|
107
|
+
elsif ::Minitest.respond_to?(:__run) && runnable.respond_to?(:run_one_method)
|
|
108
|
+
Hook5
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Prepends `mod` on `target` once. A copy on a superclass does not
|
|
113
|
+
# count, because it comes after the own methods of `target`.
|
|
114
|
+
#
|
|
115
|
+
# @param target [Module] the class or singleton class.
|
|
116
|
+
# @param mod [Module] the module to prepend.
|
|
117
|
+
# @return [void]
|
|
118
|
+
def prepend_once(target, mod)
|
|
119
|
+
return if target.ancestors.take_while { |a| !a.equal?(target) }.include?(mod)
|
|
120
|
+
|
|
121
|
+
target.prepend(mod)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "outer_reporter"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
class MinitestIntegration
|
|
7
|
+
# Stops a Minitest run at the first failing test: one failure already
|
|
8
|
+
# kills the mutant. Child-process only, like MinitestIntegration.
|
|
9
|
+
#
|
|
10
|
+
# It sets a flag instead of unwinding the stack, so that class-level
|
|
11
|
+
# wrappers (`after_all`, a block-form transaction) still finish. The
|
|
12
|
+
# remaining tests and classes then return before they start.
|
|
13
|
+
#
|
|
14
|
+
# Minitest 5 and 6 need different hook points. An unknown shape installs
|
|
15
|
+
# no hook, and the run is a full run. The outer reporter comes from
|
|
16
|
+
# {OuterReporter}, shared with {KillRecorder}.
|
|
17
|
+
module StopAtFirstFailure
|
|
18
|
+
extend OuterReporter::Client
|
|
19
|
+
|
|
20
|
+
# Prepended on `Minitest::CompositeReporter`.
|
|
21
|
+
module RecordFailure
|
|
22
|
+
# Records the result, then sets the stop flag on a failure or an
|
|
23
|
+
# error. Only the outer reporter counts: a test can build and record
|
|
24
|
+
# on its own reporter.
|
|
25
|
+
#
|
|
26
|
+
# @param result [Minitest::Result] the result of one test.
|
|
27
|
+
# @return [void]
|
|
28
|
+
def record(result)
|
|
29
|
+
super
|
|
30
|
+
return if result.passed? || result.skipped?
|
|
31
|
+
return unless StopAtFirstFailure.outer_reporter?(self)
|
|
32
|
+
|
|
33
|
+
StopAtFirstFailure.stopped = true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Prepended on the singleton class of each test class for Minitest 6.
|
|
38
|
+
module SkipAfterStop6
|
|
39
|
+
# Skips the test class after a stop.
|
|
40
|
+
#
|
|
41
|
+
# @param reporter [Minitest::CompositeReporter] the reporter.
|
|
42
|
+
# @param options [Hash] the Minitest options.
|
|
43
|
+
# @return [Object, nil] whatever Minitest returns, or nil when skipped.
|
|
44
|
+
def run_suite(reporter, options = {})
|
|
45
|
+
return if StopAtFirstFailure.stopped_here?
|
|
46
|
+
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Skips one test after a stop, without a record.
|
|
51
|
+
#
|
|
52
|
+
# @param klass [Class] the test class.
|
|
53
|
+
# @param method_name [String] the test method.
|
|
54
|
+
# @param reporter [Minitest::CompositeReporter] the reporter.
|
|
55
|
+
# @return [Object, nil] whatever Minitest returns, or nil when skipped.
|
|
56
|
+
def run(klass, method_name, reporter)
|
|
57
|
+
return if StopAtFirstFailure.stopped_here?
|
|
58
|
+
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Prepended on the singleton class of each test class for Minitest 5.
|
|
64
|
+
module SkipAfterStop5
|
|
65
|
+
# Skips the test class after a stop.
|
|
66
|
+
#
|
|
67
|
+
# @param reporter [Minitest::CompositeReporter] the reporter.
|
|
68
|
+
# @param options [Hash] the Minitest options.
|
|
69
|
+
# @return [Object, nil] whatever Minitest returns, or nil when skipped.
|
|
70
|
+
def run(reporter, options = {})
|
|
71
|
+
return if StopAtFirstFailure.stopped_here?
|
|
72
|
+
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Skips one test after a stop, without a record.
|
|
77
|
+
#
|
|
78
|
+
# @param klass [Class] the test class.
|
|
79
|
+
# @param method_name [String] the test method.
|
|
80
|
+
# @param reporter [Minitest::CompositeReporter] the reporter.
|
|
81
|
+
# @return [Object, nil] whatever Minitest returns, or nil when skipped.
|
|
82
|
+
def run_one_method(klass, method_name, reporter)
|
|
83
|
+
return if StopAtFirstFailure.stopped_here?
|
|
84
|
+
|
|
85
|
+
super
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class << self
|
|
90
|
+
# True after the outer reporter records a failure or an error.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean, nil]
|
|
93
|
+
attr_accessor :stopped
|
|
94
|
+
|
|
95
|
+
# Installs the hooks and arms the stop for this process. Call it after
|
|
96
|
+
# the test files load, so that every test class gets its prepend.
|
|
97
|
+
#
|
|
98
|
+
# @param runnables [Array<Class>] the loaded test classes.
|
|
99
|
+
# @return [Boolean] false when the Minitest shape is unknown.
|
|
100
|
+
def arm!(runnables)
|
|
101
|
+
outer, skip = hooks_for_loaded_minitest
|
|
102
|
+
return false unless outer
|
|
103
|
+
|
|
104
|
+
OuterReporter.prepend_once(::Minitest::CompositeReporter, RecordFailure)
|
|
105
|
+
OuterReporter.prepend_once(::Minitest.singleton_class, outer)
|
|
106
|
+
runnables.each { |klass| OuterReporter.prepend_once(klass.singleton_class, skip) }
|
|
107
|
+
OuterReporter.register(self)
|
|
108
|
+
self.armed_pid = Process.pid
|
|
109
|
+
self.armed_reporter = nil
|
|
110
|
+
self.stopped = false
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Clears the armed state.
|
|
115
|
+
#
|
|
116
|
+
# @return [void]
|
|
117
|
+
def disarm!
|
|
118
|
+
self.armed_pid = nil
|
|
119
|
+
self.armed_reporter = nil
|
|
120
|
+
self.stopped = false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# True when the armed run in this process has stopped.
|
|
124
|
+
#
|
|
125
|
+
# @return [Boolean]
|
|
126
|
+
def stopped_here?
|
|
127
|
+
stopped == true && armed_here?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# Picks the hook modules for the loaded Minitest.
|
|
133
|
+
#
|
|
134
|
+
# @return [Array(Module, Module), nil] the outer reporter hook and the
|
|
135
|
+
# skip hook, or nil for an unknown shape.
|
|
136
|
+
def hooks_for_loaded_minitest
|
|
137
|
+
outer = OuterReporter.hook_for_loaded_minitest
|
|
138
|
+
if outer.equal?(OuterReporter::Hook6) then [outer, SkipAfterStop6]
|
|
139
|
+
elsif outer.equal?(OuterReporter::Hook5) then [outer, SkipAfterStop5]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|