mutineer 1.6.0 → 1.7.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 +248 -0
- data/README.md +324 -23
- data/lib/mutineer/child_coverage.rb +53 -0
- data/lib/mutineer/cli.rb +182 -23
- data/lib/mutineer/concern_blocks.rb +80 -0
- data/lib/mutineer/config.rb +148 -8
- data/lib/mutineer/coverage_map.rb +175 -12
- data/lib/mutineer/daemon_backend.rb +182 -56
- data/lib/mutineer/daemon_client.rb +30 -0
- data/lib/mutineer/daemon_server.rb +157 -25
- data/lib/mutineer/external_backend.rb +12 -1
- data/lib/mutineer/init.rb +124 -0
- data/lib/mutineer/isolation.rb +14 -2
- data/lib/mutineer/job_plan.rb +129 -23
- data/lib/mutineer/migrate.rb +705 -0
- data/lib/mutineer/mutator_registry.rb +6 -2
- data/lib/mutineer/mutators/base.rb +14 -7
- data/lib/mutineer/mutators/call_to_receiver.rb +135 -0
- data/lib/mutineer/mutators/chain_link.rb +1 -1
- data/lib/mutineer/orphan_guard.rb +72 -0
- data/lib/mutineer/project.rb +80 -6
- data/lib/mutineer/rails_run_env.rb +73 -0
- data/lib/mutineer/rails_worker_db.rb +1408 -53
- data/lib/mutineer/redaction.rb +98 -0
- data/lib/mutineer/reporter.rb +66 -12
- data/lib/mutineer/result.rb +22 -13
- data/lib/mutineer/runner.rb +24 -12
- data/lib/mutineer/statement_lines.rb +15 -8
- data/lib/mutineer/subject.rb +22 -4
- data/lib/mutineer/test_runners/rspec.rb +15 -3
- data/lib/mutineer/test_runners.rb +3 -2
- data/lib/mutineer/triage.rb +111 -0
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer.rb +4 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2afb9e81b16cbdf3b7ecd0d074365ee05eb58bdb0045ff6a73643a68893a4b1a
|
|
4
|
+
data.tar.gz: 949369679de39fbc53025fc470896bbef0c5310fc59c11cb928ad80aa748cc22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d78d0145b257b25321b7a9f05ebebc51e1818459eacecd26917953ef2dd0296af34fcb755a3a782102ef5453a9f27d90cb3fb2d8261fd8760ab557f273b2d866
|
|
7
|
+
data.tar.gz: 3d844218d9e05bc1c09bc7c1bfa7617c7fa79b49c943bb1a96af20ab68fa85312baf08ed9fa37d4f9929bc07c027c943717e2ce4344e15639509feeae66ff560
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,253 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.7.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Call-to-receiver operator** (Tier-2, opt-in via `--operators`):
|
|
14
|
+
`call_to_receiver` replaces the last call of a chain with its receiver, with
|
|
15
|
+
the arguments and block of that call (`user.full_name` -> `user`). The mutant
|
|
16
|
+
survives when no test checks what that last call does. It does not overlap
|
|
17
|
+
with `chain_link`: `chain_link` drops a call in the middle of a chain, and
|
|
18
|
+
`call_to_receiver` drops only the last call. The two give the same source
|
|
19
|
+
only when the last call repeats the call before it (`node.name.name`). It
|
|
20
|
+
skips the same names as `chain_link`, and it skips operator calls and
|
|
21
|
+
setters.
|
|
22
|
+
- **Reasons on ignored mutants.** Text after `--` on
|
|
23
|
+
`# mutineer:disable-line`, and a `.mutineer.yml` `ignore:` mapping
|
|
24
|
+
(`id` plus `reason`), is stored on the ignored mutant. The JSON report
|
|
25
|
+
(`schema_version` 1.8) and the HTML report show it. A mapping with no
|
|
26
|
+
`id`, or with an unknown key, warns. Mutineer 1.7 is the first version
|
|
27
|
+
that reads a mapping. An older version matches no mutant to a mapping
|
|
28
|
+
and does not warn, so the mutant it names is not ignored. Upgrade
|
|
29
|
+
every gate that reads the file before you add a mapping.
|
|
30
|
+
`mutineer migrate` rewrites the old id inside a mapping and keeps its
|
|
31
|
+
reason.
|
|
32
|
+
- **`mutineer triage`.** It prints `ignore:` mapping entries for survivors
|
|
33
|
+
in a JSON report. `--reason` is required. Pass `--all` or `--id`. It
|
|
34
|
+
does not edit files.
|
|
35
|
+
- **Equivalent-skip page.** The docs list every built-in chain-link skip
|
|
36
|
+
and the endless-range skip, and why each one is equivalent.
|
|
37
|
+
- **Postgres worker databases for `--rails --daemon`.** Each worker gets
|
|
38
|
+
`<base>-mutineer-<N>`, copied from the test database before mutants run.
|
|
39
|
+
A second run cannot drop a database another live run holds. An app with
|
|
40
|
+
more than one database config runs one worker and says so once.
|
|
41
|
+
The worker databases stay after the run.
|
|
42
|
+
- **MySQL 8 worker databases for Minitest on `--rails --daemon`.** Each worker gets
|
|
43
|
+
`<base>-mutineer-<N>` on `mysql2` or `trilogy`, with the base shortened to fit
|
|
44
|
+
MySQL's 64-byte name limit. Mutineer creates the
|
|
45
|
+
database, loads `schema.rb` or `structure.sql`, and copies rows once at
|
|
46
|
+
the start. Triggers from the schema do not fire on the copied rows.
|
|
47
|
+
Mutineer creates them again after the copy.
|
|
48
|
+
A second run cannot drop a database another live run holds.
|
|
49
|
+
That work uses the TLS and timeout settings from the database config.
|
|
50
|
+
MariaDB is not tested. MySQL 5.7 is not supported. The worker databases
|
|
51
|
+
stay after the run.
|
|
52
|
+
- **`mutineer migrate`.** It rewrites old-format ids in `.mutineer.yml`
|
|
53
|
+
`ignore:` to the current ids. It keeps comments and every other line.
|
|
54
|
+
One old id that matches several mutants becomes every new id. An id that
|
|
55
|
+
matches nothing stays, and the command exits 1. A block scalar under
|
|
56
|
+
`ignore:` is not supported: the command exits 2 and does not change the
|
|
57
|
+
file. `--dry-run` prints each change and writes nothing. It does not
|
|
58
|
+
rewrite a baseline. Generate a new baseline with `--format json`. An
|
|
59
|
+
old-id warning names `mutineer migrate` and says that this becomes an
|
|
60
|
+
error in Mutineer 2.0. The baseline warning says to generate a new
|
|
61
|
+
baseline with `--format json`.
|
|
62
|
+
- **`mutineer init`.** It writes a commented `.mutineer.yml` with the default
|
|
63
|
+
operators. `since` stays commented, so the first run is a full scan.
|
|
64
|
+
`init --rails` also sets `rails: true` and prints the first run command.
|
|
65
|
+
An existing file is kept unless you pass `--force`.
|
|
66
|
+
- **Rails rollout guide.** The page walks from one model, to a report-only CI
|
|
67
|
+
job, to a `--since` and `--baseline` pull request gate, to `--threshold`
|
|
68
|
+
on chosen folders. It has a GitHub Actions recipe and a GitLab CI recipe.
|
|
69
|
+
- **A mutation-testing primer in the README.** "What is mutation testing?"
|
|
70
|
+
walks through one small example, from a surviving mutant to the test that
|
|
71
|
+
kills it, and says what each report row means and what to do with a
|
|
72
|
+
survivor. The usage block lists all four commands.
|
|
73
|
+
- **A production-ready section and a stability contract.** The README opens
|
|
74
|
+
with a support matrix for Ruby, database, framework, and parallelism.
|
|
75
|
+
`STABILITY.md` lists what a release keeps and what 2.0 will change.
|
|
76
|
+
`SECURITY.md`, `CONTRIBUTING.md`, and GitHub issue forms are new.
|
|
77
|
+
The docs home page takes the same section from the README at site build.
|
|
78
|
+
- **`--allow-empty` and `allow_empty:`.** A run with no mutants now warns
|
|
79
|
+
that Mutineer 2.0 will fail it, unless `--allow-empty` marks it as expected.
|
|
80
|
+
A `--since` run whose changes hold no mutants does not warn: it says the
|
|
81
|
+
changes hold nothing to test. A `--since` run with no mutants even before
|
|
82
|
+
scoping still warns. The exit code does not change in 1.x.
|
|
83
|
+
- **`--no-daemon`.** It beats a `.mutineer.yml` `daemon: true` for one run,
|
|
84
|
+
like `--no-fail-fast`. A Minitest `--rails` run without a `daemon` value now
|
|
85
|
+
prints a notice that 2.0 uses `--daemon` by default for `--rails`;
|
|
86
|
+
`--no-daemon` (or `daemon: false`) keeps the run serial and stops the
|
|
87
|
+
notice.
|
|
88
|
+
- **"Did you mean" hints.** Unknown names in `--operators`, in
|
|
89
|
+
`.mutineer.yml` `operators:` and keys, and in `# mutineer:disable-line`
|
|
90
|
+
markers get the closest valid name. The `.mutineer.yml` warnings also say
|
|
91
|
+
that 2.0 makes them errors.
|
|
92
|
+
- **`--coverage-from FILE` lets parallel shards share one coverage capture.**
|
|
93
|
+
A `--rails`/`--boot` run reads the `coverage.json` of an earlier run with
|
|
94
|
+
the same tests over these sources or more. It skips the capture. Each test
|
|
95
|
+
that covers one of its sources still runs alone, as on a cache hit, because
|
|
96
|
+
a mutant runs only those tests. A test that covers none of them does not,
|
|
97
|
+
so a shard runs far fewer tests alone than a full suite. All tests then run
|
|
98
|
+
once together, and the run saves its own cache.
|
|
99
|
+
Boot-mode caches now also record each source's fingerprint, the ownership
|
|
100
|
+
list and a digest of the other inputs. A capture that does not fit is an
|
|
101
|
+
error (exit 2), never a silent recapture. So is one where loading its other
|
|
102
|
+
sources ran these sources' code, which happens under lazy loading with
|
|
103
|
+
`--strategy redefine`. Without boot mode, and with `--daemon`,
|
|
104
|
+
`--test-command` or `--dry-run`, the flag warns that it has no effect.
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
|
|
108
|
+
- **Rails test workers stay off during a mutant run.** Mutineer sets
|
|
109
|
+
`PARALLEL_WORKERS` to `1` for in-process, `--daemon`, and `--test-command`
|
|
110
|
+
runs. A value you already set is replaced, and Mutineer prints one line.
|
|
111
|
+
The coverage cache stores the `PARALLEL_WORKERS` value, so a cache from an
|
|
112
|
+
older version rebuilds once. `--test-command` also sets `DISABLE_SPRING=1`.
|
|
113
|
+
The README explains `parallelize`, Bootsnap, and Redis after a fork.
|
|
114
|
+
- **`--test-command` exit codes 126 and 127 are scored `error`, not
|
|
115
|
+
`killed`.** A wrapper that could not start the suite ran no test. Under
|
|
116
|
+
`--threshold`, such a run can now score lower or fail the no-verdict limit,
|
|
117
|
+
where before it passed on false kills.
|
|
118
|
+
- **A run with errored mutants and no `--threshold` says so.** Errors are not
|
|
119
|
+
in the score, so they do not fail the run; the message says what
|
|
120
|
+
`--threshold` would enforce.
|
|
121
|
+
- **`--since` also narrows suppressed mutants.** A scoped report lists only
|
|
122
|
+
`ignored` mutants on changed lines, as `--dry-run --since` already did.
|
|
123
|
+
- **The empty-run message moved from the human report to one stderr warning
|
|
124
|
+
for every format.** "No mutations generated" is gone. An empty `--since`
|
|
125
|
+
run prints "nothing to test in the changes since REF" instead.
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
- **Daemon errors no longer show the database password.** Rails parses
|
|
130
|
+
`DATABASE_URL` itself, and a malformed URL makes it raise an error that
|
|
131
|
+
quotes the whole URL. The daemon sent that error to the terminal when
|
|
132
|
+
the app failed to boot, and the same could happen when it set up worker
|
|
133
|
+
databases or the coverage map, and in a `--verbose` coverage warning.
|
|
134
|
+
Mutineer now replaces the password in any URL, and the `DATABASE_URL`
|
|
135
|
+
password anywhere else (escaped or cut short included), with
|
|
136
|
+
`[REDACTED]` before it reports the error.
|
|
137
|
+
- **`--strategy reload` runs concern files with an `included` or `prepended`
|
|
138
|
+
block (#256).** Every mutant in such a file was `errored`: ActiveSupport
|
|
139
|
+
raised `MultipleIncludedBlocks` (or `MultiplePrependBlocks`) because the
|
|
140
|
+
mutant loads from a temp file with another path. Before each reload mutant
|
|
141
|
+
loads, in-process and on the daemon, the forked child now forgets the blocks
|
|
142
|
+
that the source file stored, so methods in the concern body and in
|
|
143
|
+
`class_methods` are killed or survive as usual. A block the mutant's load
|
|
144
|
+
does not store again (a concern under an `unless defined?` guard) is put
|
|
145
|
+
back. A method inside an
|
|
146
|
+
`included` or `prepended` block is `unplaceable` under reload too: the block
|
|
147
|
+
does not run again in a class that already includes or prepends the
|
|
148
|
+
concern, so the mutant would falsely survive. That includes a method of a
|
|
149
|
+
class defined inside the block. Use `--test-command` to run those mutants.
|
|
150
|
+
- **No heredoc opener without its body.** A mutation that kept a heredoc
|
|
151
|
+
opener but replaced the lines of its body gave source that does not parse,
|
|
152
|
+
such as `operand_removal` on `foo(<<~EOS) ||` with the body and `bar` on
|
|
153
|
+
the next lines. The mutant showed only as `skipped (invalid)`. Now the
|
|
154
|
+
operator does not emit it.
|
|
155
|
+
- **An RSpec run that fails only on an error outside examples scores the
|
|
156
|
+
mutant `errored`, not `killed`.** A spec file that raised while loading, or
|
|
157
|
+
a failing `before(:suite)` hook, made RSpec exit 1, and every mutant was
|
|
158
|
+
scored `killed`. On a Rails app, `rails_helper` called `SimpleCov.start`
|
|
159
|
+
after `--rails` had suspended Coverage (#228), so each spec file raised as
|
|
160
|
+
it loaded, and 18 of 18 mutants were scored `killed`; 3 of them survive.
|
|
161
|
+
Now the runner returns 2 when RSpec recorded an error outside examples and
|
|
162
|
+
no example failed, so the mutant is `errored`: out of the score and in
|
|
163
|
+
`no_verdict`, so that run now fails `--threshold` instead of passing at
|
|
164
|
+
100%. A failed example still kills.
|
|
165
|
+
Minitest and `--daemon` already score a test file that raises at load as
|
|
166
|
+
`errored`. `--test-command` sees only the exit status and still scores it
|
|
167
|
+
`killed`.
|
|
168
|
+
- **A Rails daemon boots again while a mutant file is still open.**
|
|
169
|
+
The file stays when another process holds it. The next boot skips
|
|
170
|
+
that file name instead of stopping.
|
|
171
|
+
- **A daemon crash no longer leaves a mutant file in your source folder
|
|
172
|
+
(#250).** When a daemon dies during a mutant, its child dies a moment
|
|
173
|
+
later. The new daemon cleaned up before that, so the child's file stayed.
|
|
174
|
+
The next Rails boot then failed on the file name. Each daemon run now
|
|
175
|
+
cleans up again when it ends or stops early. It waits up to 1.5 seconds
|
|
176
|
+
for the child to go. A file whose owner still runs after that stays
|
|
177
|
+
until the next daemon run removes it. On Linux, a file whose owner is a
|
|
178
|
+
zombie (in a container whose pid 1 does not reap it) is now removed.
|
|
179
|
+
With `--jobs N`, an error in one worker stops the others after their
|
|
180
|
+
current mutant, and a Ctrl-C quits every worker daemon before the run
|
|
181
|
+
ends.
|
|
182
|
+
- **SimpleCov in a test helper no longer breaks every mutant under
|
|
183
|
+
`--rails` or `--boot` (#248).** After the coverage map is built, mutineer
|
|
184
|
+
pauses Ruby's coverage (#228). SimpleCov saw the paused coverage as off and
|
|
185
|
+
tried to start it. Ruby refuses that while coverage is paused, so the test
|
|
186
|
+
helper raised in every mutant. Now a start in a mutant first stops the
|
|
187
|
+
paused coverage. A mutant whose tests start nothing keeps it paused and pays
|
|
188
|
+
nothing. This applies to `--daemon` too.
|
|
189
|
+
- **Methods in an ActiveSupport::Concern `class_methods` block are mutated
|
|
190
|
+
under `--strategy redefine` (#252).** ActiveSupport puts these methods in
|
|
191
|
+
the concern's `ClassMethods` module, and each includer gets them as class
|
|
192
|
+
methods. Mutineer named them as instance methods of the concern itself.
|
|
193
|
+
Redefine (the `--rails` default) then loaded each mutant onto the concern.
|
|
194
|
+
The tests called the includer's method, so every mutant falsely survived.
|
|
195
|
+
A concern with an instance method of the same name failed the other way:
|
|
196
|
+
the mutant replaced that instance method, so tests calling it ran the
|
|
197
|
+
wrong method and failed (an ArgumentError, when the arities differ), and
|
|
198
|
+
the mutant counted as killed whatever it changed. Both false survivors
|
|
199
|
+
and false kills are gone. An earlier redefine score is unreliable for
|
|
200
|
+
any file with a `class_methods` block: too low from false survivors, or
|
|
201
|
+
too high from false kills. Re-run those files.
|
|
202
|
+
These methods are now named `Concern::ClassMethods#name`, and redefine
|
|
203
|
+
loads the mutant onto that module. This applies to a module that ran
|
|
204
|
+
`extend ActiveSupport::Concern` earlier in the same file. The mutant ids of
|
|
205
|
+
these methods change, because an id contains the qualified name (owner and
|
|
206
|
+
method). An instance
|
|
207
|
+
method with the same name, later in the same file, can get a new id too.
|
|
208
|
+
An `ignore:` entry or a `--baseline` survivor for one of these methods no
|
|
209
|
+
longer matches. Replace it with the new id from a new run.
|
|
210
|
+
- **Methods in other `class_methods`, `included` and `prepended` blocks are
|
|
211
|
+
`unplaceable` under `--strategy redefine` (#252).** ActiveSupport runs an
|
|
212
|
+
`included` or `prepended` block in each includer, so no constant names the
|
|
213
|
+
class that owns its methods. A `class_methods` block that Mutineer cannot
|
|
214
|
+
tie to a concern has the same problem: one in a def, in Rails'
|
|
215
|
+
`concerning`, in an unassigned `Module.new`, or in a module that does not
|
|
216
|
+
run `extend ActiveSupport::Concern` earlier in the file. Redefine loaded each
|
|
217
|
+
mutant onto the enclosing module, and the mutant could falsely survive. Now
|
|
218
|
+
redefine reports these mutants as `unplaceable` and does not run them. Like
|
|
219
|
+
every `unplaceable` mutant, they leave the score and do not count toward
|
|
220
|
+
`--threshold`, so a gate that failed on these false survivors can now pass.
|
|
221
|
+
Their names and mutant ids do not change. `--strategy reload` runs them, except a
|
|
222
|
+
method in an `included` or `prepended` block (see #256 above). Another library's
|
|
223
|
+
method with one of these names is `unplaceable` under redefine too, even
|
|
224
|
+
when that library runs the block in the module itself and redefine used to
|
|
225
|
+
run it correctly. Mutineer cannot tell that library from ActiveSupport, so it
|
|
226
|
+
does not guess. For that other library's mutants, use `--strategy reload`.
|
|
227
|
+
- **A file in a namespace directory scores under `--rails --strategy reload`
|
|
228
|
+
and `--rails --daemon` (#253).** Mutineer writes each mutant to a temp file
|
|
229
|
+
next to the source and loads it. With lazy loading, loading it can be the
|
|
230
|
+
first time Zeitwerk scans that directory, such as `app/models/billing/`.
|
|
231
|
+
Zeitwerk then raised `Zeitwerk::NameError` on the temp file's name, and the
|
|
232
|
+
mutants of the file errored. Parallel daemon workers hit the same error on
|
|
233
|
+
each other's files. Now the temp files are hidden: `.mutineer_mutant*.rb`
|
|
234
|
+
and `.mutineer_daemon*.rb`. Zeitwerk skips hidden files, in every loader.
|
|
235
|
+
The cleanup sweeps still remove a file an older version left without the
|
|
236
|
+
dot. Each mode sweeps its own files before the app boots: an in-process run
|
|
237
|
+
now removes an old `mutineer_mutant*.rb` first, and the daemon removes an
|
|
238
|
+
old `mutineer_daemon*.rb`. An old file of the other mode still stops the
|
|
239
|
+
boot, so delete it by hand. Eager loading, the default on CI, scans every
|
|
240
|
+
directory at boot, so it never hit the error.
|
|
241
|
+
- **A mutant on a later line of a multi-line `&&` or `||` can be killed
|
|
242
|
+
(#254).** Ruby counts only the first line of a statement. For code that runs
|
|
243
|
+
only sometimes on a later line, such as the right side of `verified &&` on
|
|
244
|
+
its own line, Mutineer picked no tests, so the mutant was always
|
|
245
|
+
`no_coverage`, however good the tests were. Now it runs the tests that ran
|
|
246
|
+
the statement. A kill counts. A survivor stays `no_coverage`, because the
|
|
247
|
+
run cannot tell whether those tests reached it, so no survivor is added and
|
|
248
|
+
a score can rise with no test change. Such a mutant can now also end as
|
|
249
|
+
`errored` or `timeout`; like any mutant with no verdict, that counts toward
|
|
250
|
+
the `--threshold` completeness check, so a failure is reported, not hidden.
|
|
251
|
+
This also applies to a later `when` condition, the value of `x rescue y`,
|
|
252
|
+
`&.` arguments and an `||=` value on a later line, and to `--daemon`. A
|
|
253
|
+
branch of a ternary or `if`, and the body of `x if c`, keep the verdict they
|
|
254
|
+
had before.
|
|
255
|
+
|
|
9
256
|
## [1.6.0] - 2026-10-08
|
|
10
257
|
|
|
11
258
|
### Added
|
|
@@ -1150,6 +1397,7 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
|
|
|
1150
1397
|
- `.mutineer.yml` configuration (CLI > config > default precedence).
|
|
1151
1398
|
- Byte-correct source handling for multibyte (UTF-8) sources.
|
|
1152
1399
|
|
|
1400
|
+
[1.7.0]: https://github.com/davidteren/mutineer/releases/tag/v1.7.0
|
|
1153
1401
|
[1.6.0]: https://github.com/davidteren/mutineer/releases/tag/v1.6.0
|
|
1154
1402
|
[1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
|
|
1155
1403
|
[1.4.0]: https://github.com/davidteren/mutineer/releases/tag/v1.4.0
|