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
data/lib/mutineer/cli.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
-
require "set"
|
|
5
4
|
require "open3"
|
|
6
5
|
require_relative "version"
|
|
7
6
|
require_relative "config"
|
|
@@ -11,6 +10,7 @@ require_relative "pairing"
|
|
|
11
10
|
require_relative "changed_lines"
|
|
12
11
|
require_relative "runner"
|
|
13
12
|
require_relative "reporter"
|
|
13
|
+
require_relative "kill_matrix"
|
|
14
14
|
require_relative "baseline"
|
|
15
15
|
require_relative "mutator_registry"
|
|
16
16
|
|
|
@@ -29,11 +29,12 @@ module Mutineer
|
|
|
29
29
|
Usage: mutineer [options] <command> [args]
|
|
30
30
|
|
|
31
31
|
Commands:
|
|
32
|
-
run [options] <source...> --test <test
|
|
32
|
+
run [options] <source...> --test <test> [--test <test>...]
|
|
33
|
+
Mutate, run, and report
|
|
33
34
|
run --dry-run [options] <source...> Print candidate mutations only
|
|
34
35
|
|
|
35
36
|
Run options:
|
|
36
|
-
--test FILE Test file covering the sources (
|
|
37
|
+
--test FILE Test file covering the sources (one per flag; repeat it)
|
|
37
38
|
--operators LIST Comma-separated operator names (default: Tier 1 set)
|
|
38
39
|
--threshold FLOAT Fail (exit 1) when score < FLOAT (default: 0 = off)
|
|
39
40
|
--baseline FILE Fail (exit 1) on NEW survivors / score drop vs a prior
|
|
@@ -42,12 +43,22 @@ module Mutineer
|
|
|
42
43
|
--only NAME Restrict to one fully-qualified subject
|
|
43
44
|
--since REF Only mutate lines changed since git REF (e.g. origin/main)
|
|
44
45
|
--no-since Disable diff scoping (a typed no beats a .mutineer.yml since:)
|
|
45
|
-
--jobs N Parallel worker count (default: processor count)
|
|
46
|
+
--jobs N Parallel worker count (default: processor count);
|
|
47
|
+
--test-command, --fail-fast, or --rails
|
|
48
|
+
without --daemon forces 1
|
|
46
49
|
--strategy NAME reload (whole-file) or redefine (surgical); default: reload
|
|
50
|
+
--timeout SECONDS Per-mutant time limit for in-process runs (default: 10;
|
|
51
|
+
not with --daemon or --test-command); a mutant over it
|
|
52
|
+
is a timeout, excluded from the score
|
|
53
|
+
--capture-timeout SECONDS Time limit for each coverage-capture subprocess
|
|
54
|
+
and the clean check (default: 120; not with --test-command)
|
|
55
|
+
--cache-dir DIR Directory for the coverage cache (default: .mutineer;
|
|
56
|
+
not with --test-command)
|
|
47
57
|
--framework NAME minitest or rspec (default: auto-detect from --test names)
|
|
48
58
|
--boot FILE Require FILE once in the parent to boot the app env, then
|
|
49
59
|
fork per mutant (Rails apps; requires --test)
|
|
50
|
-
--rails
|
|
60
|
+
--rails Boot config/environment; without --daemon, defaults to
|
|
61
|
+
redefine and serial execution
|
|
51
62
|
--test-command CMD Run the target suite in the app's own runtime as a
|
|
52
63
|
subprocess (for apps on Ruby < 3.4). CMD must contain
|
|
53
64
|
%{files}. Scrubs Mutineer Ruby PATH pins; set RAILS_ENV
|
|
@@ -58,7 +69,13 @@ module Mutineer
|
|
|
58
69
|
--format human|json|html Report format (default: human)
|
|
59
70
|
--output FILE Write the report to FILE instead of stdout
|
|
60
71
|
--dry-run List mutations without executing
|
|
61
|
-
--fail-fast Stop at the first surviving mutant
|
|
72
|
+
--fail-fast Stop at the first surviving mutant (--no-fail-fast beats a
|
|
73
|
+
.mutineer.yml fail_fast:)
|
|
74
|
+
--matrix Run every covering test for each mutant and report blind and
|
|
75
|
+
redundant tests; the JSON report also lists each mutant's
|
|
76
|
+
killers (in-process only; not with --daemon, --test-command,
|
|
77
|
+
--fail-fast or --dry-run; --no-matrix beats a .mutineer.yml
|
|
78
|
+
matrix:)
|
|
62
79
|
--verbose Surface the real error when a fork capture fails (alias: --debug)
|
|
63
80
|
|
|
64
81
|
Options:
|
|
@@ -67,19 +84,14 @@ module Mutineer
|
|
|
67
84
|
--help Print this help and exit
|
|
68
85
|
USAGE
|
|
69
86
|
|
|
70
|
-
# Field symbols whose config-file value is suppressed when the flag is typed.
|
|
71
|
-
PRECEDENCE_FLAGS = %i[operators jobs threshold only].freeze
|
|
72
|
-
|
|
73
|
-
# Deprecated internal strategy names, mapped to their canonical equivalents.
|
|
74
|
-
STRATEGY_ALIASES = { "7a" => "reload", "7b" => "redefine" }.freeze
|
|
75
|
-
|
|
76
87
|
# Parses arguments, executes the command, and exits.
|
|
77
88
|
#
|
|
78
89
|
# @param argv [Array<String>] raw command-line arguments.
|
|
79
90
|
# @return [void]
|
|
80
91
|
def self.start(argv)
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
# The CLI layer holds only the fields the user typed, so Config.resolve
|
|
93
|
+
# can tell a typed `false`/`nil` from an absent flag by whether the key exists.
|
|
94
|
+
opts = {}
|
|
83
95
|
show_operators = false
|
|
84
96
|
|
|
85
97
|
parser = OptionParser.new do |o|
|
|
@@ -94,47 +106,46 @@ module Mutineer
|
|
|
94
106
|
end
|
|
95
107
|
o.on("--list-operators") { show_operators = true }
|
|
96
108
|
o.on("--dry-run") { opts[:dry_run] = true }
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
o.on("--
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
o.on("--
|
|
109
|
+
# A typed --no-* beats the .mutineer.yml key, so a file key that
|
|
110
|
+
# conflicts with another flag can be turned off for one run.
|
|
111
|
+
o.on("--[no-]fail-fast") { |on| opts[:fail_fast] = on }
|
|
112
|
+
o.on("--[no-]matrix") { |on| opts[:matrix] = on }
|
|
113
|
+
o.on("--only NAME") { |v| opts[:only] = v }
|
|
114
|
+
o.on("--since REF") { |v| opts[:since] = Config.parse(:since, v) }
|
|
115
|
+
# A typed "no" must beat a .mutineer.yml `since:` key: the key is present
|
|
116
|
+
# with a nil value, and nil is a value.
|
|
117
|
+
o.on("--no-since") { opts[:since] = nil }
|
|
103
118
|
o.on("--test FILE") { |v| (opts[:tests] ||= []) << v }
|
|
104
|
-
o.on("--operators LIST")
|
|
105
|
-
|
|
106
|
-
f = Float(v, exception: false)
|
|
107
|
-
if f.nil?
|
|
108
|
-
warn "mutineer: --threshold requires a number between 0 and 100 (got: #{v.inspect})"
|
|
109
|
-
exit 2
|
|
110
|
-
end
|
|
111
|
-
opts[:threshold] = f
|
|
112
|
-
explicit << :threshold
|
|
119
|
+
o.on("--operators LIST") do |v|
|
|
120
|
+
opts[:operators] = Config.parse(:operators, v.split(",").map(&:strip))
|
|
113
121
|
end
|
|
114
|
-
o.on("--
|
|
115
|
-
o.on("--
|
|
116
|
-
o.on("--
|
|
117
|
-
o.on("--
|
|
122
|
+
o.on("--threshold FLOAT") { |v| opts[:threshold] = Config.parse(:threshold, v) }
|
|
123
|
+
o.on("--jobs N") { |v| opts[:jobs] = Config.parse(:jobs, v) }
|
|
124
|
+
o.on("--timeout SECONDS") { |v| opts[:timeout] = Config.parse(:timeout, v) }
|
|
125
|
+
o.on("--capture-timeout SECONDS") { |v| opts[:capture_timeout] = Config.parse(:capture_timeout, v) }
|
|
126
|
+
o.on("--cache-dir DIR") { |v| opts[:cache_dir] = Config.parse(:cache_dir, v) }
|
|
127
|
+
o.on("--strategy STRAT") { |v| opts[:strategy] = Config.parse(:strategy, v) }
|
|
128
|
+
o.on("--framework NAME") { |v| opts[:framework] = Config.parse(:framework, v) }
|
|
129
|
+
o.on("--boot FILE") { |v| opts[:boot] = v }
|
|
118
130
|
o.on("--rails") { opts[:rails] = true }
|
|
119
131
|
o.on("--verbose") { opts[:verbose] = true }
|
|
120
132
|
o.on("--debug") { opts[:verbose] = true } # alias of --verbose
|
|
121
|
-
o.on("--format FORMAT") { |v| opts[:format] = v }
|
|
133
|
+
o.on("--format FORMAT") { |v| opts[:format] = Config.parse(:format, v) }
|
|
122
134
|
o.on("--output FILE") { |v| opts[:output] = v }
|
|
123
|
-
# --baseline is
|
|
124
|
-
|
|
125
|
-
o.on("--baseline
|
|
126
|
-
o.on("--baseline-epsilon FLOAT") { |v| opts[:baseline_epsilon] = v.to_f }
|
|
135
|
+
# --baseline-epsilon is CLI-only.
|
|
136
|
+
o.on("--baseline FILE") { |v| opts[:baseline] = v }
|
|
137
|
+
o.on("--baseline-epsilon FLOAT") { |v| opts[:baseline_epsilon] = Config.parse(:baseline_epsilon, v) }
|
|
127
138
|
# Run the target suite as a subprocess in the app's OWN runtime so
|
|
128
139
|
# mutineer (Ruby >= 3.4) can mutation-test apps pinned to an older Ruby.
|
|
129
|
-
o.on("--test-command CMD") { |v| opts[:test_command] = v
|
|
140
|
+
o.on("--test-command CMD") { |v| opts[:test_command] = v }
|
|
130
141
|
# Boot the app ONCE in a persistent daemon and fork per mutant, with
|
|
131
142
|
# per-worker DB isolation so --jobs N is safe under Rails.
|
|
132
|
-
o.on("--daemon") { opts[:daemon] = true
|
|
143
|
+
o.on("--daemon") { opts[:daemon] = true }
|
|
133
144
|
end
|
|
134
145
|
|
|
135
146
|
begin
|
|
136
147
|
parser.parse!(argv)
|
|
137
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
148
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, Mutineer::ConfigError => e
|
|
138
149
|
warn "mutineer: #{e.message}"
|
|
139
150
|
exit 2
|
|
140
151
|
end
|
|
@@ -151,8 +162,8 @@ module Mutineer
|
|
|
151
162
|
|
|
152
163
|
begin
|
|
153
164
|
file_path = Config.find_file
|
|
154
|
-
file_hash = file_path ? Config.from_file(file_path) : {}
|
|
155
|
-
config = Config.resolve(opts, file_hash
|
|
165
|
+
file_hash = file_path ? Config.from_file(file_path, defer_operators: opts.key?(:operators)) : {}
|
|
166
|
+
config = Config.resolve(opts, file_hash)
|
|
156
167
|
rescue Mutineer::ConfigError => e
|
|
157
168
|
# The lib layer raises instead of killing the host; the CLI maps a
|
|
158
169
|
# config (usage) error to exit 2.
|
|
@@ -163,10 +174,16 @@ module Mutineer
|
|
|
163
174
|
case argv.first
|
|
164
175
|
when "run"
|
|
165
176
|
warn_config_root_mismatch(file_path, config.project_root) if file_path
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
tests_as_sources = argv[1..].grep(%r{(\A|/)(test|spec)/.*_(test|spec)\.rb\z})
|
|
178
|
+
if config.explicit?(:tests) && tests_as_sources.any?
|
|
179
|
+
warn "mutineer: #{tests_as_sources.join(', ')} looks like a test file, not a source. " \
|
|
180
|
+
"--test takes one file; repeat it for each test file (--test a_test.rb --test b_test.rb)"
|
|
181
|
+
exit 2
|
|
182
|
+
end
|
|
183
|
+
# A directory source expands to its **/*.rb files; a file inside the
|
|
184
|
+
# project becomes its root-relative path (#104). Test inference (when --test is omitted) happens in validate!.
|
|
168
185
|
config.sources = Pairing.expand_sources(argv[1..], project_root: config.project_root)
|
|
169
|
-
run(config
|
|
186
|
+
run(config)
|
|
170
187
|
else
|
|
171
188
|
warn "mutineer: unknown command '#{argv.first}'"
|
|
172
189
|
exit 2
|
|
@@ -187,14 +204,13 @@ module Mutineer
|
|
|
187
204
|
# Runs the requested command after validation.
|
|
188
205
|
#
|
|
189
206
|
# @param config [Mutineer::Config] run configuration.
|
|
190
|
-
# @param explicit [Set<Symbol>] explicit CLI fields.
|
|
191
207
|
# @return [void]
|
|
192
|
-
def self.run(config
|
|
208
|
+
def self.run(config)
|
|
193
209
|
if config.sources.empty?
|
|
194
210
|
warn "mutineer: run requires at least one source file"
|
|
195
211
|
exit 2
|
|
196
212
|
end
|
|
197
|
-
validate!(config
|
|
213
|
+
validate!(config)
|
|
198
214
|
|
|
199
215
|
config.dry_run ? dry_run(config) : execute(config)
|
|
200
216
|
rescue ArgumentError => e
|
|
@@ -236,39 +252,10 @@ module Mutineer
|
|
|
236
252
|
#
|
|
237
253
|
# @api private
|
|
238
254
|
# @param config [Mutineer::Config] run configuration.
|
|
239
|
-
# @param explicit [Set<Symbol>] explicit CLI fields.
|
|
240
255
|
# @return [void]
|
|
241
|
-
def self.validate!(config
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
exit 2
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
jobs = Integer(config.jobs.to_s, exception: false)
|
|
248
|
-
if jobs.nil? || jobs < 1
|
|
249
|
-
warn "mutineer: --jobs requires a positive integer (got: #{config.jobs})"
|
|
250
|
-
exit 2
|
|
251
|
-
end
|
|
252
|
-
config.jobs = jobs
|
|
253
|
-
|
|
254
|
-
unless %w[human json html].include?(config.format)
|
|
255
|
-
warn %(mutineer: unknown format "#{config.format}". Expected: human, json, html)
|
|
256
|
-
exit 2
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# Canonical strategies are reload|redefine; 7a/7b are accepted as deprecated
|
|
260
|
-
# aliases. Normalize to canonical so the rest of the pipeline sees one name.
|
|
261
|
-
config.strategy = STRATEGY_ALIASES.fetch(config.strategy, config.strategy)
|
|
262
|
-
unless %w[reload redefine].include?(config.strategy)
|
|
263
|
-
warn %(mutineer: unknown strategy "#{config.strategy}". Expected: reload, redefine)
|
|
264
|
-
exit 2
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
unless %w[minitest rspec].include?(config.framework)
|
|
268
|
-
warn %(mutineer: unknown framework "#{config.framework}". Expected: minitest, rspec)
|
|
269
|
-
exit 2
|
|
270
|
-
end
|
|
271
|
-
|
|
256
|
+
def self.validate!(config)
|
|
257
|
+
# First, so a conflict is reported before another check rewrites the config.
|
|
258
|
+
validate_matrix!(config) if config.matrix
|
|
272
259
|
validate_test_command!(config) if config.test_command
|
|
273
260
|
|
|
274
261
|
validate_since!(config) if config.since
|
|
@@ -277,7 +264,7 @@ module Mutineer
|
|
|
277
264
|
|
|
278
265
|
# When --test is omitted, infer each source's test by convention. Autopair
|
|
279
266
|
# also re-detects framework from inferred tests when --framework was not set.
|
|
280
|
-
autopair!(config
|
|
267
|
+
autopair!(config) unless config.dry_run
|
|
281
268
|
|
|
282
269
|
# Daemon validation runs AFTER autopair so auto-inferred *_spec.rb tests
|
|
283
270
|
# cannot bypass the RSpec rejection (framework would still be minitest if we
|
|
@@ -322,6 +309,7 @@ module Mutineer
|
|
|
322
309
|
"(surgical redefine needs a shared VM; the subprocess has its own)"
|
|
323
310
|
exit 2
|
|
324
311
|
end
|
|
312
|
+
warn_unused_settings(config, "--test-command", %i[timeout capture_timeout cache_dir])
|
|
325
313
|
return unless config.jobs > 1
|
|
326
314
|
|
|
327
315
|
warn "[mutineer] --test-command runs serially (no per-worker DB isolation yet); forcing --jobs 1."
|
|
@@ -350,6 +338,7 @@ module Mutineer
|
|
|
350
338
|
"(rspec is not implemented on the daemon path yet)"
|
|
351
339
|
exit 2
|
|
352
340
|
end
|
|
341
|
+
warn_unused_settings(config, "--daemon", %i[timeout])
|
|
353
342
|
return if config.strategy == "reload"
|
|
354
343
|
|
|
355
344
|
# --rails defaults strategy to redefine; daemon always whole-file loads.
|
|
@@ -358,6 +347,66 @@ module Mutineer
|
|
|
358
347
|
config.strategy = "reload"
|
|
359
348
|
end
|
|
360
349
|
|
|
350
|
+
# Where each setting a backend may ignore applies, for the warning.
|
|
351
|
+
UNUSED_SETTING_SCOPE = {
|
|
352
|
+
timeout: "in-process runs only",
|
|
353
|
+
capture_timeout: "in-process and --daemon coverage capture",
|
|
354
|
+
cache_dir: "the in-process and --daemon coverage cache"
|
|
355
|
+
}.freeze
|
|
356
|
+
|
|
357
|
+
# A backend that never reads a setting the user set would silently do
|
|
358
|
+
# nothing with it, so say so. `--test-command` builds no coverage map and
|
|
359
|
+
# has its own runtime, so it reads none of {UNUSED_SETTING_SCOPE}; the
|
|
360
|
+
# daemon reads the capture limit and the cache directory but keeps its own
|
|
361
|
+
# per-mutant limit.
|
|
362
|
+
#
|
|
363
|
+
# @api private
|
|
364
|
+
# @param config [Mutineer::Config] run configuration.
|
|
365
|
+
# @param backend [String] the backend's flag.
|
|
366
|
+
# @param keys [Array<Symbol>] the settings that backend ignores.
|
|
367
|
+
# @return [void]
|
|
368
|
+
def self.warn_unused_settings(config, backend, keys)
|
|
369
|
+
keys.each do |key|
|
|
370
|
+
next unless config.explicit?(key)
|
|
371
|
+
|
|
372
|
+
applies = UNUSED_SETTING_SCOPE.fetch(key)
|
|
373
|
+
warn "[mutineer] #{config.origin(key)} has no effect with #{backend} (it applies to #{applies}); ignoring it."
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# --matrix runs on the in-process backend and needs every mutant's whole
|
|
378
|
+
# covering run, so a backend that cannot name the failing test, or a run that
|
|
379
|
+
# stops early, is a usage error (exit 2), never a quietly partial matrix.
|
|
380
|
+
#
|
|
381
|
+
# @api private
|
|
382
|
+
# @param config [Mutineer::Config] run configuration.
|
|
383
|
+
# @return [void]
|
|
384
|
+
def self.validate_matrix!(config)
|
|
385
|
+
conflict, reason =
|
|
386
|
+
if config.dry_run
|
|
387
|
+
[:dry_run, "a dry run runs no tests, so it has no matrix"]
|
|
388
|
+
elsif config.daemon
|
|
389
|
+
[:daemon, "the kill matrix runs on the in-process backend only"]
|
|
390
|
+
elsif config.test_command
|
|
391
|
+
[:test_command, "the external suite reports pass or fail, not which test failed"]
|
|
392
|
+
elsif config.fail_fast
|
|
393
|
+
[:fail_fast, "a fail-fast run is partial, so blind and redundant tests would be wrong"]
|
|
394
|
+
end
|
|
395
|
+
return unless conflict
|
|
396
|
+
|
|
397
|
+
# Say how to turn off whichever side came from the file, for this run.
|
|
398
|
+
hint =
|
|
399
|
+
if config.origin(:matrix) != "--matrix"
|
|
400
|
+
"; pass --no-matrix to run without it"
|
|
401
|
+
elsif conflict == :fail_fast && config.origin(:fail_fast) != "--fail-fast"
|
|
402
|
+
"; pass --no-fail-fast to run without it"
|
|
403
|
+
else
|
|
404
|
+
""
|
|
405
|
+
end
|
|
406
|
+
warn "mutineer: #{config.origin(:matrix)} cannot be combined with #{config.origin(conflict)} (#{reason}#{hint})"
|
|
407
|
+
exit 2
|
|
408
|
+
end
|
|
409
|
+
|
|
361
410
|
# --since needs a real git repo and a resolvable ref; either failure is a
|
|
362
411
|
# usage error (exit 2) so CI sees "bad invocation," not "tests too weak."
|
|
363
412
|
#
|
|
@@ -399,30 +448,30 @@ module Mutineer
|
|
|
399
448
|
end
|
|
400
449
|
|
|
401
450
|
# Auto-pair sources to tests by path convention when no --test was given
|
|
402
|
-
# (explicit --test wins). Each source with
|
|
403
|
-
# run; a source with none is dropped with a one-line stderr
|
|
404
|
-
# run continues with the rest.
|
|
451
|
+
# (explicit --test wins). Each source with at least one inferred test on
|
|
452
|
+
# disk joins the run; a source with none is dropped with a one-line stderr
|
|
453
|
+
# warning and the run continues with the rest. Split files for one source
|
|
454
|
+
# are all kept (#87). If every source is dropped: in boot mode the
|
|
405
455
|
# dedicated --boot/--rails-requires-test check reports it; otherwise exit 2
|
|
406
456
|
# with a usage message. The framework is re-detected from the inferred set
|
|
407
457
|
# unless it was set explicitly (a spec-only project loads/reports as rspec).
|
|
408
458
|
#
|
|
409
459
|
# @api private
|
|
410
460
|
# @param config [Mutineer::Config] run configuration.
|
|
411
|
-
# @param explicit [Set<Symbol>] explicit CLI fields.
|
|
412
461
|
# @return [void]
|
|
413
|
-
def self.autopair!(config
|
|
462
|
+
def self.autopair!(config)
|
|
414
463
|
return unless config.tests.empty?
|
|
415
464
|
|
|
416
465
|
paired = config.sources.filter_map do |s|
|
|
417
|
-
|
|
418
|
-
[s,
|
|
466
|
+
tests = Pairing.infer_tests(s, project_root: config.project_root, prefer: config.framework)
|
|
467
|
+
[s, tests] unless tests.empty?
|
|
419
468
|
end
|
|
420
469
|
(config.sources - paired.map(&:first)).each do |s|
|
|
421
470
|
warn "[mutineer] no test found by convention for #{s}; skipping"
|
|
422
471
|
end
|
|
423
472
|
config.sources = paired.map(&:first)
|
|
424
|
-
config.tests = paired.
|
|
425
|
-
config.framework = Config.detect_framework(config.tests) unless explicit
|
|
473
|
+
config.tests = paired.flat_map(&:last).uniq
|
|
474
|
+
config.framework = Config.detect_framework(config.tests) unless config.explicit?(:framework)
|
|
426
475
|
|
|
427
476
|
return unless config.sources.empty?
|
|
428
477
|
return if config.boot # let the --boot/--rails-requires-test check report it
|
|
@@ -472,7 +521,8 @@ module Mutineer
|
|
|
472
521
|
|
|
473
522
|
aggregate, source_map, extras = Runner.execute(config)
|
|
474
523
|
warn_legacy_ignore_matches(extras[:legacy_ignore_matches])
|
|
475
|
-
|
|
524
|
+
matrix = KillMatrix.new(aggregate.results) if config.matrix
|
|
525
|
+
reporter = Reporter.new(aggregate, source_map, matrix: matrix)
|
|
476
526
|
|
|
477
527
|
# Diff the current run against the baseline (preflighted above) by the
|
|
478
528
|
# stable survivor id. The delta is rendered inline (human section / additive
|