mutineer 1.7.0 → 1.8.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 +432 -0
- data/README.md +157 -747
- data/lib/mutineer/baseline.rb +1 -1
- data/lib/mutineer/changed_lines.rb +2 -1
- data/lib/mutineer/cli.rb +119 -27
- data/lib/mutineer/concern_blocks.rb +168 -5
- data/lib/mutineer/config.rb +6 -0
- data/lib/mutineer/coverage_map.rb +92 -17
- data/lib/mutineer/daemon_backend.rb +75 -32
- data/lib/mutineer/daemon_client.rb +50 -17
- data/lib/mutineer/daemon_server.rb +92 -40
- data/lib/mutineer/external_backend.rb +150 -17
- data/lib/mutineer/isolation.rb +67 -25
- data/lib/mutineer/job_plan.rb +117 -33
- data/lib/mutineer/kill_channel.rb +24 -14
- data/lib/mutineer/migrate.rb +10 -8
- data/lib/mutineer/minitest_integration/in_test_database_error.rb +184 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +25 -17
- data/lib/mutineer/minitest_integration.rb +44 -14
- data/lib/mutineer/mutators/array_literal.rb +4 -2
- data/lib/mutineer/mutators/base.rb +22 -0
- data/lib/mutineer/mutators/call_to_receiver.rb +20 -7
- data/lib/mutineer/mutators/chain_link.rb +6 -6
- data/lib/mutineer/mutators/condition_forcing.rb +6 -3
- data/lib/mutineer/mutators/operand_removal.rb +4 -2
- data/lib/mutineer/mutators/range_literal.rb +105 -4
- data/lib/mutineer/mutators/return_nil.rb +1 -0
- data/lib/mutineer/orphan_guard.rb +18 -6
- data/lib/mutineer/pairing.rb +62 -1
- data/lib/mutineer/project.rb +226 -12
- data/lib/mutineer/rails_worker_db.rb +200 -16
- data/lib/mutineer/redaction.rb +112 -11
- data/lib/mutineer/reporter.rb +109 -21
- data/lib/mutineer/result.rb +33 -5
- data/lib/mutineer/runner.rb +126 -18
- data/lib/mutineer/statement_lines.rb +14 -8
- data/lib/mutineer/subject.rb +13 -5
- data/lib/mutineer/test_runners/minitest.rb +2 -1
- data/lib/mutineer/test_runners.rb +4 -2
- data/lib/mutineer/triage.rb +1 -1
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +2 -1
- data/skills/mutineer/SKILL.md +84 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86c3963ad7e14aed11afda2b474e0465f5eeec91d4bac3aea68c8aaa8d405391
|
|
4
|
+
data.tar.gz: d8d632c8cde79fccda2e29583ac213cff95460f9f430afed56e31675170c27ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffd90e04f7af0a47b630e7083873d2dfa2442b386900a03ee4ae12e0ef08709f126b1cedfa3e9397e68448b49c51795012b7430264717aed6975f1c69ee74bca
|
|
7
|
+
data.tar.gz: d5dadd48db5f383cae7220cc6eda44f6fe1c53b5e6348eda92f97be16a176af239c2ce5556f605b980141e54d5805b62a33a96f2254fdfdbe590a5c7f1c78d51
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,437 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.8.0] - 2026-10-29
|
|
10
|
+
|
|
11
|
+
### Highlights
|
|
12
|
+
|
|
13
|
+
Read these before you upgrade. Details are in the sections below.
|
|
14
|
+
|
|
15
|
+
- **Scores can change after the upgrade.** Under `--strategy reload`, most methods
|
|
16
|
+
in a concern's `included` or `prepended` block are now scored (#297). Runs
|
|
17
|
+
that list `call_to_receiver` get more mutants (#274). Runs that list
|
|
18
|
+
`range` get new mutants for endless ranges (#299). A mutant that two
|
|
19
|
+
operators make now counts once (#275). These can change counts, scores
|
|
20
|
+
and survivors. If you gate on `--baseline`, write a new full baseline.
|
|
21
|
+
- **Delete `ignore:` entries for `range` mutants (#299).** A new `range`
|
|
22
|
+
mutant can take an old id, so an old entry can hide a real survivor.
|
|
23
|
+
- **Fewer false kills.** Under `--test-command`, a non-zero exit other than 1
|
|
24
|
+
scores `error`, and so does an exit 1 with no failing test in the summary
|
|
25
|
+
line (#292). An Active Record connection or lock error inside a Minitest
|
|
26
|
+
test scores `error` too (#296). Before, these scored `killed`.
|
|
27
|
+
- **Exit codes match STABILITY.md (#288).** A boot, source or `require:`
|
|
28
|
+
file that raises while it loads exits 1. A missing boot or `require:`
|
|
29
|
+
file exits 2.
|
|
30
|
+
- **`--threshold` compares the exact score (#280).** A run with 1999 of
|
|
31
|
+
2000 mutants killed shows 100.0% but no longer passes `--threshold 100`.
|
|
32
|
+
- **New report data.** The JSON report adds `summary.declined` and
|
|
33
|
+
`summary.declined_by_operator` (schema `1.9`), and stderr warns when many
|
|
34
|
+
mutants could not run (#291).
|
|
35
|
+
- **Parallel `--daemon` with unused test databases (#316).** List them under
|
|
36
|
+
`shared_databases` to run more than one worker. Needs Rails 7.1 or later.
|
|
37
|
+
- **The gem ships the agent skill (#315).**
|
|
38
|
+
- **Releases wait for a maintainer.** Nothing publishes on a schedule or on
|
|
39
|
+
a tag push.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`shared_databases` config key** (#316). An app with a second test
|
|
44
|
+
database ran one worker under `--daemon`. List the databases your tests
|
|
45
|
+
never use (`shared_databases: [cable]`), and the workers share them and run
|
|
46
|
+
in parallel. After each mutant, a parallel worker checks those databases.
|
|
47
|
+
The mutant scores `error` when a pool for one is still connected after the
|
|
48
|
+
tests, or ran SQL during them. A replica in the same `connects_to` counts
|
|
49
|
+
as its database. Listing the primary database stops the run with exit 2.
|
|
50
|
+
A name that matches no test database warns and is not checked. The check
|
|
51
|
+
needs Rails 7.1 or later. Three safety changes come with it for
|
|
52
|
+
`--rails --daemon`: a test database with `database_tasks: false` now
|
|
53
|
+
counts toward the one-worker limit, and the run keeps one worker when the
|
|
54
|
+
app's databases cannot be listed or the first daemon does not boot
|
|
55
|
+
(before, each case could start several workers). See
|
|
56
|
+
[More than one database](https://github.com/davidteren/mutineer/blob/main/docs/rails-setup.md#more-than-one-database)
|
|
57
|
+
for the transactional-tests caveat.
|
|
58
|
+
- **Counts of mutants that were not scored (#291).** Operators now count the
|
|
59
|
+
candidates they decline: a heredoc they cannot mutate safely, or an
|
|
60
|
+
equivalent skip such as an endless range or a skipped chain-link name. The
|
|
61
|
+
dry run prints the count. The human report prints it when the run has
|
|
62
|
+
mutants; when every candidate was declined, the empty-run warning names the
|
|
63
|
+
count instead (that warning is off under `--allow-empty`). The JSON report
|
|
64
|
+
adds `summary.declined` and `summary.declined_by_operator` (schema `1.9`).
|
|
65
|
+
A new stderr warning fires when unplaceable and skipped-invalid mutants are
|
|
66
|
+
more than one and more than 10% of the mutants a run tried to score. The
|
|
67
|
+
warning does not change the exit code. Measured on commit caa9e1a (this
|
|
68
|
+
change merged with main) with every operator: the dry run on `lib/`
|
|
69
|
+
declines 195 candidates beside 17,374 mutations (call_to_receiver 122,
|
|
70
|
+
chain_link 45, range 12, statement_removal 10, return_nil 6); the dry run
|
|
71
|
+
on the Rails fixture app declines 0 beside 69 mutations. A
|
|
72
|
+
`--strategy redefine --rails` run on the fixture app models (order,
|
|
73
|
+
price_list, billing/fee) has 0 unplaceable and 0 skipped-invalid of 12
|
|
74
|
+
attempted, so no warning. The fixture has no concerns, so it cannot tune
|
|
75
|
+
the 10% limit.
|
|
76
|
+
- **Included and prepended block methods under reload.** After you upgrade,
|
|
77
|
+
follow the [full baseline refresh recipe](https://davidteren.github.io/mutineer/agentic-coding.html#ci-gate)
|
|
78
|
+
with `--no-since --format json --output .mutineer/refreshed-baseline.json` and your
|
|
79
|
+
existing source and test options. Review the report and replace the stored
|
|
80
|
+
baseline. This applies only to runs that use `--strategy reload`: under
|
|
81
|
+
redefine these methods stay `unplaceable`. GitHub Action users on reload,
|
|
82
|
+
the Action's default (a Rails app without a daemon uses redefine), must
|
|
83
|
+
replace the file passed to the `baseline` input too. A check against an older
|
|
84
|
+
report lists the new survivors as regressions.
|
|
85
|
+
Under `--strategy reload`, in-process and on the daemon, a method in a
|
|
86
|
+
concern's `included` or `prepended` block is now mutated and scored (#297).
|
|
87
|
+
Mutineer loads the mutated method into each class that already has it. A
|
|
88
|
+
prepended wrapper that calls `super` reaches that mutated method too. A
|
|
89
|
+
concern included into an object's singleton class is supported. A class
|
|
90
|
+
that includes the concern later gets it from the reloaded block. The
|
|
91
|
+
rest of the block does not run again, so validators and callbacks are not
|
|
92
|
+
added twice. These mutants used to be `unplaceable`, so a reload score can go
|
|
93
|
+
down and a `--threshold` gate can start to fail. Some methods stay
|
|
94
|
+
`unplaceable`: a method in a class, module or `Struct.new` block inside the
|
|
95
|
+
block, a method inside another method or block, a method with an explicit
|
|
96
|
+
receiver other than `self` (such as `def base.tag` in `included do |base|`),
|
|
97
|
+
a hook-block method whose source line conflicts with another `def` or an
|
|
98
|
+
explicit Ruby method creator (the full rule is in `docs/json-schema.md`),
|
|
99
|
+
every method in a block that uses `alias` or `alias_method`, a method the
|
|
100
|
+
block itself calls, and every method in a block whose `included do` call is
|
|
101
|
+
not a plain line of the module
|
|
102
|
+
body (for example inside `if`, `unless`, or `x = included do`). The full list
|
|
103
|
+
is in `docs/json-schema.md`. If a class aliases a method that Mutineer does
|
|
104
|
+
run (one not in that list) outside the block, the mutant scores `error`,
|
|
105
|
+
because the alias keeps the original method. This also applies when a
|
|
106
|
+
wrapper replaces the original name and calls the saved alias. A retained
|
|
107
|
+
`Method` or `UnboundMethod` for the original also makes the mutant score
|
|
108
|
+
`error`: a wrapper can call that handle without testing the mutated method.
|
|
109
|
+
`--strategy redefine` still
|
|
110
|
+
reports all of these methods `unplaceable`.
|
|
111
|
+
- **The gem ships the agent skill.** `skills/mutineer/SKILL.md` is now
|
|
112
|
+
packaged in the gem, and the gemspec opts in as a
|
|
113
|
+
[Rails Hyperdrive](https://github.com/rails-hyperdrive/rails-hyperdrive)
|
|
114
|
+
companion (`hyperdrive_targets: "*"`, `hyperdrive_artifacts: "skill"`). In a
|
|
115
|
+
Rails app that bundles mutineer, `bin/rails hyperdrive:init` installs the
|
|
116
|
+
skill to `.claude/skills/mutineer/SKILL.md`. A test in the default suite
|
|
117
|
+
fails if the gemspec stops shipping the skill or drops the opt-in (#315).
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- **`--daemon` stops on MySQL older than 8.0 (#298).** The support matrix said
|
|
122
|
+
MySQL 5.7 is not supported, but `--daemon` ran on it. Mutineer now reads the
|
|
123
|
+
server version before it copies any worker database. MySQL older than 8.0
|
|
124
|
+
stops the run with `could not provision <base>: MySQL <version> is not
|
|
125
|
+
supported`. Pass `--no-daemon` (or set `daemon: false`) to stay serial.
|
|
126
|
+
MariaDB, MySQL 9 or later, and a server version Mutineer cannot read print
|
|
127
|
+
one warning and run.
|
|
128
|
+
- **An in-test database error scores `error`, not `killed` (#296).** When
|
|
129
|
+
the first failing test of a Minitest mutant run failed with an Active
|
|
130
|
+
Record connection or lock error (`ConnectionNotEstablished`, including pool
|
|
131
|
+
timeouts, `ConnectionFailed`, `Deadlocked`, `LockWaitTimeout`), the mutant
|
|
132
|
+
now scores `error` on `--daemon` and in-process. This includes an error in
|
|
133
|
+
`setup`. Errors are out of the score, so a lost database connection during
|
|
134
|
+
a run no longer raises the score. A stderr line names the error. A mutant
|
|
135
|
+
that really causes a deadlock, or that breaks connection-handling code, now
|
|
136
|
+
also scores `error`. More `error` verdicts can fail a `--threshold` run
|
|
137
|
+
through the existing no-verdict gate. Other database errors, such as a bad
|
|
138
|
+
SQL statement, still score `killed`. So does `ConnectionNotDefined`, which
|
|
139
|
+
Rails 8 raises for an unknown role or shard. Rails 7.2 raises a plain
|
|
140
|
+
`ConnectionNotEstablished` there, so such a mutant scores `error` on 7.2.
|
|
141
|
+
With `--matrix`, a database-failed test is never a killer and leaves its
|
|
142
|
+
row incomplete, even if an earlier assertion already killed the mutant.
|
|
143
|
+
- **An old temp file of the other mode no longer stops the boot (#287).**
|
|
144
|
+
Before the app boots, each mode sweeps the source directories. In 1.7.0
|
|
145
|
+
an in-process run removed only old `mutineer_mutant*.rb` files and the
|
|
146
|
+
daemon removed only old `mutineer_daemon*.rb` files, so a file the other
|
|
147
|
+
mode left made Zeitwerk raise. Now both boot sweeps remove an orphan of
|
|
148
|
+
either mode, and the daemon tells Zeitwerk to skip both names. You no
|
|
149
|
+
longer delete such a file by hand. A file of your own whose name only
|
|
150
|
+
starts with `mutineer_mutant` or `mutineer_daemon`, such as
|
|
151
|
+
`mutineer_mutant_helper.rb`, is no longer removed: a sweep now matches
|
|
152
|
+
only the temp file shape. An unlocked old file is removed even when its
|
|
153
|
+
creator's pid has been reused; a held file lock still protects it.
|
|
154
|
+
Do not run Mutineer older than 1.7.0
|
|
155
|
+
at the same time in the same checkout: it still writes temp files
|
|
156
|
+
without the dot, and a newer run's mutants error when Zeitwerk finds
|
|
157
|
+
one.
|
|
158
|
+
- **A second in-process run no longer deletes a live mutant file (#295).**
|
|
159
|
+
Each in-process run sweeps the source directories before its boot and
|
|
160
|
+
after its run. That sweep treated every `.mutineer_mutant*.rb` file as an
|
|
161
|
+
orphan, so it could delete another run's file between the write and the
|
|
162
|
+
load, and that mutant scored `errored`. Now the file is locked while it
|
|
163
|
+
loads, as the daemon's file is, and a sweep keeps a file whose creating
|
|
164
|
+
process still runs.
|
|
165
|
+
- **The `range` operator swaps endless ranges outside slices.** Before, it
|
|
166
|
+
skipped every endless range (`1..`, `1..nil`) as equivalent. But `(1..)`
|
|
167
|
+
and `(1...)` differ on `==`, `hash`, `exclude_end?` and `to_s`, so a test
|
|
168
|
+
can kill that mutant. Now the operator skips an endless range only when it
|
|
169
|
+
is a direct index argument of `[]`, `[]=`, `slice`, `slice!`, `byteslice`,
|
|
170
|
+
`bytesplice`, `values_at` or an index write, and the receiver is not a
|
|
171
|
+
class (`Array[1..]` is swapped). A Hash, Proc, or custom `[]`/`slice` can
|
|
172
|
+
still tell `1..` from `1...`, so those skips can hide a killable mutant. See
|
|
173
|
+
[Equivalent skips](https://davidteren.github.io/mutineer/equivalent-skips.html#endless-range).
|
|
174
|
+
A run that names `range` can get new mutants, new survivors and new mutant
|
|
175
|
+
ids. A new mutant can take the id of an older `range` mutant in the same
|
|
176
|
+
method, so an old `ignore:` id can name a different mutant. If you use
|
|
177
|
+
`range`:
|
|
178
|
+
- Delete the `ignore:` entries for `range` mutants.
|
|
179
|
+
- Save a new report with the same operators you gate with:
|
|
180
|
+
`mutineer run <paths> --operators <list> --no-since --format json --output <file>`.
|
|
181
|
+
Use it as your new baseline.
|
|
182
|
+
- Print new entries with
|
|
183
|
+
`mutineer triage <file> --reason TEXT (--id ID ... | --all)` and paste
|
|
184
|
+
them under `ignore:`. The command does not edit files.
|
|
185
|
+
|
|
186
|
+
Some new mutants are equivalent, for example a range used only with
|
|
187
|
+
`include?`, or a range in an Active Record query such as
|
|
188
|
+
`where(created_at: time..)`. Suppress them with `# mutineer:disable-line`
|
|
189
|
+
or an `ignore:` entry. (#299)
|
|
190
|
+
- **A test file given as a source after `--test` now exits 2 in more layouts
|
|
191
|
+
(#293).** The check missed Minitest `test_*.rb` files under `test/` or
|
|
192
|
+
`spec/`, and `*_test.rb` or `*_spec.rb` files in the same directory as a
|
|
193
|
+
`--test` file, such as the project root. Mutineer then mutated that file as
|
|
194
|
+
a source and never ran it as a test. With `--format json`, the stray file's
|
|
195
|
+
Minitest output also made the report invalid JSON. A support file with a
|
|
196
|
+
`test_` name under the top-level `test/` or `spec/` (other than
|
|
197
|
+
`test_helper.rb`) now also exits 2 when given as a source with `--test`.
|
|
198
|
+
- **A mutant on a later line of a call inside a one-line block can be killed
|
|
199
|
+
(#289).** A call can span several lines, such as
|
|
200
|
+
`users.map { |u| format("%s",` with arguments on the next lines. Mutineer
|
|
201
|
+
used to pick no tests for a mutant on a later line, so it was always
|
|
202
|
+
`no_coverage`. Now, when the lines of the statement give no tests, Mutineer
|
|
203
|
+
retries. It runs the tests that ran the first or last line of the
|
|
204
|
+
statement, never the `def` line. A kill counts. A survivor stays
|
|
205
|
+
`no_coverage` (or `ran_at_load`), because those tests may not have run it. A
|
|
206
|
+
score can rise with no test change. Such a mutant can now also end as
|
|
207
|
+
`error` or `timeout`. That counts toward the `--threshold` completeness
|
|
208
|
+
check.
|
|
209
|
+
- Mutineer reads its files and pipes as UTF-8 when `LANG` and `LC_ALL` are
|
|
210
|
+
unset (#303). Before, a non-ASCII character in a source file, a baseline, a
|
|
211
|
+
coverage cache, a triage report, `.mutineer.yml` (for `migrate`),
|
|
212
|
+
`db/schema.rb`, a `--since` diff, test-command output, or a daemon message
|
|
213
|
+
stopped the run with "invalid byte sequence in US-ASCII" (exit 2) or a
|
|
214
|
+
backtrace (exit 1). An invalid byte in test-command output now shows as
|
|
215
|
+
`\xNN`. Non-ASCII file and folder names are not covered yet (#346).
|
|
216
|
+
- **Upgrade note for `--test-command` with a custom failure exit code (#292).**
|
|
217
|
+
An RSpec setup with `--failure-exit-code 3` now scores every kill as
|
|
218
|
+
`error`. Use the default exit code, or a wrapper that maps a failure to
|
|
219
|
+
exit 1. An `error` verdict leaves the score, so the score can drop, and more
|
|
220
|
+
`error` verdicts can fail a `--threshold` run through the existing no-verdict
|
|
221
|
+
check. A mutant that only trips a SimpleCov `minimum_coverage` gate now
|
|
222
|
+
scores `survived` when the output has SimpleCov's gate message and a
|
|
223
|
+
passing Minitest or RSpec summary line. Without a summary line that
|
|
224
|
+
Mutineer recognizes, the gate's exit scores `error`, which leaves the
|
|
225
|
+
score. Either way the old false kill is gone, so the score can drop. A `--baseline` made before
|
|
226
|
+
this change can report new survivors and errors: make a new baseline after
|
|
227
|
+
the upgrade.
|
|
228
|
+
- **A `--test-command` exit other than 1 no longer counts as a kill (#292).**
|
|
229
|
+
Only exit 1 scores `killed`, the same rule as the in-process backend. One
|
|
230
|
+
exception: a SimpleCov gate exit whose summary reports a failing test also
|
|
231
|
+
scores `killed`. Any other non-zero exit scores `error`, such as a wrapper's
|
|
232
|
+
137 after an out-of-memory kill.
|
|
233
|
+
- **A load crash under `--test-command` scores `error` (#292).** When the clean
|
|
234
|
+
run prints a Minitest or RSpec summary line, an exit 1 also needs a failing
|
|
235
|
+
test in that line. Code under test that calls `exit 1` or `abort` while the
|
|
236
|
+
default Minitest reporter runs its tests still scores `killed`, as in
|
|
237
|
+
process. Under RSpec or minitest-reporters, that case now scores `error`.
|
|
238
|
+
When a SimpleCov `minimum_coverage` gate replaces the exit code, the summary
|
|
239
|
+
line decides: a failing test is a kill, and a passing run is `survived`, as
|
|
240
|
+
in process. When the clean run
|
|
241
|
+
prints no summary line, Mutineer warns once and keeps the exit-code rule.
|
|
242
|
+
|
|
243
|
+
- **`--output .mutineer/run.json` on a fresh checkout.** The documented
|
|
244
|
+
report path no longer exits 2 when `.mutineer/` does not exist yet. The
|
|
245
|
+
output check now creates the cache directory (`cache_dir`) when it is the
|
|
246
|
+
missing parent. Any other missing directory still exits 2. This is a CLI
|
|
247
|
+
fix: the GitHub Action's `output` input still needs an existing directory
|
|
248
|
+
(#270, #340).
|
|
249
|
+
|
|
250
|
+
- **`--output` can no longer replace its own baseline.** When `--output`
|
|
251
|
+
and `--baseline` or `baseline:` in `.mutineer.yml` name the same file
|
|
252
|
+
(also through a symlink or hard link), a real `mutineer run` exits 2
|
|
253
|
+
before the run, with the GitHub Action's error adapted for the CLI. A dry run
|
|
254
|
+
still works because it writes no report. Before, a
|
|
255
|
+
failing run wrote its report over the baseline, and the next run passed
|
|
256
|
+
against it (#281).
|
|
257
|
+
|
|
258
|
+
- **`--threshold` compares the exact score, not the rounded one (#280).** The
|
|
259
|
+
score is rounded to one decimal for display, and the gate used to compare that
|
|
260
|
+
rounded value. A run with 1999 of 2000 mutants killed showed 100.0% and passed
|
|
261
|
+
`--threshold 100` with a survivor. The exit code, the Action, and the PASSED or
|
|
262
|
+
FAILED line now use the exact counts. When the rounded score points the other
|
|
263
|
+
way, the PASSED or FAILED line gives the counts and says that the score is
|
|
264
|
+
rounded.
|
|
265
|
+
|
|
266
|
+
- **The Minitest clean check now runs the tests in the order the mutant runs
|
|
267
|
+
use (#282).** Since 1.1.0, each mutant run used seed `1`, but coverage
|
|
268
|
+
capture, the clean checks and the `--daemon` mutant runs used a random
|
|
269
|
+
seed. An order-dependent suite could pass the clean check and then fail in
|
|
270
|
+
every mutant run, so each mutant scored a false `killed`. Every Minitest
|
|
271
|
+
run that Mutineer starts itself (not `--test-command`, see #338) now uses seed `1`,
|
|
272
|
+
or `SEED` when the environment sets it. Such a suite now stops the run
|
|
273
|
+
with "the unmutated suite is not green". The message names the configured
|
|
274
|
+
seed or default and notes that test helpers can change it. Capture, clean
|
|
275
|
+
checks and mutant runs all read `SEED` after loading the tests and helpers.
|
|
276
|
+
Check the listed test files with the same seed and helpers to investigate
|
|
277
|
+
the order-dependent test. A coverage cache from another
|
|
278
|
+
`SEED` in Mutineer's environment or an older version is captured again. A
|
|
279
|
+
`--coverage-from` capture from another `SEED` or an older version is
|
|
280
|
+
rejected with exit 2: capture it again with this version. The internal constant
|
|
281
|
+
`MinitestIntegration::STOP_AT_FIRST_FAILURE_SEED` is now `PINNED_SEED`.
|
|
282
|
+
One gap stays: when a mutant runs several covering test files, their test
|
|
283
|
+
classes can still run in a different order than in the combined clean
|
|
284
|
+
check. RSpec does not change.
|
|
285
|
+
|
|
286
|
+
- **`--daemon` SQLite workers load the schema file the app names (#317).**
|
|
287
|
+
The daemon always loaded `db/schema.rb` over an out-of-date worker copy,
|
|
288
|
+
even when `config/database.yml` sets `schema_dump:` to another file. An
|
|
289
|
+
app that keeps its SQLite schema in `db/schema_sqlite.rb` beside a MySQL
|
|
290
|
+
`db/schema.rb` errored every mutant. The booted daemon now asks Rails for
|
|
291
|
+
the file: `SCHEMA` when it is set, else `schema_dump:`, else
|
|
292
|
+
`db/schema.rb`. With `structure.sql`, a missing file, or
|
|
293
|
+
`schema_dump: false` (unless `SCHEMA` is set) it loads no schema, even
|
|
294
|
+
when a `db/schema.rb` exists: the worker keeps its copy of the test
|
|
295
|
+
database. A Postgres worker is a copy of the test database, so it was not
|
|
296
|
+
affected. MySQL already asked Rails for the file. It now also reads the
|
|
297
|
+
database's own `schema_format:` (Rails 8.1) and a string format such as
|
|
298
|
+
`"sql"`, the same as SQLite.
|
|
299
|
+
|
|
300
|
+
- **No `DATABASE_URL` password in load errors and mutineer's own messages (#288).**
|
|
301
|
+
A load error in the tool process (an
|
|
302
|
+
in-process `--boot` or `--rails` run, a source file, or a `require:`
|
|
303
|
+
file) no longer prints the raw password or a backtrace. The run prints a
|
|
304
|
+
`mutineer: cannot load <path>: ...` message (a `SyntaxError` keeps its code
|
|
305
|
+
excerpt) and exits 1. By default, a load error adds a hint to re-run with `--verbose`;
|
|
306
|
+
verbose output gives a redacted backtrace and cause chain. CLI error messages, the
|
|
307
|
+
fork child's error line, the worker crash text in the report, the daemon's
|
|
308
|
+
stderr lines (except bare passwords known only to the app after boot),
|
|
309
|
+
the failing test report and any load error from the
|
|
310
|
+
clean-suite and coverage checks, and the `--verbose` test command output
|
|
311
|
+
are redacted too. Output that app code or a test writes straight to
|
|
312
|
+
stderr in a fork or subprocess is not filtered (#341 tracks this and the
|
|
313
|
+
other known gaps). A `password=` query parameter in DATABASE_URL is now
|
|
314
|
+
hidden too.
|
|
315
|
+
Daemon stderr is now copied line by line as UTF-8, so an invalid byte
|
|
316
|
+
shows as U+FFFD. Output without a newline waits for a newline or EOF, so
|
|
317
|
+
credentials split across writes can be redacted together. A password that
|
|
318
|
+
equals the username no longer blanks the same text elsewhere (for example
|
|
319
|
+
`role "postgres"`). Each password form of 3 characters or fewer is hidden
|
|
320
|
+
only when no ASCII letter, digit or underscore touches either edge.
|
|
321
|
+
The limit applies separately to raw, decoded and escaped forms: `%61%62`
|
|
322
|
+
is still replaced inside longer text, while its decoded `ab` is matched
|
|
323
|
+
only as a whole token. A longer form is hidden wherever it appears,
|
|
324
|
+
and the password inside a URL is always
|
|
325
|
+
hidden.
|
|
326
|
+
|
|
327
|
+
- **`--strategy redefine` and path-relative methods.** Redefine now loads
|
|
328
|
+
the mutated method from a hidden file beside the source, as reload does.
|
|
329
|
+
It used the system temp directory before, so `__dir__` and
|
|
330
|
+
`require_relative` in the method pointed there, and every mutant of such
|
|
331
|
+
a method was falsely killed (#279). Redefine is also the default under
|
|
332
|
+
`--rails` without `--daemon`. When a source directory is not writable,
|
|
333
|
+
redefine still uses the system temp directory there, as before, and
|
|
334
|
+
the run warns that path-relative methods there can be falsely killed
|
|
335
|
+
(tracked in #339).
|
|
336
|
+
|
|
337
|
+
- `--test-command` now sets `DISABLE_BOOTSNAP_COMPILE_CACHE=1` for the test
|
|
338
|
+
command. Mutineer writes each mutant over the source file, and Bootsnap keys
|
|
339
|
+
its compile cache on path, size and whole-second mtime. Two same-size mutants
|
|
340
|
+
written in the same second could load the first one's cached code, so a
|
|
341
|
+
mutant could be scored on code that was not the mutant. Each mutant boot now
|
|
342
|
+
compiles the app again, which is slower. The variable works through
|
|
343
|
+
`require "bootsnap/setup"` on Bootsnap 1.7.0 or later; an app that calls
|
|
344
|
+
`Bootsnap.setup` itself must pass
|
|
345
|
+
`compile_cache_iseq: ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"].nil?`, so only
|
|
346
|
+
mutant runs lose the cache. The `--daemon` path is unchanged (#285).
|
|
347
|
+
|
|
348
|
+
- **A mutant that two operators make is scored once** (#275).
|
|
349
|
+
`literal_mutation` and `string_literal` both turn `"hi"` into `""`, and
|
|
350
|
+
`chain_link` and `call_to_receiver` both turn `node.name.name` into
|
|
351
|
+
`node.name`. Each copy had its own id, so one surviving edit counted twice.
|
|
352
|
+
Now one copy runs: the copy of the operator that comes first in
|
|
353
|
+
`mutineer --list-operators`, whatever the `--operators` order. Only the
|
|
354
|
+
same text counts: a single-quoted `'hi'` still gives two mutants (`""` and
|
|
355
|
+
`''`). Both edits must also start on the same line: in a chain with one
|
|
356
|
+
call per line, the two copies start on different lines, so both are kept,
|
|
357
|
+
and `--since` filters each copy by its own start line.
|
|
358
|
+
- Every kept mutant keeps its id. The dropped copy's id no longer appears
|
|
359
|
+
in reports. Runs that use only one of these operators keep the same
|
|
360
|
+
mutants and ids.
|
|
361
|
+
- If you combine these operators, the mutant count and the score can move
|
|
362
|
+
with no code change, so a `--threshold` or `--baseline` gate can change
|
|
363
|
+
its result. Regenerate a `--baseline` file after upgrading, from a full
|
|
364
|
+
`--format json` run with no `--since`, `--only` or `--fail-fast` (pass
|
|
365
|
+
`--no-since` and `--no-fail-fast` when `.mutineer.yml` sets them). A
|
|
366
|
+
scoped report is refused as a baseline, and a partial one misses
|
|
367
|
+
mutants. In a full (unscoped) comparison, a dropped copy that survived
|
|
368
|
+
is listed as fixed.
|
|
369
|
+
- An ignore entry for one copy's id still ignores only that copy, as for
|
|
370
|
+
the #159 copies. So if you ignore the copy a report shows, the other copy
|
|
371
|
+
then runs and is reported, and it needs its own entry. Keep both entries:
|
|
372
|
+
a run with one operator alone still uses its entry, and
|
|
373
|
+
`mutineer migrate` keeps both as current ids. This also fixes the #159
|
|
374
|
+
copies: an `ignore:` list that names both copies of a repeated edit no
|
|
375
|
+
longer makes `mutineer migrate` exit 1.
|
|
376
|
+
- An old-format ignore entry that matches only a dropped copy gets the
|
|
377
|
+
old-id-format warning and counts in `summary.legacy_id_matches.ignore`,
|
|
378
|
+
so `mutineer migrate` updates it. For the #159 copies this is new: such
|
|
379
|
+
an entry was not counted before, so with one operator the count can go
|
|
380
|
+
up, and the warning can name an id that no report shows.
|
|
381
|
+
- Within a method, mutants now run and list in the `--list-operators`
|
|
382
|
+
order. This applies to any run whose `--operators` or `operators:` list
|
|
383
|
+
is in another order, so `--dry-run` lists and `--fail-fast` can differ.
|
|
384
|
+
- **A nested method's mutant under `--strategy redefine`.** Redefine now
|
|
385
|
+
also loads the mutated method into each method that encloses it. Before,
|
|
386
|
+
a call to the outer method defined the original inner method again, so
|
|
387
|
+
the mutant could falsely survive (#290). For a method nested in other
|
|
388
|
+
methods, redefine now gives the same verdict as reload when a test calls
|
|
389
|
+
an enclosing method by its own name. Mutant ids do not change, so
|
|
390
|
+
baselines and `ignore:` entries stay valid. Scores under redefine can
|
|
391
|
+
rise, and an `ignore:` entry added only to hide such a survivor can now
|
|
392
|
+
be removed. A method defined inside a non-builder block (for example
|
|
393
|
+
inside `define_method`), inside a `module_function` method, inside
|
|
394
|
+
`class << self` in a method, or called through an alias of the outer
|
|
395
|
+
method can still falsely survive under redefine. An enclosing method
|
|
396
|
+
that a memoizer or decorator wraps after its `def` loses that wrapper
|
|
397
|
+
when redefine loads it, so its verdict can differ from reload. Use
|
|
398
|
+
`--strategy reload` for those cases.
|
|
399
|
+
|
|
400
|
+
### Changed
|
|
401
|
+
|
|
402
|
+
- **A release waits for a maintainer.** The Monday release-PR schedule is
|
|
403
|
+
gone, and pushing a `vX.Y.Z` tag leaves the gem unpublished. A human
|
|
404
|
+
collaborator with maintain or admin prepares a release PR by hand, tags by
|
|
405
|
+
hand, then runs the Release workflow on that tag with `confirm=publish`.
|
|
406
|
+
A required reviewer approves the `rubygems` GitHub environment before the
|
|
407
|
+
gem is pushed. A new Release run refuses to start the publish when that
|
|
408
|
+
environment is missing, has no required reviewers, or lets admins bypass
|
|
409
|
+
them. Re-running only the publish job of an earlier run does not repeat
|
|
410
|
+
that check. The publish job builds the commit that was dispatched, and it
|
|
411
|
+
stops before `gem push` if the tag moved while the job waited for
|
|
412
|
+
approval. The job no longer moves the floating major tag (`v1`). When the
|
|
413
|
+
release is the newest stable tag in its line, the job prints the command
|
|
414
|
+
and a maintainer runs it, so a ruleset can keep that tag for maintainers.
|
|
415
|
+
For a patch on an older line, it prints no command.
|
|
416
|
+
- **Load failures now use the runtime-error exit code (#288).** An
|
|
417
|
+
`ArgumentError` or `SystemCallError` raised while a boot file, a source
|
|
418
|
+
file or a `.mutineer.yml` `require:` file loads now exits 1 (runtime
|
|
419
|
+
error), instead of 2 (usage error). A missing boot or `require:` file now
|
|
420
|
+
exits 2 (usage error) with a one-line message. Before, it crashed with a
|
|
421
|
+
raw `LoadError` and a backtrace, and exited 1. With `--daemon` it is
|
|
422
|
+
checked before the daemon starts, so it exits 2 there too. Both moves bring
|
|
423
|
+
the code in line with the exit-code table in STABILITY.md, which already
|
|
424
|
+
lists a bad path as 2 and a runtime error as 1.
|
|
425
|
+
- **`call_to_receiver` now mutates last-call conversions such as `to_s` and
|
|
426
|
+
`to_i`.** `m[1].to_i` now gives the mutant `m[1]`. Before, the operator skipped the
|
|
427
|
+
same names as `chain_link`. Now it skips only `new`, `dup`, `clone`,
|
|
428
|
+
`freeze`, `itself` and `to_set`. `chain_link` does not change. Only runs
|
|
429
|
+
that list `call_to_receiver` in `--operators` (or `operators:`) change: they
|
|
430
|
+
get more mutants (113 more on Mutineer's own `lib/`). If you gate such a run
|
|
431
|
+
with `--threshold` or `--baseline`, expect new survivors: write a new
|
|
432
|
+
full-scope baseline with `mutineer run ... --no-since --no-fail-fast
|
|
433
|
+
--format json`, with no `--only`. A `to_s` mutant on a value that is already
|
|
434
|
+
a String is equivalent: suppress it with
|
|
435
|
+
`# mutineer:disable-line call_to_receiver` or an `ignore:` entry. (#274)
|
|
436
|
+
- Explain mutation testing before setup, with a runnable beginner guide and a
|
|
437
|
+
first run on one file. Simplify wording across the website and clarify Rails
|
|
438
|
+
setup, CI baselines, and reasons for ignoring a mutant.
|
|
439
|
+
|
|
9
440
|
## [1.7.0] - 2026-10-23
|
|
10
441
|
|
|
11
442
|
### Added
|
|
@@ -1397,6 +1828,7 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
|
|
|
1397
1828
|
- `.mutineer.yml` configuration (CLI > config > default precedence).
|
|
1398
1829
|
- Byte-correct source handling for multibyte (UTF-8) sources.
|
|
1399
1830
|
|
|
1831
|
+
[1.8.0]: https://github.com/davidteren/mutineer/releases/tag/v1.8.0
|
|
1400
1832
|
[1.7.0]: https://github.com/davidteren/mutineer/releases/tag/v1.7.0
|
|
1401
1833
|
[1.6.0]: https://github.com/davidteren/mutineer/releases/tag/v1.6.0
|
|
1402
1834
|
[1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
|