mutineer 0.11.2 → 0.11.4
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 +67 -0
- data/README.md +2 -2
- data/lib/mutineer/baseline.rb +14 -16
- data/lib/mutineer/cli.rb +37 -39
- data/lib/mutineer/config.rb +17 -19
- data/lib/mutineer/coverage_map.rb +54 -52
- data/lib/mutineer/daemon_backend.rb +316 -0
- data/lib/mutineer/daemon_client.rb +60 -36
- data/lib/mutineer/daemon_server.rb +65 -61
- data/lib/mutineer/file_swap.rb +16 -15
- data/lib/mutineer/rails_worker_db.rb +44 -43
- data/lib/mutineer/reporter.rb +162 -41
- data/lib/mutineer/result.rb +25 -26
- data/lib/mutineer/runner.rb +69 -284
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +23 -23
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85591c1b61bcdc3c8a1d39b5b657f667bf7de9039d61e05ab3e5691692f555f0
|
|
4
|
+
data.tar.gz: 76acdc054e82d9ea10dee4a63bcbb5f0f86db37dd585323f734991f413f21e61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5855d2e0a9d4e226fe0ad2034cc4d08f4b5ac5e2d34df9e92f97b9d344ea5a50c4df01bbdcb0973dc66e8ccde717baab8842935fa07a4cad1238f77b9bebf42
|
|
7
|
+
data.tar.gz: 3a5c57795de651128a3d3597b2f1776fd32ff76aeaea4edf14037a1ec32e13e72e54c38a56ef8c209f3ee1d6afc0ffbb5fd47153967803c841e64d989d80a047
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,71 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.11.4] - 2026-07-29
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **`--threshold` now gates on the run being complete, not just its score** — a
|
|
13
|
+
score is `killed / (killed + survived)`, so mutants that error, time out or come
|
|
14
|
+
back uncapturable are excluded from the denominator: a broken harness *raised*
|
|
15
|
+
the score instead of lowering it. Ninety errored mutants and ten that ran (nine
|
|
16
|
+
killed) reported 90% and exited 0, so CI could not tell a complete run from a
|
|
17
|
+
mostly-broken one. Past 10% of attempted mutants producing no verdict, a positive
|
|
18
|
+
`--threshold` now exits 1 whatever the score, and the report says which states
|
|
19
|
+
broke, on every `--format`. A single bad mutant never trips it, however small the
|
|
20
|
+
run, so a `--since` PR with a handful of mutants keeps its flake tolerance (#78).
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`no_verdict[]` in the JSON report** (`schema_version` 1.2) — every attempted
|
|
24
|
+
mutant that produced no verdict, with its `status` and, where there is one, the
|
|
25
|
+
`details` explaining the cause. `Result#details` was built and rendered in no
|
|
26
|
+
format at all, so a daemon crash reached the user as nothing but a larger errored
|
|
27
|
+
count. `summary` gains `attempted` and `no_verdict`, the two figures the
|
|
28
|
+
completeness gate is computed from (#78).
|
|
29
|
+
|
|
30
|
+
## [0.11.3] - 2026-07-29
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **Nothing to mutate now costs nothing** — `--since` matching no changed line (a
|
|
34
|
+
docs-only PR, which README documents `--since origin/<base>` for) still did the
|
|
35
|
+
expensive part before discovering there was no work: `--daemon` booted the app
|
|
36
|
+
once for the coverage map and again for every `--jobs` worker, and
|
|
37
|
+
`--test-command` ran the whole suite for a smoke check that calibrates a timeout
|
|
38
|
+
no mutant would use. Both backends now return as soon as the job list is empty.
|
|
39
|
+
The in-process path is unchanged: it boots before collecting jobs, so it cannot
|
|
40
|
+
know the list is empty yet. One consequence worth stating: a zero-job `--daemon`
|
|
41
|
+
run no longer boots the app, so an app that fails to boot now exits 0 (nothing
|
|
42
|
+
to do) instead of exit 1 (#76).
|
|
43
|
+
- **A dead daemon ends the run instead of scoring the rest against it** — a
|
|
44
|
+
daemon that dies while running one mutant was already handled: `DaemonClient`
|
|
45
|
+
respawns and answers `error` for that mutant. But a daemon that could not come
|
|
46
|
+
back was not. `restart!` closes the pipes before respawning, so if the respawn
|
|
47
|
+
failed at the OS level (`EMFILE` or `ENOMEM` under `--jobs N`, `ENOENT` when
|
|
48
|
+
`bundle` does not resolve) the client was left permanently dead while raising
|
|
49
|
+
errors that read as ordinary per-mutant failures. Every remaining mutant scored
|
|
50
|
+
`error` against nothing, and Mutineer printed a mutation score built on the
|
|
51
|
+
fraction of mutants that ran before the daemon died. `DaemonClient` now raises
|
|
52
|
+
`DaemonBootError` whenever it is gone for good — a refused spawn, a failed boot
|
|
53
|
+
handshake on respawn, closed pipes, or `MAX_RESTARTS` crashes — and the CLI
|
|
54
|
+
reports it as a message and exit 1 rather than a backtrace. Under `--jobs N`
|
|
55
|
+
the remaining queue is dropped so sibling workers stop too. Errored mutants
|
|
56
|
+
still cannot fail the `--threshold` gate once any mutant is scored (#78).
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- **Daemon orchestration split out of `Runner`** — the persistent-daemon backend
|
|
60
|
+
(job fan-out, worker DBs, coverage-map build, boot config, verdict mapping) now
|
|
61
|
+
lives in `Mutineer::DaemonBackend`. `Runner` keeps job collection, coverage
|
|
62
|
+
selection and the single in-process mutant run, and drops from 662 to 439 lines.
|
|
63
|
+
The external backend's orchestration stays on `Runner` for now, so the two
|
|
64
|
+
backends are not yet symmetrical. No CLI or behaviour change (#58).
|
|
65
|
+
Internal-only removals: `Runner.execute_daemon`, `Runner.daemon_coverage_map`
|
|
66
|
+
and `Runner::DAEMON_TIMEOUT` no longer exist. They were never documented —
|
|
67
|
+
the supported programmatic contract is the JSON report, the stable mutant ids
|
|
68
|
+
and the exit codes — so only code calling `Runner` internals directly is
|
|
69
|
+
affected.
|
|
70
|
+
- **Comment diet on orchestration files** — present-tense YARD contracts; drop
|
|
71
|
+
ticket/phase/KTD history tags from runner, CLI, daemon pair, coverage map,
|
|
72
|
+
reporter, result, and related modules. Safety and score invariants kept (#57).
|
|
73
|
+
|
|
9
74
|
## [0.11.2] - 2026-07-24
|
|
10
75
|
|
|
11
76
|
### Fixed
|
|
@@ -253,6 +318,8 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
|
|
|
253
318
|
- `.mutineer.yml` configuration (CLI > config > default precedence).
|
|
254
319
|
- Byte-correct source handling for multibyte (UTF-8) sources.
|
|
255
320
|
|
|
321
|
+
[0.11.4]: https://github.com/davidteren/mutineer/releases/tag/v0.11.4
|
|
322
|
+
[0.11.3]: https://github.com/davidteren/mutineer/releases/tag/v0.11.3
|
|
256
323
|
[0.11.2]: https://github.com/davidteren/mutineer/releases/tag/v0.11.2
|
|
257
324
|
[0.11.1]: https://github.com/davidteren/mutineer/releases/tag/v0.11.1
|
|
258
325
|
[0.11.0]: https://github.com/davidteren/mutineer/releases/tag/v0.11.0
|
data/README.md
CHANGED
|
@@ -47,7 +47,7 @@ mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
|
|
|
47
47
|
|------|---------|
|
|
48
48
|
| `--test FILE` | Test file covering the sources (repeatable) |
|
|
49
49
|
| `--operators LIST` | Comma-separated operator names (default: the Tier-1 set) |
|
|
50
|
-
| `--threshold FLOAT` | Exit 1 when the score is below FLOAT (default: 0 = off) |
|
|
50
|
+
| `--threshold FLOAT` | Exit 1 when the score is below FLOAT, or when nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted (default: 0 = off) |
|
|
51
51
|
| `--only NAME` | Restrict to one fully-qualified subject, e.g. `Calculator#add` |
|
|
52
52
|
| `--framework NAME` | `minitest` (default) or `rspec`; auto-detected as rspec when most `--test` files end in `_spec.rb` |
|
|
53
53
|
| `--since REF` | Only mutate lines changed since git `REF` (e.g. `origin/main`) — ideal for PR CI |
|
|
@@ -70,7 +70,7 @@ mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
|
|
|
70
70
|
| Code | Meaning |
|
|
71
71
|
|------|---------|
|
|
72
72
|
| `0` | Score ≥ threshold (or no threshold set) |
|
|
73
|
-
| `1` |
|
|
73
|
+
| `1` | Score below threshold, nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted, a `--baseline` regression, or a runtime error |
|
|
74
74
|
| `2` | Usage / invalid-flag error |
|
|
75
75
|
|
|
76
76
|
### Operators
|
data/lib/mutineer/baseline.rb
CHANGED
|
@@ -4,29 +4,27 @@ require "json"
|
|
|
4
4
|
require_relative "config" # for Mutineer::ConfigError
|
|
5
5
|
|
|
6
6
|
module Mutineer
|
|
7
|
-
#
|
|
8
|
-
# `mutineer run --format json` document (
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# fork, no Rails — so it's testable in isolation from a canned JSON + a hand-
|
|
14
|
-
# built AggregateResult.
|
|
7
|
+
# CI baseline/delta gating. A baseline is a prior
|
|
8
|
+
# `mutineer run --format json` document (no bespoke format to version).
|
|
9
|
+
# Diff the current run against it by the stable survivor id: a NEW survivor
|
|
10
|
+
# (id present now, absent in the baseline) OR a score drop is a regression the
|
|
11
|
+
# CLI turns into exit 1. Pure data, stdlib `json` only, no fork, no Rails, so
|
|
12
|
+
# it is testable in isolation from a canned JSON + a hand-built AggregateResult.
|
|
15
13
|
class Baseline
|
|
16
14
|
# The verdict of diffing a current run against the baseline.
|
|
17
|
-
# new_survivors
|
|
15
|
+
# new_survivors - current Result objects whose stable id is absent from
|
|
18
16
|
# the baseline (the regressions to name).
|
|
19
|
-
# fixed_survivors
|
|
17
|
+
# fixed_survivors - baseline survivor hashes absent from the current run
|
|
20
18
|
# (informational, never gates).
|
|
21
|
-
# score_drop
|
|
19
|
+
# score_drop - current score < baseline score - epsilon. nil on
|
|
22
20
|
# either side skips the check (see #diff).
|
|
23
|
-
# regressed
|
|
21
|
+
# regressed - any new survivors OR a score drop.
|
|
24
22
|
Delta = Data.define(:new_survivors, :fixed_survivors,
|
|
25
23
|
:score_before, :score_after, :score_drop, :regressed)
|
|
26
24
|
|
|
27
|
-
# Load a prior --format json run. Raises ConfigError (NOT exit
|
|
28
|
-
#
|
|
29
|
-
#
|
|
25
|
+
# Load a prior --format json run. Raises ConfigError (NOT exit: a data class
|
|
26
|
+
# must never kill the host) on a missing/unreadable file, unparseable JSON,
|
|
27
|
+
# or a doc that is not a baseline shape, so the CLI maps it to exit 2
|
|
30
28
|
# (usage) like every other bad-path flag.
|
|
31
29
|
#
|
|
32
30
|
# @param path [String] baseline JSON file path.
|
|
@@ -72,7 +70,7 @@ module Mutineer
|
|
|
72
70
|
|
|
73
71
|
current_score = aggregate.mutation_score
|
|
74
72
|
# nil-score discipline (mirrors Reporter#exit_code): a score absent on
|
|
75
|
-
# either side
|
|
73
|
+
# either side cannot be compared. Skip the drop check, keep the new-
|
|
76
74
|
# survivor check.
|
|
77
75
|
score_drop = !@score.nil? && !current_score.nil? &&
|
|
78
76
|
current_score < @score - epsilon
|
data/lib/mutineer/cli.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Mutineer
|
|
|
18
18
|
# Command-line entry point. `start` is the single public method called by
|
|
19
19
|
# bin/mutineer; it parses argv, acts, and exits with a pinned code.
|
|
20
20
|
#
|
|
21
|
-
# Exit codes
|
|
21
|
+
# Exit codes:
|
|
22
22
|
# 0 success / requested output (--version, --help, score >= threshold)
|
|
23
23
|
# 1 survivors below threshold, or a runtime error
|
|
24
24
|
# 2 usage / flag error (unknown subcommand, invalid flag, unknown operator,
|
|
@@ -78,7 +78,7 @@ module Mutineer
|
|
|
78
78
|
# @return [void]
|
|
79
79
|
def self.start(argv)
|
|
80
80
|
opts = {} # symbol => value, the CLI-provided Config fields
|
|
81
|
-
explicit = Set.new # precedence keys the user typed
|
|
81
|
+
explicit = Set.new # precedence keys the user typed
|
|
82
82
|
show_operators = false
|
|
83
83
|
|
|
84
84
|
parser = OptionParser.new do |o|
|
|
@@ -116,15 +116,15 @@ module Mutineer
|
|
|
116
116
|
o.on("--debug") { opts[:verbose] = true } # alias of --verbose
|
|
117
117
|
o.on("--format FORMAT") { |v| opts[:format] = v }
|
|
118
118
|
o.on("--output FILE") { |v| opts[:output] = v }
|
|
119
|
-
#
|
|
120
|
-
#
|
|
119
|
+
# --baseline is also a .mutineer.yml key, so mark it explicit when typed
|
|
120
|
+
# (CLI wins over the file). --baseline-epsilon is CLI-only.
|
|
121
121
|
o.on("--baseline FILE") { |v| opts[:baseline] = v; explicit << :baseline }
|
|
122
122
|
o.on("--baseline-epsilon FLOAT") { |v| opts[:baseline_epsilon] = v.to_f }
|
|
123
|
-
#
|
|
123
|
+
# Run the target suite as a subprocess in the app's OWN runtime so
|
|
124
124
|
# mutineer (Ruby >= 3.4) can mutation-test apps pinned to an older Ruby.
|
|
125
125
|
o.on("--test-command CMD") { |v| opts[:test_command] = v; explicit << :test_command }
|
|
126
|
-
#
|
|
127
|
-
#
|
|
126
|
+
# Boot the app ONCE in a persistent daemon and fork per mutant, with
|
|
127
|
+
# per-worker DB isolation so --jobs N is safe under Rails.
|
|
128
128
|
o.on("--daemon") { opts[:daemon] = true; explicit << :daemon }
|
|
129
129
|
end
|
|
130
130
|
|
|
@@ -150,7 +150,7 @@ module Mutineer
|
|
|
150
150
|
file_hash = file_path ? Config.from_file(file_path) : {}
|
|
151
151
|
config = Config.resolve(opts, file_hash, explicit)
|
|
152
152
|
rescue Mutineer::ConfigError => e
|
|
153
|
-
#
|
|
153
|
+
# The lib layer raises instead of killing the host; the CLI maps a
|
|
154
154
|
# config (usage) error to exit 2.
|
|
155
155
|
warn "mutineer: #{e.message}"
|
|
156
156
|
exit 2
|
|
@@ -158,7 +158,7 @@ module Mutineer
|
|
|
158
158
|
|
|
159
159
|
case argv.first
|
|
160
160
|
when "run"
|
|
161
|
-
#
|
|
161
|
+
# A directory source expands to its **/*.rb files; literal files pass
|
|
162
162
|
# through. Test inference (when --test is omitted) happens in validate!.
|
|
163
163
|
config.sources = Pairing.expand_sources(argv[1..], project_root: config.project_root)
|
|
164
164
|
run(config, explicit)
|
|
@@ -197,7 +197,7 @@ module Mutineer
|
|
|
197
197
|
warn "mutineer: #{e.message}"
|
|
198
198
|
exit 2
|
|
199
199
|
rescue SystemCallError => e
|
|
200
|
-
#
|
|
200
|
+
# A missing/unreadable path reaches here as Errno::ENOENT etc. A plain
|
|
201
201
|
# message and usage exit, never a raw backtrace.
|
|
202
202
|
warn "mutineer: #{e.message}"
|
|
203
203
|
exit 2
|
|
@@ -210,15 +210,19 @@ module Mutineer
|
|
|
210
210
|
warn "mutineer: error reading: #{e.message}"
|
|
211
211
|
exit 1
|
|
212
212
|
rescue Mutineer::SmokeCheckError => e
|
|
213
|
-
#
|
|
213
|
+
# The unmutated suite is not green under --test-command: a broken
|
|
214
214
|
# environment, not weak tests. Runtime error (exit 1), not usage (exit 2).
|
|
215
215
|
warn "mutineer: #{e.message}"
|
|
216
216
|
exit 1
|
|
217
|
+
rescue Mutineer::DaemonBootError => e
|
|
218
|
+
# The daemon is gone for good, so the run ended rather than scoring the rest
|
|
219
|
+
# against it. A deliberate stop deserves a message, not a raw backtrace.
|
|
220
|
+
warn "mutineer: #{e.message}"
|
|
221
|
+
exit 1
|
|
217
222
|
end
|
|
218
223
|
|
|
219
|
-
# Flag validation: every flag/usage failure exits 2
|
|
220
|
-
# taxonomy above
|
|
221
|
-
# Validates the run configuration.
|
|
224
|
+
# Flag validation: every flag/usage failure exits 2, consistent with the
|
|
225
|
+
# taxonomy above. CI can tell "mistyped flag" from "tests too weak."
|
|
222
226
|
#
|
|
223
227
|
# @api private
|
|
224
228
|
# @param config [Mutineer::Config] run configuration.
|
|
@@ -279,13 +283,12 @@ module Mutineer
|
|
|
279
283
|
validate_paths!(config)
|
|
280
284
|
end
|
|
281
285
|
|
|
282
|
-
#
|
|
283
|
-
#
|
|
286
|
+
# --test-command runs the target suite in the app's own runtime. Validate its
|
|
287
|
+
# shape up front (usage errors → exit 2) and force serial execution: each
|
|
284
288
|
# subprocess boots the app and opens its own fixture transaction against the
|
|
285
|
-
# same DB, so --jobs > 1 would corrupt results (
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
# Validates the --test-command configuration.
|
|
289
|
+
# same DB, so --jobs > 1 would corrupt results (fixture-contention hazard).
|
|
290
|
+
# Unlike --rails, this path has NO per-worker DB isolation to opt into, so an
|
|
291
|
+
# explicit --jobs N is forced to 1 rather than honored.
|
|
289
292
|
#
|
|
290
293
|
# @api private
|
|
291
294
|
# @param config [Mutineer::Config] run configuration.
|
|
@@ -347,7 +350,6 @@ module Mutineer
|
|
|
347
350
|
|
|
348
351
|
# --since needs a real git repo and a resolvable ref; either failure is a
|
|
349
352
|
# usage error (exit 2) so CI sees "bad invocation," not "tests too weak."
|
|
350
|
-
# Validates the --since ref.
|
|
351
353
|
#
|
|
352
354
|
# @api private
|
|
353
355
|
# @param config [Mutineer::Config] run configuration.
|
|
@@ -370,10 +372,9 @@ module Mutineer
|
|
|
370
372
|
exit 2
|
|
371
373
|
end
|
|
372
374
|
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
376
|
-
# Validates source and test paths.
|
|
375
|
+
# Validate path existence up front so a typo is a clean usage error (exit 2),
|
|
376
|
+
# not an Errno::ENOENT backtrace from deep in the run. Flag checks run first
|
|
377
|
+
# so a bad flag still reports the flag, not the missing file.
|
|
377
378
|
#
|
|
378
379
|
# @api private
|
|
379
380
|
# @param config [Mutineer::Config] run configuration.
|
|
@@ -387,14 +388,13 @@ module Mutineer
|
|
|
387
388
|
exit 2
|
|
388
389
|
end
|
|
389
390
|
|
|
390
|
-
#
|
|
391
|
-
# (explicit --test wins
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
391
|
+
# Auto-pair sources to tests by path convention when no --test was given
|
|
392
|
+
# (explicit --test wins). Each source with an inferred test on disk joins the
|
|
393
|
+
# run; a source with none is dropped with a one-line stderr warning and the
|
|
394
|
+
# run continues with the rest. If every source is dropped: in boot mode the
|
|
395
|
+
# dedicated --boot/--rails-requires-test check reports it; otherwise exit 2
|
|
395
396
|
# with a usage message. The framework is re-detected from the inferred set
|
|
396
397
|
# unless it was set explicitly (a spec-only project loads/reports as rspec).
|
|
397
|
-
# Auto-pairs sources and tests when --test is absent.
|
|
398
398
|
#
|
|
399
399
|
# @api private
|
|
400
400
|
# @param config [Mutineer::Config] run configuration.
|
|
@@ -421,11 +421,10 @@ module Mutineer
|
|
|
421
421
|
exit 2
|
|
422
422
|
end
|
|
423
423
|
|
|
424
|
-
#
|
|
424
|
+
# A missing/unreadable/unparseable baseline is a usage error (exit 2),
|
|
425
425
|
# mirroring --output/--since preflight, so CI sees "bad invocation," not a
|
|
426
426
|
# backtrace mid-run. Validating up front = attempting the load (it raises
|
|
427
427
|
# ConfigError/SystemCallError; the actual diff reloads in execute).
|
|
428
|
-
# Preflights a baseline file.
|
|
429
428
|
#
|
|
430
429
|
# @api private
|
|
431
430
|
# @param path [String] baseline file path.
|
|
@@ -464,7 +463,7 @@ module Mutineer
|
|
|
464
463
|
aggregate, source_map = Runner.execute(config)
|
|
465
464
|
reporter = Reporter.new(aggregate, source_map)
|
|
466
465
|
|
|
467
|
-
#
|
|
466
|
+
# Diff the current run against the baseline (preflighted above) by the
|
|
468
467
|
# stable survivor id. The delta is rendered inline (human section / additive
|
|
469
468
|
# json block) and gates exit independently of --threshold.
|
|
470
469
|
delta = (Baseline.load(config.baseline).diff(aggregate, epsilon: config.baseline_epsilon) if config.baseline)
|
|
@@ -476,28 +475,27 @@ module Mutineer
|
|
|
476
475
|
# is not comparable to an in-process run: no coverage narrowing (uncovered
|
|
477
476
|
# mutants count as survivors), and an infra failure is scored as a kill
|
|
478
477
|
# (upper bound). Daemon coverage fallback warnings are emitted from the runner
|
|
479
|
-
# only when the map is unavailable
|
|
478
|
+
# only when the map is unavailable, not on every --daemon run.
|
|
480
479
|
if config.test_command
|
|
481
480
|
warn "[mutineer] --test-command score is an upper bound, not comparable to an " \
|
|
482
481
|
"in-process run: no coverage narrowing (uncovered mutants count as survivors) " \
|
|
483
482
|
"and an infra failure is scored as a kill."
|
|
484
483
|
end
|
|
485
484
|
|
|
486
|
-
# Nudge toward the opt-in tier-2 operators (human report only
|
|
485
|
+
# Nudge toward the opt-in tier-2 operators (human report only: never
|
|
487
486
|
# pollute JSON output).
|
|
488
487
|
if !%w[json html].include?(config.format) && (hint = tier2_hint(config.operators))
|
|
489
488
|
puts hint
|
|
490
489
|
end
|
|
491
490
|
|
|
492
|
-
#
|
|
491
|
+
# --baseline and --threshold are independent gates OR'd together.
|
|
493
492
|
# `max` of two 0/1 codes is the OR; usage (2) is handled earlier and wins.
|
|
494
493
|
baseline_exit = delta&.regressed ? 1 : 0
|
|
495
494
|
exit [reporter.exit_code(threshold: config.threshold), baseline_exit].max
|
|
496
495
|
end
|
|
497
496
|
|
|
498
497
|
# The tier-2 operators not in the active set, as a one-line hint (or nil when
|
|
499
|
-
# they
|
|
500
|
-
# Builds a Tier-2 operator hint.
|
|
498
|
+
# they are all already enabled). `active` nil means the default (Tier-1) set.
|
|
501
499
|
#
|
|
502
500
|
# @param active [Array<String>, nil] active operator names.
|
|
503
501
|
# @return [String, nil] hint text or nil.
|
data/lib/mutineer/config.rb
CHANGED
|
@@ -4,20 +4,20 @@ require "etc"
|
|
|
4
4
|
require "yaml"
|
|
5
5
|
|
|
6
6
|
module Mutineer
|
|
7
|
-
# Raised by the config layer instead of calling exit/abort
|
|
8
|
-
# never kill the host process
|
|
7
|
+
# Raised by the config layer instead of calling exit/abort. A data class must
|
|
8
|
+
# never kill the host process. The CLI rescues this and maps it to exit 2.
|
|
9
9
|
class ConfigError < StandardError; end
|
|
10
10
|
|
|
11
11
|
# Plain run configuration, populated by the CLI (or directly by the
|
|
12
12
|
# integration test). `operators` nil means "all default operators";
|
|
13
13
|
# `threshold` 0.0 means the CI gate is off (spec §10).
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
# strategy (reload|redefine), require_paths (extra files to load).
|
|
17
|
-
# the CLI > file > default precedence merge live here
|
|
15
|
+
# Also holds: jobs (parallel workers), format (human|json), output (report
|
|
16
|
+
# file), strategy (reload|redefine), require_paths (extra files to load).
|
|
17
|
+
# Config loading and the CLI > file > default precedence merge live here.
|
|
18
18
|
#
|
|
19
|
-
# Boot mode adds: boot (a file to require ONCE in the parent so the app env
|
|
20
|
-
# e.g. Rails
|
|
19
|
+
# Boot mode adds: boot (a file to require ONCE in the parent so the app env,
|
|
20
|
+
# e.g. Rails, is booted before forking; sources are then NOT manually required)
|
|
21
21
|
# and rails (sugar: defaults boot to config/environment and strategy to redefine,
|
|
22
22
|
# and reconnects ActiveRecord per fork).
|
|
23
23
|
Config = Struct.new(
|
|
@@ -25,7 +25,7 @@ module Mutineer
|
|
|
25
25
|
:cache_dir, :project_root, :load_paths,
|
|
26
26
|
:jobs, :format, :output, :strategy, :require_paths,
|
|
27
27
|
:boot, :rails, :since, :framework, :verbose, :ignore,
|
|
28
|
-
# :daemon is user-facing
|
|
28
|
+
# :daemon is user-facing (--daemon flag + KNOWN_KEYS + boolean coerce).
|
|
29
29
|
# :daemon_timeout stays programmatic (set by tests/Runner; no flag yet).
|
|
30
30
|
:baseline, :baseline_epsilon, :fail_fast, :test_command,
|
|
31
31
|
:daemon, :daemon_timeout,
|
|
@@ -33,7 +33,7 @@ module Mutineer
|
|
|
33
33
|
) do
|
|
34
34
|
# Config file name.
|
|
35
35
|
CONFIG_FILE = ".mutineer.yml"
|
|
36
|
-
# Keys accepted in .mutineer.yml
|
|
36
|
+
# Keys accepted in .mutineer.yml. `require` maps to the :require_paths field.
|
|
37
37
|
KNOWN_KEYS = %w[operators jobs threshold only require boot rails since framework verbose ignore baseline fail_fast test_command daemon].freeze
|
|
38
38
|
|
|
39
39
|
def initialize(**kwargs)
|
|
@@ -59,8 +59,8 @@ module Mutineer
|
|
|
59
59
|
|
|
60
60
|
# Walk from `start` toward `home`, returning the first .mutineer.yml path found
|
|
61
61
|
# or nil. Checks `home` itself, then stops; if `start` is above `home`
|
|
62
|
-
# (e.g. /tmp), the walk continues to the filesystem root
|
|
63
|
-
#
|
|
62
|
+
# (e.g. /tmp), the walk continues to the filesystem root. Pure discovery;
|
|
63
|
+
# reads no file content.
|
|
64
64
|
def self.find_file(start = Dir.pwd, home = File.expand_path("~"))
|
|
65
65
|
dir = File.expand_path(start)
|
|
66
66
|
loop do
|
|
@@ -77,9 +77,9 @@ module Mutineer
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# Parse a .mutineer.yml into a symbol-keyed hash of recognized keys. Unknown
|
|
80
|
-
# keys / unknown operator names emit a one-line stderr warning and are ignored
|
|
81
|
-
#
|
|
82
|
-
#
|
|
80
|
+
# keys / unknown operator names emit a one-line stderr warning and are ignored.
|
|
81
|
+
# A YAML syntax error raises ConfigError: never a silent fallback to defaults,
|
|
82
|
+
# and never an exit from the lib layer.
|
|
83
83
|
def self.from_file(path)
|
|
84
84
|
raw = YAML.safe_load(File.read(path)) || {}
|
|
85
85
|
name = File.basename(path)
|
|
@@ -103,7 +103,7 @@ module Mutineer
|
|
|
103
103
|
raise ConfigError, "#{File.basename(path)} parse error: #{e.message}"
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
# Apply precedence
|
|
106
|
+
# Apply precedence: start from the CLI-provided values, then fill in a
|
|
107
107
|
# config-file value only for keys the user did NOT type on the command line.
|
|
108
108
|
# `explicit` is a Set of field symbols the CLI saw with a value; a Set (not
|
|
109
109
|
# nil-sentinels) is used because some valid values are zero/false.
|
|
@@ -114,7 +114,7 @@ module Mutineer
|
|
|
114
114
|
|
|
115
115
|
# --rails sugar: boot config/environment. Prefer redefine only for the
|
|
116
116
|
# in-process path (daemon is whole-file reload only). In-process --rails
|
|
117
|
-
# shares one test database
|
|
117
|
+
# shares one test database, so force serial unless --daemon.
|
|
118
118
|
if config.rails
|
|
119
119
|
config.boot ||= "config/environment"
|
|
120
120
|
unless config.daemon || explicit.include?(:strategy)
|
|
@@ -138,7 +138,6 @@ module Mutineer
|
|
|
138
138
|
|
|
139
139
|
# Pick rspec when a MAJORITY of the given test files end with _spec.rb;
|
|
140
140
|
# otherwise minitest. Empty/ambiguous -> minitest (the safe default).
|
|
141
|
-
# Detects the test framework from the file list.
|
|
142
141
|
#
|
|
143
142
|
# @param tests [Array<String>] test file paths.
|
|
144
143
|
# @return [String] `"rspec"` or `"minitest"`.
|
|
@@ -187,10 +186,9 @@ module Mutineer
|
|
|
187
186
|
end
|
|
188
187
|
end
|
|
189
188
|
|
|
190
|
-
# Drop (with a warning) operator names the registry
|
|
189
|
+
# Drop (with a warning) operator names the registry does not know.
|
|
191
190
|
# Referenced lazily so config.rb carries no load-order dependency on the
|
|
192
191
|
# registry; by the time a config is parsed at runtime, it is loaded.
|
|
193
|
-
# Filters out unknown operator names.
|
|
194
192
|
#
|
|
195
193
|
# @api private
|
|
196
194
|
# @param names [Array<String>] operator names.
|