mutineer 1.5.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: 50113a063a85737ad99476f85e78cb2ac015f267018d19ba6350ba36ff4ac6df
4
- data.tar.gz: c12343a00d2df6d495859644c326bc650e33d013ddf009f2648c06a199aceb86
3
+ metadata.gz: 2afb9e81b16cbdf3b7ecd0d074365ee05eb58bdb0045ff6a73643a68893a4b1a
4
+ data.tar.gz: 949369679de39fbc53025fc470896bbef0c5310fc59c11cb928ad80aa748cc22
5
5
  SHA512:
6
- metadata.gz: a170b15c6883a4dcec6a2798c3b102a5f9c9ded1f46c166df98d0e5bdf5b891ca5b24892efe06245680b45a94da38638adc36ca9a48a48d2ac1c07b29126f173
7
- data.tar.gz: a30272ff6afb48dd666d6945c6532d987ef335e0e78dc422420cf26cdbec639ee1c850118a10ba25e10a426a2644b3926e39698126eaad0646707a4f2be2e719
6
+ metadata.gz: d78d0145b257b25321b7a9f05ebebc51e1818459eacecd26917953ef2dd0296af34fcb755a3a782102ef5453a9f27d90cb3fb2d8261fd8760ab557f273b2d866
7
+ data.tar.gz: 3d844218d9e05bc1c09bc7c1bfa7617c7fa79b49c943bb1a96af20ab68fa85312baf08ed9fa37d4f9929bc07c027c943717e2ce4344e15639509feeae66ff560
data/CHANGELOG.md CHANGED
@@ -6,6 +6,477 @@ 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
+
256
+ ## [1.6.0] - 2026-10-08
257
+
258
+ ### Added
259
+
260
+ - **An `unplaceable` mutant status.** Under `--strategy redefine`, a mutant in
261
+ a method whose class or module has no constant name is not run. It is left
262
+ out of the score and, unlike `uncapturable`, out of `no_verdict`, so it does
263
+ not fail `--threshold`. The human report gives it an `Unplaceable:` row, the
264
+ HTML report a count, and the JSON report `summary.unplaceable` and an
265
+ `unplaceable[]` list; `schema_version` is `1.7`.
266
+ - **The agent skill installs with `gh skill` and `npx skills` (#125).** It
267
+ moved from `docs/skill.md` to `skills/mutineer/SKILL.md`, the layout of the
268
+ Agent Skills specification, so `gh skill install davidteren/mutineer
269
+ mutineer` finds it and `npx skills add davidteren/mutineer --skill mutineer`
270
+ installs one `SKILL.md` instead of the whole `docs/` site. The site build
271
+ copies that file to `skill.md`, so its URL still works. The skill's
272
+ description now says when to use it, and the README shows how to install it.
273
+ - **A `ran_at_load` mutant status** (#187). A mutant on a method-body line
274
+ that ran while the app booted or its class loaded gets this status when it
275
+ survives, or when only the load ran the line. A kill stays a kill. It is
276
+ left out of the score and out of `no_verdict`, so it does not fail
277
+ `--threshold`. The human report gives it a `Ran at load:` row that points
278
+ to `--test-command` to verify these mutants, the HTML report a count, and
279
+ the JSON report `summary.ran_at_load` and a `ran_at_load[]` list, within
280
+ schema `1.7`.
281
+
282
+ ### Changed
283
+
284
+ - **Mutants no longer run under Coverage with `--boot` or `--rails` (#228).** Coverage
285
+ starts before the app boots so the coverage map can be built, and it used to
286
+ keep running in every mutant fork after that, though nothing read it. It is
287
+ now suspended once the map is built, from a fresh capture or from the cache.
288
+ Coverage that a host process started before the run is left running.
289
+ On a small app whose test calls tiny methods in a loop, the CPU time of a
290
+ whole in-process `--boot` run fell from 0.76s to 0.57s (median of five). On
291
+ the Rails fixture app the run is mostly boot, and the change is within noise
292
+ (1.61s before, 1.60s after). `--daemon` runs its mutants in worker daemons
293
+ that never start Coverage, so its time does not change (3.48s before, 3.44s
294
+ after); the map-building daemon now suspends Coverage after the map too.
295
+ - **A mutant runs its paired test files first, then the cheapest (#203).** The
296
+ covering test files used to run in the order the coverage map stored them,
297
+ so a mutant that one fast file kills could still score `timeout` when slow
298
+ files ran first. Now the files that pairing finds for the source run first,
299
+ then the rest from fastest to slowest by the time each file took during
300
+ coverage capture, less the fastest file's time (the startup cost each
301
+ capture pays). The order is fixed for a given cache. Costs compare in
302
+ doubling buckets, so files of close cost keep their path order when a
303
+ rebuilt cache measures them again; a file near a bucket edge can still move,
304
+ so keep `.mutineer/` between CI runs for the most stable `--baseline`. The
305
+ order applies to Minitest (after the seeded class shuffle, with
306
+ `parallelize_me!` classes still last) and to RSpec. `--daemon` runs every
307
+ covering test with no stop, so the order does not change its verdicts. The
308
+ coverage cache now saves these timings, so a cache from an earlier version
309
+ rebuilds once. A complete run gives the same verdicts and `--matrix` rows,
310
+ but the new order can change which mutants time out, so a `--baseline` gate
311
+ can see a one-time change on upgrade.
312
+ - **Job selection moved from `Mutineer::Runner` to a new `Mutineer::JobPlan`
313
+ module (#75).** The in-process runner, the `--daemon` backend and
314
+ `--dry-run` share it, so each file now requires what it uses and no
315
+ `require_relative` cycle exists in `lib/mutineer`. The moved methods are
316
+ `collect_jobs`, `filter_since`, `coverage_selection`, `ran_at_load?`,
317
+ `load_verdict`, `abort_if_unclean!`, `test_load_roots`, `source_dirs`,
318
+ `sweep_orphans`, `suppress_map`, `suppressed?` and `result_keys`. These were
319
+ internal methods, so `Runner` keeps no copies. Runs, reports and caches do
320
+ not change.
321
+
322
+ ### Fixed
323
+
324
+ - **An endless method that only the tests call is no longer `no_coverage`
325
+ under `--rails`, `--boot` or `--daemon` (#209).** Ruby counts no line when
326
+ an endless method such as `def half(x) = x / 2` runs, only its `def` line
327
+ when it is defined. The boot defines it before any test runs, so the forked
328
+ coverage capture never saw that line run. Coverage now also
329
+ counts methods (`Coverage.start(lines: true, methods: true)`), and a test
330
+ that calls such a method covers its `def` line. Coverage caches from earlier
331
+ versions rebuild once.
332
+ - **`module_function :name` in a reopened module inside `class << self`
333
+ promotes the method from the earlier opening (#216).** Ruby treats both
334
+ openings as one module, but Mutineer matched them only within one body, so
335
+ it named the method `#<Class:App>::M#c` instead of `#<Class:App>::M.c`.
336
+ The two openings now match by the module's full name. A module whose name
337
+ Mutineer cannot be sure of still matches only within its own body: a path
338
+ such as `self::X` or `Foo::X` written directly inside `class << self`, a
339
+ name under an anonymous class, or a module in a `class << self` inside a
340
+ method or a `class_eval` block. A `module self::X` in a block, or a
341
+ `self::X = ...` in a `class_eval` block, matches only openings in the same
342
+ block. A `self::X` like these now also stays apart from a
343
+ module of the same name outside `class << self`, where a `module_function`
344
+ in that module used to promote its methods.
345
+ - **A `module self::X` or `class self::X` in a block is `unplaceable` under
346
+ `--strategy redefine` (#229).** In a block, `self` is not always the
347
+ enclosing class: in `Other.class_eval do` it is `Other`, and in
348
+ `Foo = Class.new do` it is the new class. Mutineer gave these modules a
349
+ known owner on the enclosing namespace, so redefine loaded the mutant onto
350
+ a module the tests never use, and the mutant could falsely survive or
351
+ score `error`. Such a module, everything nested in it, and a
352
+ `self::X = ...` in a block that does not build a class now have
353
+ their owner unknown: redefine reports their mutants as `unplaceable`, and
354
+ `--strategy reload` still runs them. In a `Data.define`, `Struct.new`,
355
+ `Class.new` or `Module.new` block, the module is now named under the
356
+ builder's constant (`Outer::Foo::X#c`, was `Outer::X#c`), so the subject
357
+ names and mutant ids of those methods change. `self::X` directly in a class
358
+ or module body is unchanged.
359
+
360
+ - **`--daemon` worker databases start as a copy of the test database
361
+ (#222).** Each worker slot's SQLite database used to start from
362
+ `db/schema.rb` alone. Rows the daemon wrote while it booted, in
363
+ initializers or `--require` files, were only in the base test database, so
364
+ a test that read them passed in-process but failed under `--daemon`. On its
365
+ first use, a worker database is now a copy of the base test database (schema
366
+ and rows, made with the SQLite online backup API). The worker still loads
367
+ `db/schema.rb` when the copy's schema differs from it (another schema
368
+ version, or another or no `schema_sha1` checksum in `ar_internal_metadata`),
369
+ for example when the test database is empty, out of date, or set up with a
370
+ plain `load "db/schema.rb"`. That load drops the
371
+ copied rows of the tables it defines.
372
+ - **Standalone coverage capture loads the `--require` files (#217).** A run
373
+ without `--boot` requires the sources and then each `--require` (or
374
+ `require:`) file before it forks the mutants, but the coverage capture
375
+ loaded only the sources. A source method that a `--require` file called
376
+ while it loaded was then not seen as run at load, so its mutant was a false
377
+ `survived` instead of `ran_at_load`. The capture and the clean-suite check
378
+ now load the `--require` files after the sources. With or without
379
+ `--boot`, the coverage cache rebuilds when one of them changes, is added,
380
+ is removed, or moves in the load order.
381
+ - **`--daemon` loads the `--require` files (#220).** The daemon booted the app
382
+ but never loaded the `--require` (or `require:`) files, so a test that
383
+ needs one failed under `--daemon` and passed in-process. A source method
384
+ that such a file called while it loaded was also not seen as run at load,
385
+ so its mutant could be a false `survived` instead of `ran_at_load`. The
386
+ daemon now loads the files after the app boots, as an in-process run does,
387
+ and the daemon's coverage cache rebuilds when one of them changes.
388
+ - **The release workflow publishes only stable tags** (#161). Its trigger,
389
+ `v*.*.*`, also matches a prerelease tag such as `v1.3.1-rc1`, and the only
390
+ guard checked that the tag matched `Mutineer::VERSION`. A prerelease tag
391
+ could then publish the gem and move the floating major tag (`v1`) that
392
+ Action users pin. The workflow now stops with a clear error before any
393
+ publish step unless the tag is a stable `vMAJOR.MINOR.PATCH`.
394
+ - **Coverage capture keeps to `capture_timeout` from start to finish.** Under
395
+ `--boot` and `--daemon`, a hung test file ignored the timeout and blocked
396
+ the run; it is now skipped as "timed out", like a standalone capture. A
397
+ process that a test started and left running could keep the result pipe
398
+ open and make a boot-mode capture wait for it to exit. Each capture now runs
399
+ in its own process group, and a timeout stops the whole group, not only the
400
+ test process. A capture whose test finished just before the deadline could
401
+ also lose its valid result and report "invalid coverage output"; it now
402
+ gets a short grace period to read the result. (#101, #129)
403
+ - **`--daemon` no longer waits forever on a daemon that stops answering**
404
+ (#101). A mutant whose verdict has not arrived 30 seconds after its own
405
+ timeout is scored `error`, and the daemon is killed and respawned, as after
406
+ a crash. A boot that has not finished after 600 seconds ends the run with a
407
+ message that says so (it is not retried by a second daemon), and shutdown no longer waits on a stuck daemon.
408
+
409
+ - **A method in a `Data.define`, `Struct.new`, `Class.new` or `Module.new`
410
+ block belongs to the class or module the block builds.** In
411
+ `class App; Argo = Data.define(:url) do def self.load = ...; end; end` the
412
+ subject is now `App::Argo.load`, not `App.load`; at the top level it was
413
+ `.load`, with no owner. Under `--strategy redefine` (the `--rails` default)
414
+ the mutated method was loaded into the wrong class, so the real method never
415
+ changed and every such mutant falsely survived. The class or module is
416
+ named after the constant it is assigned to with `=`, `||=` or `&&=`, through
417
+ parentheses or `begin`, resolved as Ruby resolves the assignment: `X` in the
418
+ current namespace, `::X` and any path at the top level from `Object`,
419
+ `self::X` under the current class. When it cannot be named statically (no
420
+ constant, a relative path such as `User::Permission` inside a namespace, or
421
+ a constant assigned in `class << self`), redefine reports its mutants with
422
+ a new status, `unplaceable`, rather than load them onto the wrong class;
423
+ reload still runs them. `module_function :name` in a `Module.new` block
424
+ promotes that module's method. Mutant ids for these subjects change:
425
+ regenerate `ignore:` entries and baselines that name them.
426
+ - **The automated release PR gets its required CI checks without a token
427
+ secret (#85).** `release-pr.yml` pushes the release branch with the default
428
+ `GITHUB_TOKEN`, which starts no other workflow, so the four required checks
429
+ never ran and the PR stayed blocked until a person closed and reopened it.
430
+ The job now dispatches `ci.yml` on the release branch (CI gained a
431
+ `workflow_dispatch` trigger), and those runs report the required checks on
432
+ the PR's head commit. With a `RELEASE_PR_TOKEN` secret set, the push
433
+ triggers CI as before and no dispatch is made.
434
+ - **Code that runs while the app boots or a class loads no longer gives a false
435
+ `survived` or `no_coverage`** (#187). A class body such as
436
+ `ALL = [price(3)].freeze` runs `price` before any mutant is applied. Under
437
+ `--strategy redefine` (the `--rails` default) the load never runs again, so
438
+ a test that checks `ALL` saw the original value and the mutant falsely
439
+ survived. Under `--rails` or `--boot` no test was credited with lines that
440
+ ran during boot, so those mutants were falsely `no_coverage`. Mutineer now
441
+ reads the lines that ran at load (from the boot, or from requiring the
442
+ sources in standalone capture) and reports these mutants as `ran_at_load`,
443
+ under both strategies and with `--daemon`: reload runs the file's class body
444
+ again but not an initializer or another file's code, so a survivor there is
445
+ not trusted either. Under `--boot` with redefine, a
446
+ lazily loaded class (Zeitwerk, `autoload`) is loaded before the boot lines
447
+ are read, so its class body counts as load too. A baseline survivor that is
448
+ now `ran_at_load` is listed as fixed, not as a regression. Standalone
449
+ coverage caches from earlier versions rebuild once. A one-line or endless
450
+ `def` keeps its body on the `def` line, which Ruby counts when the method is
451
+ defined, so Mutineer reads the method's call count at load for it instead
452
+ (#209).
453
+ - **A boot file that prints to stdout no longer breaks the daemon handshake
454
+ (#102).** Output from `puts`, `STDOUT`, `$stdout`, a direct fd 1 write, or a
455
+ subprocess reached the JSON channel before the ready message, so the daemon
456
+ failed with "daemon exited before the handshake". The daemon now keeps a
457
+ private copy of its original stdout for the protocol and points the app's
458
+ stdout at stderr, where that output stays visible.
459
+ - **A class or module opened inside `class << self` belongs to the singleton
460
+ class (#208).** In `class App; class << self; class Q; def q1 ...` the
461
+ subject was `App::Q.q1`, a class method of a constant Ruby never defines:
462
+ `Q` is `App.singleton_class::Q`, and `q1` is an instance method. It is now
463
+ `#<Class:App>::Q#q1` with no known owner, as is a class or module opened in
464
+ a builder block there, and anything nested in either. Inside a builder
465
+ block, `class << self` opens the built class's singleton class, so a class
466
+ or constant there is named under it, for example `#<Class:App::P>::W`. Under
467
+ `--strategy redefine` loading the mutant raised `NameError` and scored
468
+ `error`; redefine now reports these mutants as `unplaceable`, and reload
469
+ still runs them. A compact `class Foo::X` and a top-level `class ::X` or
470
+ `::X = Class.new do` there are named as written (`::X` as `X`, now with
471
+ instance methods) and are owner-unknown too: redefine reopens them without
472
+ the singleton class, so constants their bodies look up through it would not
473
+ resolve. `module_function :name` in a body whose owner is unknown now
474
+ promotes that body's own methods, wherever it is: a `Foo::X = Module.new`
475
+ block's `c` is `Foo::X.c`, not `Foo::X#c`, and a def in an anonymous
476
+ `Class.new` block is no longer promoted by a `module_function` of the module
477
+ around it. Mutant ids for these subjects change: regenerate `ignore:`
478
+ entries and baselines that name them.
479
+
9
480
  ## [1.5.0] - 2026-10-06
10
481
 
11
482
  ### Added
@@ -926,6 +1397,8 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
926
1397
  - `.mutineer.yml` configuration (CLI > config > default precedence).
927
1398
  - Byte-correct source handling for multibyte (UTF-8) sources.
928
1399
 
1400
+ [1.7.0]: https://github.com/davidteren/mutineer/releases/tag/v1.7.0
1401
+ [1.6.0]: https://github.com/davidteren/mutineer/releases/tag/v1.6.0
929
1402
  [1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
930
1403
  [1.4.0]: https://github.com/davidteren/mutineer/releases/tag/v1.4.0
931
1404
  [1.3.0]: https://github.com/davidteren/mutineer/releases/tag/v1.3.0