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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eeac45268c75073f522daa5d26dd65fe425ae66b604cfb7543c0b529ab71f2e6
4
- data.tar.gz: 834755a3b106f02013ef75b9515f18e692d1c6ac219bee42b837da8a6f696fc7
3
+ metadata.gz: 2afb9e81b16cbdf3b7ecd0d074365ee05eb58bdb0045ff6a73643a68893a4b1a
4
+ data.tar.gz: 949369679de39fbc53025fc470896bbef0c5310fc59c11cb928ad80aa748cc22
5
5
  SHA512:
6
- metadata.gz: '092252eef7f756e5fa117784b7c3696777127b0f1f1b96825c38d99a9f8d362a23416ad2351685e62bd780563a2b7fa2eab0e926c10bee3e293293ff88e0d28d'
7
- data.tar.gz: 02344ad3f678a639a37f0272577e50e5f189e585a9246be0b0f1451202a03c63cb57e013181e349ab7b4db04f36fb2333e2de7d864063c4da92d4394f5dce6b3
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