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 +4 -4
- data/CHANGELOG.md +473 -0
- data/README.md +352 -26
- data/lib/mutineer/child_coverage.rb +53 -0
- data/lib/mutineer/cli.rb +184 -24
- data/lib/mutineer/concern_blocks.rb +80 -0
- data/lib/mutineer/config.rb +148 -8
- data/lib/mutineer/coverage_map.rb +541 -92
- data/lib/mutineer/daemon_backend.rb +227 -86
- data/lib/mutineer/daemon_client.rb +72 -9
- data/lib/mutineer/daemon_server.rb +229 -51
- data/lib/mutineer/external_backend.rb +12 -1
- data/lib/mutineer/file_swap.rb +1 -1
- data/lib/mutineer/init.rb +124 -0
- data/lib/mutineer/isolation.rb +15 -2
- data/lib/mutineer/job_plan.rb +458 -0
- data/lib/mutineer/migrate.rb +705 -0
- data/lib/mutineer/minitest_integration.rb +48 -1
- 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 +100 -0
- data/lib/mutineer/project.rb +411 -21
- data/lib/mutineer/rails_run_env.rb +73 -0
- data/lib/mutineer/rails_worker_db.rb +1477 -48
- data/lib/mutineer/redaction.rb +98 -0
- data/lib/mutineer/reporter.rb +83 -12
- data/lib/mutineer/result.rb +49 -8
- data/lib/mutineer/runner.rb +66 -290
- data/lib/mutineer/statement_lines.rb +54 -7
- data/lib/mutineer/subject.rb +24 -2
- data/lib/mutineer/test_runners/rspec.rb +54 -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 +5 -0
- metadata +11 -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,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
|