mutineer-mongoid 2.0.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 +7 -0
- data/CHANGELOG.md +1194 -0
- data/LICENSE +21 -0
- data/README.md +659 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/baseline.rb +185 -0
- data/lib/mutineer/changed_lines.rb +136 -0
- data/lib/mutineer/child_stdout.rb +35 -0
- data/lib/mutineer/cli.rb +681 -0
- data/lib/mutineer/config.rb +372 -0
- data/lib/mutineer/coverage_map.rb +1366 -0
- data/lib/mutineer/daemon_backend.rb +351 -0
- data/lib/mutineer/daemon_client.rb +407 -0
- data/lib/mutineer/daemon_server.rb +351 -0
- data/lib/mutineer/external_backend.rb +288 -0
- data/lib/mutineer/file_swap.rb +194 -0
- data/lib/mutineer/isolation.rb +321 -0
- data/lib/mutineer/job_plan.rb +352 -0
- data/lib/mutineer/kill_channel.rb +284 -0
- data/lib/mutineer/kill_matrix.rb +88 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
- data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
- data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
- data/lib/mutineer/minitest_integration.rb +137 -0
- data/lib/mutineer/mongoid_failures.rb +82 -0
- data/lib/mutineer/mongoid_worker_db.rb +97 -0
- data/lib/mutineer/mutant_id.rb +126 -0
- data/lib/mutineer/mutation.rb +27 -0
- data/lib/mutineer/mutator_registry.rb +109 -0
- data/lib/mutineer/mutators/arithmetic.rb +35 -0
- data/lib/mutineer/mutators/array_literal.rb +44 -0
- data/lib/mutineer/mutators/base.rb +105 -0
- data/lib/mutineer/mutators/boolean_connector.rb +52 -0
- data/lib/mutineer/mutators/boolean_literal.rb +56 -0
- data/lib/mutineer/mutators/chain_link.rb +132 -0
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +35 -0
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/condition_negation.rb +49 -0
- data/lib/mutineer/mutators/literal_mutation.rb +51 -0
- data/lib/mutineer/mutators/negation_removal.rb +44 -0
- data/lib/mutineer/mutators/operand_removal.rb +58 -0
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/range_literal.rb +54 -0
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +72 -0
- data/lib/mutineer/mutators/safe_navigation.rb +76 -0
- data/lib/mutineer/mutators/statement_removal.rb +33 -0
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/pairing.rb +298 -0
- data/lib/mutineer/parser.rb +45 -0
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/project.rb +506 -0
- data/lib/mutineer/project_path.rb +57 -0
- data/lib/mutineer/rails_worker_db.rb +212 -0
- data/lib/mutineer/reporter.rb +926 -0
- data/lib/mutineer/result.rb +240 -0
- data/lib/mutineer/runner.rb +418 -0
- data/lib/mutineer/statement_lines.rb +183 -0
- data/lib/mutineer/subject.rb +38 -0
- data/lib/mutineer/test_runners/minitest.rb +22 -0
- data/lib/mutineer/test_runners/rspec.rb +330 -0
- data/lib/mutineer/test_runners.rb +30 -0
- data/lib/mutineer/version.rb +6 -0
- data/lib/mutineer/worker_pool.rb +147 -0
- data/lib/mutineer.rb +48 -0
- metadata +143 -0
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [2.0.0] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
- Rename the gem, repository, and action to `mutineer-mongoid`.
|
|
10
|
+
- Document inherited Minitest/SQLite support alongside Mongoid/RSpec support.
|
|
11
|
+
- Preserve the `mutineer` command and the v1.7.0 release for existing users.
|
|
12
|
+
|
|
13
|
+
## [1.7.0] - 2026-10-15
|
|
14
|
+
|
|
15
|
+
- Add Mongoid 9 / RSpec daemon execution with a disposable database per child.
|
|
16
|
+
- Preserve boot-time documents and indexes, and clean databases after timeouts.
|
|
17
|
+
- Package the fork as `mutineer-gaia`, retaining the existing executable.
|
|
18
|
+
- Ship a composite GitHub action with a daemon input and a bundled-source installer.
|
|
19
|
+
- Attach the gem and checksum to GitHub releases.
|
|
20
|
+
|
|
21
|
+
## [Unreleased]
|
|
22
|
+
|
|
23
|
+
## [1.6.0] - 2026-10-08
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **An `unplaceable` mutant status.** Under `--strategy redefine`, a mutant in
|
|
28
|
+
a method whose class or module has no constant name is not run. It is left
|
|
29
|
+
out of the score and, unlike `uncapturable`, out of `no_verdict`, so it does
|
|
30
|
+
not fail `--threshold`. The human report gives it an `Unplaceable:` row, the
|
|
31
|
+
HTML report a count, and the JSON report `summary.unplaceable` and an
|
|
32
|
+
`unplaceable[]` list; `schema_version` is `1.7`.
|
|
33
|
+
- **The agent skill installs with `gh skill` and `npx skills` (#125).** It
|
|
34
|
+
moved from `docs/skill.md` to `skills/mutineer/SKILL.md`, the layout of the
|
|
35
|
+
Agent Skills specification, so `gh skill install davidteren/mutineer
|
|
36
|
+
mutineer` finds it and `npx skills add davidteren/mutineer --skill mutineer`
|
|
37
|
+
installs one `SKILL.md` instead of the whole `docs/` site. The site build
|
|
38
|
+
copies that file to `skill.md`, so its URL still works. The skill's
|
|
39
|
+
description now says when to use it, and the README shows how to install it.
|
|
40
|
+
- **A `ran_at_load` mutant status** (#187). A mutant on a method-body line
|
|
41
|
+
that ran while the app booted or its class loaded gets this status when it
|
|
42
|
+
survives, or when only the load ran the line. A kill stays a kill. It is
|
|
43
|
+
left out of the score and out of `no_verdict`, so it does not fail
|
|
44
|
+
`--threshold`. The human report gives it a `Ran at load:` row that points
|
|
45
|
+
to `--test-command` to verify these mutants, the HTML report a count, and
|
|
46
|
+
the JSON report `summary.ran_at_load` and a `ran_at_load[]` list, within
|
|
47
|
+
schema `1.7`.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Mutants no longer run under Coverage with `--boot` or `--rails` (#228).** Coverage
|
|
52
|
+
starts before the app boots so the coverage map can be built, and it used to
|
|
53
|
+
keep running in every mutant fork after that, though nothing read it. It is
|
|
54
|
+
now suspended once the map is built, from a fresh capture or from the cache.
|
|
55
|
+
Coverage that a host process started before the run is left running.
|
|
56
|
+
On a small app whose test calls tiny methods in a loop, the CPU time of a
|
|
57
|
+
whole in-process `--boot` run fell from 0.76s to 0.57s (median of five). On
|
|
58
|
+
the Rails fixture app the run is mostly boot, and the change is within noise
|
|
59
|
+
(1.61s before, 1.60s after). `--daemon` runs its mutants in worker daemons
|
|
60
|
+
that never start Coverage, so its time does not change (3.48s before, 3.44s
|
|
61
|
+
after); the map-building daemon now suspends Coverage after the map too.
|
|
62
|
+
- **A mutant runs its paired test files first, then the cheapest (#203).** The
|
|
63
|
+
covering test files used to run in the order the coverage map stored them,
|
|
64
|
+
so a mutant that one fast file kills could still score `timeout` when slow
|
|
65
|
+
files ran first. Now the files that pairing finds for the source run first,
|
|
66
|
+
then the rest from fastest to slowest by the time each file took during
|
|
67
|
+
coverage capture, less the fastest file's time (the startup cost each
|
|
68
|
+
capture pays). The order is fixed for a given cache. Costs compare in
|
|
69
|
+
doubling buckets, so files of close cost keep their path order when a
|
|
70
|
+
rebuilt cache measures them again; a file near a bucket edge can still move,
|
|
71
|
+
so keep `.mutineer/` between CI runs for the most stable `--baseline`. The
|
|
72
|
+
order applies to Minitest (after the seeded class shuffle, with
|
|
73
|
+
`parallelize_me!` classes still last) and to RSpec. `--daemon` runs every
|
|
74
|
+
covering test with no stop, so the order does not change its verdicts. The
|
|
75
|
+
coverage cache now saves these timings, so a cache from an earlier version
|
|
76
|
+
rebuilds once. A complete run gives the same verdicts and `--matrix` rows,
|
|
77
|
+
but the new order can change which mutants time out, so a `--baseline` gate
|
|
78
|
+
can see a one-time change on upgrade.
|
|
79
|
+
- **Job selection moved from `Mutineer::Runner` to a new `Mutineer::JobPlan`
|
|
80
|
+
module (#75).** The in-process runner, the `--daemon` backend and
|
|
81
|
+
`--dry-run` share it, so each file now requires what it uses and no
|
|
82
|
+
`require_relative` cycle exists in `lib/mutineer`. The moved methods are
|
|
83
|
+
`collect_jobs`, `filter_since`, `coverage_selection`, `ran_at_load?`,
|
|
84
|
+
`load_verdict`, `abort_if_unclean!`, `test_load_roots`, `source_dirs`,
|
|
85
|
+
`sweep_orphans`, `suppress_map`, `suppressed?` and `result_keys`. These were
|
|
86
|
+
internal methods, so `Runner` keeps no copies. Runs, reports and caches do
|
|
87
|
+
not change.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- **An endless method that only the tests call is no longer `no_coverage`
|
|
92
|
+
under `--rails`, `--boot` or `--daemon` (#209).** Ruby counts no line when
|
|
93
|
+
an endless method such as `def half(x) = x / 2` runs, only its `def` line
|
|
94
|
+
when it is defined. The boot defines it before any test runs, so the forked
|
|
95
|
+
coverage capture never saw that line run. Coverage now also
|
|
96
|
+
counts methods (`Coverage.start(lines: true, methods: true)`), and a test
|
|
97
|
+
that calls such a method covers its `def` line. Coverage caches from earlier
|
|
98
|
+
versions rebuild once.
|
|
99
|
+
- **`module_function :name` in a reopened module inside `class << self`
|
|
100
|
+
promotes the method from the earlier opening (#216).** Ruby treats both
|
|
101
|
+
openings as one module, but Mutineer matched them only within one body, so
|
|
102
|
+
it named the method `#<Class:App>::M#c` instead of `#<Class:App>::M.c`.
|
|
103
|
+
The two openings now match by the module's full name. A module whose name
|
|
104
|
+
Mutineer cannot be sure of still matches only within its own body: a path
|
|
105
|
+
such as `self::X` or `Foo::X` written directly inside `class << self`, a
|
|
106
|
+
name under an anonymous class, or a module in a `class << self` inside a
|
|
107
|
+
method or a `class_eval` block. A `module self::X` in a block, or a
|
|
108
|
+
`self::X = ...` in a `class_eval` block, matches only openings in the same
|
|
109
|
+
block. A `self::X` like these now also stays apart from a
|
|
110
|
+
module of the same name outside `class << self`, where a `module_function`
|
|
111
|
+
in that module used to promote its methods.
|
|
112
|
+
- **A `module self::X` or `class self::X` in a block is `unplaceable` under
|
|
113
|
+
`--strategy redefine` (#229).** In a block, `self` is not always the
|
|
114
|
+
enclosing class: in `Other.class_eval do` it is `Other`, and in
|
|
115
|
+
`Foo = Class.new do` it is the new class. Mutineer gave these modules a
|
|
116
|
+
known owner on the enclosing namespace, so redefine loaded the mutant onto
|
|
117
|
+
a module the tests never use, and the mutant could falsely survive or
|
|
118
|
+
score `error`. Such a module, everything nested in it, and a
|
|
119
|
+
`self::X = ...` in a block that does not build a class now have
|
|
120
|
+
their owner unknown: redefine reports their mutants as `unplaceable`, and
|
|
121
|
+
`--strategy reload` still runs them. In a `Data.define`, `Struct.new`,
|
|
122
|
+
`Class.new` or `Module.new` block, the module is now named under the
|
|
123
|
+
builder's constant (`Outer::Foo::X#c`, was `Outer::X#c`), so the subject
|
|
124
|
+
names and mutant ids of those methods change. `self::X` directly in a class
|
|
125
|
+
or module body is unchanged.
|
|
126
|
+
|
|
127
|
+
- **`--daemon` worker databases start as a copy of the test database
|
|
128
|
+
(#222).** Each worker slot's SQLite database used to start from
|
|
129
|
+
`db/schema.rb` alone. Rows the daemon wrote while it booted, in
|
|
130
|
+
initializers or `--require` files, were only in the base test database, so
|
|
131
|
+
a test that read them passed in-process but failed under `--daemon`. On its
|
|
132
|
+
first use, a worker database is now a copy of the base test database (schema
|
|
133
|
+
and rows, made with the SQLite online backup API). The worker still loads
|
|
134
|
+
`db/schema.rb` when the copy's schema differs from it (another schema
|
|
135
|
+
version, or another or no `schema_sha1` checksum in `ar_internal_metadata`),
|
|
136
|
+
for example when the test database is empty, out of date, or set up with a
|
|
137
|
+
plain `load "db/schema.rb"`. That load drops the
|
|
138
|
+
copied rows of the tables it defines.
|
|
139
|
+
- **Standalone coverage capture loads the `--require` files (#217).** A run
|
|
140
|
+
without `--boot` requires the sources and then each `--require` (or
|
|
141
|
+
`require:`) file before it forks the mutants, but the coverage capture
|
|
142
|
+
loaded only the sources. A source method that a `--require` file called
|
|
143
|
+
while it loaded was then not seen as run at load, so its mutant was a false
|
|
144
|
+
`survived` instead of `ran_at_load`. The capture and the clean-suite check
|
|
145
|
+
now load the `--require` files after the sources. With or without
|
|
146
|
+
`--boot`, the coverage cache rebuilds when one of them changes, is added,
|
|
147
|
+
is removed, or moves in the load order.
|
|
148
|
+
- **`--daemon` loads the `--require` files (#220).** The daemon booted the app
|
|
149
|
+
but never loaded the `--require` (or `require:`) files, so a test that
|
|
150
|
+
needs one failed under `--daemon` and passed in-process. A source method
|
|
151
|
+
that such a file called while it loaded was also not seen as run at load,
|
|
152
|
+
so its mutant could be a false `survived` instead of `ran_at_load`. The
|
|
153
|
+
daemon now loads the files after the app boots, as an in-process run does,
|
|
154
|
+
and the daemon's coverage cache rebuilds when one of them changes.
|
|
155
|
+
- **The release workflow publishes only stable tags** (#161). Its trigger,
|
|
156
|
+
`v*.*.*`, also matches a prerelease tag such as `v1.3.1-rc1`, and the only
|
|
157
|
+
guard checked that the tag matched `Mutineer::VERSION`. A prerelease tag
|
|
158
|
+
could then publish the gem and move the floating major tag (`v1`) that
|
|
159
|
+
Action users pin. The workflow now stops with a clear error before any
|
|
160
|
+
publish step unless the tag is a stable `vMAJOR.MINOR.PATCH`.
|
|
161
|
+
- **Coverage capture keeps to `capture_timeout` from start to finish.** Under
|
|
162
|
+
`--boot` and `--daemon`, a hung test file ignored the timeout and blocked
|
|
163
|
+
the run; it is now skipped as "timed out", like a standalone capture. A
|
|
164
|
+
process that a test started and left running could keep the result pipe
|
|
165
|
+
open and make a boot-mode capture wait for it to exit. Each capture now runs
|
|
166
|
+
in its own process group, and a timeout stops the whole group, not only the
|
|
167
|
+
test process. A capture whose test finished just before the deadline could
|
|
168
|
+
also lose its valid result and report "invalid coverage output"; it now
|
|
169
|
+
gets a short grace period to read the result. (#101, #129)
|
|
170
|
+
- **`--daemon` no longer waits forever on a daemon that stops answering**
|
|
171
|
+
(#101). A mutant whose verdict has not arrived 30 seconds after its own
|
|
172
|
+
timeout is scored `error`, and the daemon is killed and respawned, as after
|
|
173
|
+
a crash. A boot that has not finished after 600 seconds ends the run with a
|
|
174
|
+
message that says so (it is not retried by a second daemon), and shutdown no longer waits on a stuck daemon.
|
|
175
|
+
|
|
176
|
+
- **A method in a `Data.define`, `Struct.new`, `Class.new` or `Module.new`
|
|
177
|
+
block belongs to the class or module the block builds.** In
|
|
178
|
+
`class App; Argo = Data.define(:url) do def self.load = ...; end; end` the
|
|
179
|
+
subject is now `App::Argo.load`, not `App.load`; at the top level it was
|
|
180
|
+
`.load`, with no owner. Under `--strategy redefine` (the `--rails` default)
|
|
181
|
+
the mutated method was loaded into the wrong class, so the real method never
|
|
182
|
+
changed and every such mutant falsely survived. The class or module is
|
|
183
|
+
named after the constant it is assigned to with `=`, `||=` or `&&=`, through
|
|
184
|
+
parentheses or `begin`, resolved as Ruby resolves the assignment: `X` in the
|
|
185
|
+
current namespace, `::X` and any path at the top level from `Object`,
|
|
186
|
+
`self::X` under the current class. When it cannot be named statically (no
|
|
187
|
+
constant, a relative path such as `User::Permission` inside a namespace, or
|
|
188
|
+
a constant assigned in `class << self`), redefine reports its mutants with
|
|
189
|
+
a new status, `unplaceable`, rather than load them onto the wrong class;
|
|
190
|
+
reload still runs them. `module_function :name` in a `Module.new` block
|
|
191
|
+
promotes that module's method. Mutant ids for these subjects change:
|
|
192
|
+
regenerate `ignore:` entries and baselines that name them.
|
|
193
|
+
- **The automated release PR gets its required CI checks without a token
|
|
194
|
+
secret (#85).** `release-pr.yml` pushes the release branch with the default
|
|
195
|
+
`GITHUB_TOKEN`, which starts no other workflow, so the four required checks
|
|
196
|
+
never ran and the PR stayed blocked until a person closed and reopened it.
|
|
197
|
+
The job now dispatches `ci.yml` on the release branch (CI gained a
|
|
198
|
+
`workflow_dispatch` trigger), and those runs report the required checks on
|
|
199
|
+
the PR's head commit. With a `RELEASE_PR_TOKEN` secret set, the push
|
|
200
|
+
triggers CI as before and no dispatch is made.
|
|
201
|
+
- **Code that runs while the app boots or a class loads no longer gives a false
|
|
202
|
+
`survived` or `no_coverage`** (#187). A class body such as
|
|
203
|
+
`ALL = [price(3)].freeze` runs `price` before any mutant is applied. Under
|
|
204
|
+
`--strategy redefine` (the `--rails` default) the load never runs again, so
|
|
205
|
+
a test that checks `ALL` saw the original value and the mutant falsely
|
|
206
|
+
survived. Under `--rails` or `--boot` no test was credited with lines that
|
|
207
|
+
ran during boot, so those mutants were falsely `no_coverage`. Mutineer now
|
|
208
|
+
reads the lines that ran at load (from the boot, or from requiring the
|
|
209
|
+
sources in standalone capture) and reports these mutants as `ran_at_load`,
|
|
210
|
+
under both strategies and with `--daemon`: reload runs the file's class body
|
|
211
|
+
again but not an initializer or another file's code, so a survivor there is
|
|
212
|
+
not trusted either. Under `--boot` with redefine, a
|
|
213
|
+
lazily loaded class (Zeitwerk, `autoload`) is loaded before the boot lines
|
|
214
|
+
are read, so its class body counts as load too. A baseline survivor that is
|
|
215
|
+
now `ran_at_load` is listed as fixed, not as a regression. Standalone
|
|
216
|
+
coverage caches from earlier versions rebuild once. A one-line or endless
|
|
217
|
+
`def` keeps its body on the `def` line, which Ruby counts when the method is
|
|
218
|
+
defined, so Mutineer reads the method's call count at load for it instead
|
|
219
|
+
(#209).
|
|
220
|
+
- **A boot file that prints to stdout no longer breaks the daemon handshake
|
|
221
|
+
(#102).** Output from `puts`, `STDOUT`, `$stdout`, a direct fd 1 write, or a
|
|
222
|
+
subprocess reached the JSON channel before the ready message, so the daemon
|
|
223
|
+
failed with "daemon exited before the handshake". The daemon now keeps a
|
|
224
|
+
private copy of its original stdout for the protocol and points the app's
|
|
225
|
+
stdout at stderr, where that output stays visible.
|
|
226
|
+
- **A class or module opened inside `class << self` belongs to the singleton
|
|
227
|
+
class (#208).** In `class App; class << self; class Q; def q1 ...` the
|
|
228
|
+
subject was `App::Q.q1`, a class method of a constant Ruby never defines:
|
|
229
|
+
`Q` is `App.singleton_class::Q`, and `q1` is an instance method. It is now
|
|
230
|
+
`#<Class:App>::Q#q1` with no known owner, as is a class or module opened in
|
|
231
|
+
a builder block there, and anything nested in either. Inside a builder
|
|
232
|
+
block, `class << self` opens the built class's singleton class, so a class
|
|
233
|
+
or constant there is named under it, for example `#<Class:App::P>::W`. Under
|
|
234
|
+
`--strategy redefine` loading the mutant raised `NameError` and scored
|
|
235
|
+
`error`; redefine now reports these mutants as `unplaceable`, and reload
|
|
236
|
+
still runs them. A compact `class Foo::X` and a top-level `class ::X` or
|
|
237
|
+
`::X = Class.new do` there are named as written (`::X` as `X`, now with
|
|
238
|
+
instance methods) and are owner-unknown too: redefine reopens them without
|
|
239
|
+
the singleton class, so constants their bodies look up through it would not
|
|
240
|
+
resolve. `module_function :name` in a body whose owner is unknown now
|
|
241
|
+
promotes that body's own methods, wherever it is: a `Foo::X = Module.new`
|
|
242
|
+
block's `c` is `Foo::X.c`, not `Foo::X#c`, and a def in an anonymous
|
|
243
|
+
`Class.new` block is no longer promoted by a `module_function` of the module
|
|
244
|
+
around it. Mutant ids for these subjects change: regenerate `ignore:`
|
|
245
|
+
entries and baselines that name them.
|
|
246
|
+
|
|
247
|
+
## [1.5.0] - 2026-10-06
|
|
248
|
+
|
|
249
|
+
### Added
|
|
250
|
+
- **`--timeout SECONDS` and `--capture-timeout SECONDS`**, also `timeout:` and
|
|
251
|
+
`capture_timeout:` in `.mutineer.yml`. They set the in-process per-mutant
|
|
252
|
+
time limit (default 10s) and the coverage-capture time limit (default 120s),
|
|
253
|
+
which were fixed. On a large Rails suite the clean run of the unmutated
|
|
254
|
+
tests took longer than 120s, so every run that offered the whole suite
|
|
255
|
+
stopped as not green, and a mutant on a line many tests cover took longer
|
|
256
|
+
than 10s and was scored `timeout`. `--daemon` keeps its own per-mutant
|
|
257
|
+
limit but uses `--capture-timeout` for its coverage capture;
|
|
258
|
+
`--test-command` uses neither. Either backend warns about a limit it was
|
|
259
|
+
given and does not use.
|
|
260
|
+
- **`--cache-dir DIR`**, also `cache_dir:` in `.mutineer.yml`, sets where the
|
|
261
|
+
coverage cache is written (default `.mutineer`, as before). Two runs from the
|
|
262
|
+
same project root shared `.mutineer/coverage.json` and could overwrite each
|
|
263
|
+
other's map; each can now have its own directory. `--daemon` uses it too.
|
|
264
|
+
A blank value (an unset CI variable) exits 2 rather than writing the cache
|
|
265
|
+
into the project root.
|
|
266
|
+
- **The README states how timeouts affect the score** (#62): a timed-out
|
|
267
|
+
mutant is left out of the score, and counts as a mutant with no verdict
|
|
268
|
+
under `--threshold`.
|
|
269
|
+
- **`--matrix` reports which tests kill each mutant** (`matrix: true` in
|
|
270
|
+
`.mutineer.yml`). Each mutant runs every test in its covering files instead
|
|
271
|
+
of stopping at the first failure, and the child sends each test's outcome to
|
|
272
|
+
the parent as it is recorded. The report names blind tests, which ran in a
|
|
273
|
+
complete row and killed no mutant, and redundant tests, whose every kill
|
|
274
|
+
another test also makes. Redundancy is judged one test at a time, so delete
|
|
275
|
+
redundant tests one at a time. The human report lists up to 20 of each, the
|
|
276
|
+
HTML report all of them, and the JSON report moves to schema `1.5` with a
|
|
277
|
+
`matrix` block that appears only with the flag. A test is its file and its
|
|
278
|
+
id (the example id under RSpec), so examples that share a description stay
|
|
279
|
+
apart and an example whose generated description changes with the mutant
|
|
280
|
+
stays one test. Verdicts, the score and the exit code do not change: each
|
|
281
|
+
mutant gets the verdict a run without the flag gives. After a serial test
|
|
282
|
+
fails, an exit, a crash or the timeout in a later test, class or group
|
|
283
|
+
leaves it `killed`; an end in the code that run still reaches (the failing
|
|
284
|
+
test's class wrapper or group hooks, the suite's own cleanup) and a failure
|
|
285
|
+
in a `parallelize_me!` class keep the exit status. A row is complete only
|
|
286
|
+
when the child's stream arrived in order and every test reported (under
|
|
287
|
+
Minitest the recorder saw every test, under RSpec every planned example),
|
|
288
|
+
so a caught `Interrupt` leaves it incomplete. The human report names up
|
|
289
|
+
to 20 incomplete rows and the HTML and JSON reports all of them; the human
|
|
290
|
+
and HTML reports warn not to delete a blind test until its rows are
|
|
291
|
+
complete.
|
|
292
|
+
Minitest and RSpec 3.3+ are supported, on the in-process backend only;
|
|
293
|
+
`--daemon`, `--test-command`, `--fail-fast` and `--dry-run` exit 2 with it.
|
|
294
|
+
`--no-matrix` and `--no-fail-fast` beat the matching `.mutineer.yml` key,
|
|
295
|
+
so a file that sets one can still run with a flag that conflicts with it.
|
|
296
|
+
- **`no_coverage[]` and `uncapturable[]` name each mutant.** Every entry in
|
|
297
|
+
the JSON report now has `operator`, `token` and `id`, as `ignored[]` has, so
|
|
298
|
+
mutants on one line can be told apart and an `id` can go in `ignore:`. The
|
|
299
|
+
entries are sorted by `(file, line, operator, id)`. `schema_version` is `1.6`.
|
|
300
|
+
|
|
301
|
+
- **The site and README explain "clean-room" and compare Mutineer with
|
|
302
|
+
Mutant**, and the landing page hero has a one-minute explainer video. It
|
|
303
|
+
plays at 55% volume while in view (not with reduced motion; muted until
|
|
304
|
+
the first click or key where the browser requires it), pauses when it scrolls
|
|
305
|
+
away or the tab is hidden, and has an Enlarge button.
|
|
306
|
+
|
|
307
|
+
### Changed
|
|
308
|
+
- **The human report has a `Timeout:` row**, and the score line lists timeouts
|
|
309
|
+
apart from errored mutants. Before, the `Errored:` row and the score line
|
|
310
|
+
added the two together, while the JSON report kept them apart. The HTML
|
|
311
|
+
summary now shows a `timeout` count apart from `errored` too.
|
|
312
|
+
- **A test file given as a source after `--test` exits 2.** `--test` takes one
|
|
313
|
+
file, so in `mutineer run app/x.rb --test spec/a_spec.rb spec/b_spec.rb` the
|
|
314
|
+
second spec became a source to mutate, and the run tested with one spec file
|
|
315
|
+
without a word. Now a source named `*_test.rb` or `*_spec.rb` inside a
|
|
316
|
+
`test/` or `spec/` directory, given with `--test`, is a usage error that says
|
|
317
|
+
to repeat `--test`. The usage line now reads `--test <test> [--test <test>...]`.
|
|
318
|
+
|
|
319
|
+
### Fixed
|
|
320
|
+
- **The RSpec runner keeps what gems configured before the run.** It called
|
|
321
|
+
`RSpec.reset`, which drops the whole configuration. Under `rails: true`, a
|
|
322
|
+
`--boot` file or `require:`, the app's gems load first, so a setting such as
|
|
323
|
+
rspec-retry's `verbose_retry` was gone, and a support file that set it
|
|
324
|
+
raised `NoMethodError`. The run stopped with "the unmutated suite is not
|
|
325
|
+
green". The runner now calls `RSpec.clear_examples`, which keeps the
|
|
326
|
+
configuration. Hooks that gems added now apply too: an example with
|
|
327
|
+
rspec-retry's `retry: 3` runs up to 3 times per mutant, as under `rspec`.
|
|
328
|
+
- **`--since` no longer scores a file that Git ignores** (#168). A file
|
|
329
|
+
outside the index is scored in full, so a new file is not missed. That
|
|
330
|
+
check also caught files that Git ignores on purpose (`.gitignore`,
|
|
331
|
+
`.git/info/exclude` or the global excludes file), such as generated code
|
|
332
|
+
inside a directory run. Those files now get no mutants under `--since`,
|
|
333
|
+
and a warning names each one. Other untracked files are still scored in
|
|
334
|
+
full, and so is a file when Git cannot tell, such as one beyond a symlink or
|
|
335
|
+
inside a submodule.
|
|
336
|
+
- **The human report escapes control characters from the source** (#163).
|
|
337
|
+
A surviving line that held a terminal control byte (for example ESC) was
|
|
338
|
+
printed as that byte, so it could change what the terminal showed. The
|
|
339
|
+
token, the replacement, the diff lines, method names, file paths and the
|
|
340
|
+
kill matrix's test names now print each control character except tab as
|
|
341
|
+
its Ruby escape (`\e`), and a byte that is not valid UTF-8 (a Latin-1
|
|
342
|
+
source) as `\xNN`. The JSON and HTML
|
|
343
|
+
reports already escaped text and do not change.
|
|
344
|
+
- **One edit on a line is scored once** (#159). Some opt-in operators emitted
|
|
345
|
+
two mutants that give the same source: `literal_mutation` on `0` (the
|
|
346
|
+
"change to 1" and "add 1" rules), `negation_removal` on `!!x` (either `!`
|
|
347
|
+
removed), and `chain_link` or `operand_removal` on a repeated part such as
|
|
348
|
+
`a.b.b.c` or `x || x`. Each copy had its own id, so one surviving edit
|
|
349
|
+
counted twice. Now only the first copy on a line runs. Copies that start
|
|
350
|
+
on different lines (a multi-line chain) both stay, so `--since` never
|
|
351
|
+
loses the edit. Ids are assigned before the copy is dropped, so every kept
|
|
352
|
+
mutant keeps its id, and the dropped copy's id simply stops appearing.
|
|
353
|
+
Default runs do not change. If you use these operators with `--baseline`,
|
|
354
|
+
regenerate the baseline after upgrading: the score can move with no code
|
|
355
|
+
change, and in a full (unscoped) comparison a dropped copy that survived
|
|
356
|
+
is listed as fixed. An ignore entry for one copy's id still ignores only
|
|
357
|
+
that copy; the other copy still runs.
|
|
358
|
+
- **`# mutineer:disable-line` inside a string no longer silences a line**
|
|
359
|
+
(#158). The marker was found with a text search, so a string such as
|
|
360
|
+
`"# mutineer:disable-line"` ignored every mutant on its line and left
|
|
361
|
+
them out of the score. Only a real `#` comment counts now. A heredoc or
|
|
362
|
+
regex that holds the text is ignored too.
|
|
363
|
+
- **`./lib/x.rb` and an absolute path pair with a test** (#104). Auto-pairing
|
|
364
|
+
found the test for `lib/calc.rb` but not for `./lib/calc.rb` or the
|
|
365
|
+
absolute path to the same file. It reported "no test found by convention"
|
|
366
|
+
and exited 2. A file argument inside the project is now made relative to
|
|
367
|
+
the project root first, so equivalent spellings (`./`, `..`, absolute)
|
|
368
|
+
pair with the same test and run once. A symlink keeps its own name, so it
|
|
369
|
+
stays a separate source from its target. Reports show that root-relative
|
|
370
|
+
path. A path outside the project stays as typed.
|
|
371
|
+
- **A mutant on another line of a multi-line statement runs its tests**
|
|
372
|
+
instead of being reported as `no_coverage`. Ruby counts one line of a
|
|
373
|
+
statement only. When the mutant's own line has no count, the runner now uses
|
|
374
|
+
the tests that ran the statement that holds the mutation. A heredoc body
|
|
375
|
+
belongs to its statement. This fallback skips a first or last line that also
|
|
376
|
+
holds other code, such as the `def` line, and the body of `x while c` and
|
|
377
|
+
`x until c`. It also gives no tests to a mutant in code of the statement
|
|
378
|
+
that runs only sometimes: a `when` or `in` condition, a `rescue` class
|
|
379
|
+
list, a parameter default, the rescue side of `x rescue y`, the value of
|
|
380
|
+
`||=` or `&&=`, the operand of `defined?`, the right side of `a ||` or
|
|
381
|
+
`a &&`, a branch of `x if c`, `x unless c`, a ternary or `else`, the
|
|
382
|
+
pattern of `v in p` or `v => p`, an
|
|
383
|
+
argument or block of `x&.m(...)`, and the value of `x&.m += v`. Those
|
|
384
|
+
mutants stay `no_coverage`, as before.
|
|
385
|
+
- **A survivor diff for a multi-line mutant applies with `git apply`** (#106).
|
|
386
|
+
The JSON `diff` header always read `@@ -N +N @@`, even when the mutant
|
|
387
|
+
removed two lines and added one, so `git apply` rejected the patch as
|
|
388
|
+
corrupt. The header now carries each side's line count (`@@ -3,2 +3 @@`).
|
|
389
|
+
A one-line diff keeps its old header. A mutant that empties a line now
|
|
390
|
+
shows that empty line as a `+` line. A side whose last line has no final
|
|
391
|
+
newline gets ``. Lines of a CRLF file now keep
|
|
392
|
+
their `\r`, so the patch applies to that file.
|
|
393
|
+
- **Split test files pair with their source** (#87). A source such as
|
|
394
|
+
`app/foo/bar.rb` now also uses `test/foo/bar_upsert_test.rb` and
|
|
395
|
+
`test/foo/bar_guards_test.rb`, together with `test/foo/bar_test.rb` when
|
|
396
|
+
that file exists. The same names under `test/lib/` pair with a `lib/`
|
|
397
|
+
source. Before, only the exact `bar_test.rb` name was tried, so a suite
|
|
398
|
+
split across `bar_*_test.rb` files was skipped and a gate could pass with
|
|
399
|
+
no mutants run. A spec the old rules already find is unchanged. The
|
|
400
|
+
split name is left for `user_session.rb` when that file exists, so
|
|
401
|
+
`user.rb` does not run `user_session_test.rb`. The same rule holds outside
|
|
402
|
+
`app/` and `lib/`: `src/user.rb` does not take `test/src/user_session_test.rb`
|
|
403
|
+
when `src/user_session.rb` exists. A longer source claims a split file
|
|
404
|
+
only when that source itself searches the directory the file is in.
|
|
405
|
+
A failed capture of `test/foo/bar_upsert_test.rb` can mark `app/foo/bar.rb`
|
|
406
|
+
uncapturable only when no successful capture covered that source and
|
|
407
|
+
`bar_upsert.rb` does not exist. A test outside that directory does not,
|
|
408
|
+
and neither does a source that only shares the test basename.
|
|
409
|
+
`user_session__test.rb` stays with `user.rb` when `user_session.rb`
|
|
410
|
+
exists, because that name is not the exact test or a split test for
|
|
411
|
+
`user_session`. A failed exact spec in the mirrored `spec/` or `spec/lib/` directory
|
|
412
|
+
marks an `app/` or `lib/` source the same way. A failed spec in another
|
|
413
|
+
directory does not. A project rooted at `/` still finds split tests
|
|
414
|
+
inside that root. A stored failure keeps the pairing from the moment it
|
|
415
|
+
was checked. Adding or deleting a longer source rebuilds the coverage
|
|
416
|
+
map, so that failure is not moved onto the shorter source.
|
|
417
|
+
- **A blank `operators` key no longer runs zero mutants and exits 0** (#167).
|
|
418
|
+
`operators:` with no value became an empty list. The run made no mutants
|
|
419
|
+
and the gate passed. A blank operator list is now an error, and so is a
|
|
420
|
+
list of only unknown operator names. An empty `require` or `ignore` list
|
|
421
|
+
is still valid. `--operators` on the command line still replaces the
|
|
422
|
+
file list, including a blank or unknown file list.
|
|
423
|
+
- **The daemon no longer runs the tool's Ruby settings inside the app** (#100).
|
|
424
|
+
Open3 keeps a variable when the spawn hash omits it, so a parent `RUBYOPT`
|
|
425
|
+
ran inside the app. The child now gets the environment Bundler saved before
|
|
426
|
+
it activated, passed with `unsetenv_others`. The app keeps its gem home,
|
|
427
|
+
excluded groups, Bundler config, and chruby path. An `RBENV_VERSION` or
|
|
428
|
+
`ASDF_RUBY_VERSION` already in the environment stays. That pin is often
|
|
429
|
+
the Ruby that started the tool. `.ruby-version` applies only when no pin
|
|
430
|
+
is set. The tool's `RUBYOPT` and `RUBYLIB` do not reach the child, including
|
|
431
|
+
a value Bundler had saved. A gem home, `BUNDLE_PATH`, or `BUNDLE_WITHOUT`
|
|
432
|
+
reaches the child only when Bundler saved the app's value. The same
|
|
433
|
+
rule covers `GEM_PATH` and `BUNDLER_VERSION`. An rbenv version
|
|
434
|
+
bin under `RBENV_ROOT`, or under `~/.rbenv` when that variable is unset,
|
|
435
|
+
leaves `PATH` only when that root has a shim directory. An asdf version
|
|
436
|
+
bin leaves only when an asdf shim directory exists, including
|
|
437
|
+
`ASDF_DATA_DIR/shims`. Without that shim, the bin stays and `bundle` can
|
|
438
|
+
still be found. The app Gemfile, an explicit Ruby pin, and Rails env are
|
|
439
|
+
applied after that.
|
|
440
|
+
- **The Action refuses to run when `output` and `baseline` are the same file** (#160).
|
|
441
|
+
That setup used to work, and a failing run copied its report onto the
|
|
442
|
+
baseline. The next run then treated the failed report as the baseline, so
|
|
443
|
+
the gate could pass. The Action now stops before the run and exits 2.
|
|
444
|
+
The baseline may come from the `baseline` input, from `--baseline` in
|
|
445
|
+
`extra-args`, or from `.mutineer.yml`. A numeric path such as `123` is
|
|
446
|
+
read the same way as the CLI. Write the report to a separate path. Copy
|
|
447
|
+
that file onto the baseline in a later step, and only when this step
|
|
448
|
+
exits 0. You can also refresh the baseline with the CLI, and keep the
|
|
449
|
+
file only when the run exits 0. The next release moves the `v1` tag, so
|
|
450
|
+
workflows on `@v1` get this refusal then.
|
|
451
|
+
- **A mutation no longer leaves a heredoc body behind as Ruby** (#134).
|
|
452
|
+
Replacing a node deletes the heredoc opener and not the body. The body
|
|
453
|
+
then parses as code, raises, and counts as a kill. A mutation that drops
|
|
454
|
+
the opener is skipped. A replacement that still contains the opener stays.
|
|
455
|
+
- **A bare `public`, `private` or `protected` ends `module_function` mode**,
|
|
456
|
+
so later `def`s in the module body are named as instance methods, not
|
|
457
|
+
singleton methods (#144).
|
|
458
|
+
- **Turning on reduced motion mid-visit finishes the landing page count-ups**,
|
|
459
|
+
so no number keeps changing after the visitor asks for less motion (#176).
|
|
460
|
+
- **`rake site:build` can no longer delete a directory such as `lib/` or
|
|
461
|
+
`.git`** (#162). The task removed the destination it was given, and only
|
|
462
|
+
refused the checkout, its parents and `docs/`. It now always builds into
|
|
463
|
+
`_site/` at the checkout root, and `rake "site:build[DEST]"` and
|
|
464
|
+
`SITE_BUILD_DEST` are gone.
|
|
465
|
+
- **Documentation matches the 1.4 behavior.** Config examples use supported keys,
|
|
466
|
+
agent loops check the exit code and a real score, and report totals explain
|
|
467
|
+
partial `--fail-fast` runs. The README lists every config key and its validation
|
|
468
|
+
rules. API README links work outside GitHub, repeated changelog headings have
|
|
469
|
+
unique anchors, and the website checks validate built links and anchors.
|
|
470
|
+
Mutant-id wording and the gem's Minitest/RSpec description are also corrected.
|
|
471
|
+
- **A mutant in a nested method counts once** (#157). A `def` inside
|
|
472
|
+
another method is a subject of its own, but most operators also mutated
|
|
473
|
+
its code on the outer method. The same edit then counted twice in the
|
|
474
|
+
score, with two ids. Every operator now skips a nested `def` when it
|
|
475
|
+
mutates the outer method. A `def` inside `class << obj`, where `obj` is
|
|
476
|
+
not `self`, is not a subject, so the outer method still mutates its code.
|
|
477
|
+
`array_literal`, `chain_link`, `condition_true`, `condition_false`,
|
|
478
|
+
`operand_removal`, `operator_assignment` and `return_nil` skipped that
|
|
479
|
+
code before. Their new mutants there can show as new survivors against a
|
|
480
|
+
baseline.
|
|
481
|
+
|
|
482
|
+
A mutant id includes an ordinal among the mutants with the same operator
|
|
483
|
+
and token in a subject. When the duplicates leave the outer method, the
|
|
484
|
+
ordinals of its own mutants change. An old duplicate id can then name a
|
|
485
|
+
different mutant on the outer method, and an `ignore:` entry or a
|
|
486
|
+
baseline entry with that id applies to that mutant with no warning. If
|
|
487
|
+
your code has nested methods, regenerate the `ignore:` entries and the
|
|
488
|
+
baseline for the outer methods.
|
|
489
|
+
|
|
490
|
+
Under `--strategy redefine` (the `--rails` default), a mutant in a nested
|
|
491
|
+
method can survive or error even when a test would catch it. `redefine`
|
|
492
|
+
loads only the inner method, and a call to the outer method defines the
|
|
493
|
+
original inner method again. Before, the copy of the edit on the outer
|
|
494
|
+
method was killed. Now each edit has one verdict, from the inner method.
|
|
495
|
+
|
|
496
|
+
## [1.4.0] - 2026-09-30
|
|
497
|
+
|
|
498
|
+
### Added
|
|
499
|
+
- **Condition-forcing operators** (Tier-2, opt-in via `--operators`):
|
|
500
|
+
`condition_true` and `condition_false` replace an `if`/`elsif`/`unless`,
|
|
501
|
+
ternary, modifier or `case`/`in` guard condition with `(true)` or `(false)`, so
|
|
502
|
+
its branch always runs or never runs. A surviving mutant means no selected
|
|
503
|
+
test detected the forced condition. A literal condition, also in
|
|
504
|
+
parentheses, makes no mutant: forcing it changes nothing or repeats the
|
|
505
|
+
`boolean_literal` flip. A condition that holds a heredoc makes no mutant.
|
|
506
|
+
A condition that assigns a variable keeps its code, and only its value
|
|
507
|
+
is forced (`(m = x; true)`), so later code still sees the variable.
|
|
508
|
+
The never-runs side of an else-less conditional can
|
|
509
|
+
be the same program as the `nil` that `statement_removal` or `return_nil`
|
|
510
|
+
puts in its place; it is still made, so these mutants and their ids do not
|
|
511
|
+
depend on which other operators run.
|
|
512
|
+
- **Operator-assignment operator** (Tier-2, opt-in via `--operators`):
|
|
513
|
+
`operator_assignment` swaps the operator of a compound assignment:
|
|
514
|
+
`+=` <-> `-=`, `*=` <-> `/=`, and `%=` and `**=` -> `*=`. It covers local,
|
|
515
|
+
instance, class and global variables, constants, calls (`a.b += 1`) and
|
|
516
|
+
index calls (`a[i] += 1`). The `arithmetic` operator never sees these
|
|
517
|
+
forms, because Prism does not parse them as calls. The operator does not
|
|
518
|
+
change `||=`, `&&=`, or the bitwise and shift forms (`|=`, `<<=`).
|
|
519
|
+
|
|
520
|
+
### Fixed
|
|
521
|
+
- **A typed `--rails`, `--verbose` or `--debug` beats `rails: false` and
|
|
522
|
+
`verbose: false` in `.mutineer.yml`** (#103). The flag was dropped, so the
|
|
523
|
+
run went on without Rails boot or verbose output. The config layers now keep
|
|
524
|
+
only the keys the user wrote, and "did the user write this" comes from those
|
|
525
|
+
keys, not from a hand-kept list.
|
|
526
|
+
- **`framework: rspec` in `.mutineer.yml` survives test pairing** (#103).
|
|
527
|
+
Pairing re-detected the framework from the test file names, so an RSpec
|
|
528
|
+
suite in `test/calc_test.rb` ran under Minitest.
|
|
529
|
+
- **A bad number is an error, not a rounded value** (#105). `--jobs 1.9` and
|
|
530
|
+
`--baseline-epsilon abc` no longer become `1` and `0.0`. One option schema
|
|
531
|
+
parses each value once, where it enters, for both the command line and
|
|
532
|
+
`.mutineer.yml`.
|
|
533
|
+
- **`--since` keeps a source file that git does not track yet** (#156). Before,
|
|
534
|
+
such a file counted as unchanged. The run scored no mutants, and a positive
|
|
535
|
+
`--threshold` still exited 0. The file is now new in full, so all its
|
|
536
|
+
mutants run.
|
|
537
|
+
|
|
538
|
+
### Changed
|
|
539
|
+
- **These now exit 2 with a message that names the option**: `jobs: 1.9` or
|
|
540
|
+
`jobs: true` in `.mutineer.yml` (before: `1`, or a crash); a bad, negative
|
|
541
|
+
or non-finite `--baseline-epsilon` (before: `0.0`); a boolean key in
|
|
542
|
+
`.mutineer.yml` that is not `true` or `false`, such as the string `"yes"`
|
|
543
|
+
(before: `false`).
|
|
544
|
+
- **`--jobs` takes plain digits only.** `0x2`, `+2`, `" 2"` and `1_0` exit 2.
|
|
545
|
+
Before, `Integer()` read them as 2, 2, 2 and 10. `--threshold` and
|
|
546
|
+
`--baseline-epsilon` take a plain decimal, such as `2` or `2.5`, from a
|
|
547
|
+
string. `0x10`, `+2`, `1_0` and `1e2` exit 2. Before, `Float()` read them
|
|
548
|
+
as 16.0, 2.0, 10.0 and 100.0.
|
|
549
|
+
- **A string option that gets `true`, `false` or no value exits 2**, such as
|
|
550
|
+
`only: false` or `baseline:` in `.mutineer.yml`. A blank `since` also exits 2,
|
|
551
|
+
in the file and on the command line, so an unset shell variable does not
|
|
552
|
+
turn scoping off. `since: false` still means no scoping.
|
|
553
|
+
- **Config file errors start with the file and key**, for example
|
|
554
|
+
`.mutineer.yml: threshold must be a number between 0 and 100`, where they
|
|
555
|
+
started with `--threshold`.
|
|
556
|
+
- **The docs site is built in CI** — a Pages workflow runs `rake site:build`
|
|
557
|
+
and deploys the result, so the YARD HTML under `/api/`, `llms-full.txt`,
|
|
558
|
+
`json-schema.html` and `sitemap.xml` are no longer committed. CI checks
|
|
559
|
+
that the site builds, in place of `rake yard:pages:check`. The site keeps
|
|
560
|
+
its URLs (#153).
|
|
561
|
+
|
|
562
|
+
## [1.3.0] - 2026-09-29
|
|
563
|
+
|
|
564
|
+
### Changed
|
|
565
|
+
- **Mutant ids now include the project-relative file path** (#126). Before, two
|
|
566
|
+
mutants with the same method name, operator and token in different files got
|
|
567
|
+
the same id. That happened with a top-level `def` or block in two files, and
|
|
568
|
+
with a class reopened in another file. One `ignore:` entry then suppressed
|
|
569
|
+
both mutants, and `--baseline` could hide a new survivor behind an old one.
|
|
570
|
+
- Every survivor id changes once in this release. This affects any external
|
|
571
|
+
tool that tracks survivors by id.
|
|
572
|
+
- Moving or renaming a file now changes its ids.
|
|
573
|
+
- Ids follow the project root: the directory mutineer runs from, or the
|
|
574
|
+
Action's `working-directory`. A run from a different root gives different ids.
|
|
575
|
+
mutineer finds `.mutineer.yml` by walking up, so a run from a subdirectory
|
|
576
|
+
prints one `[mutineer]` warning that the loaded ignore ids will not match.
|
|
577
|
+
- A source outside the project root uses its absolute path, so its ids differ
|
|
578
|
+
between machines.
|
|
579
|
+
- Two methods with the same qualified name in one file (for example two
|
|
580
|
+
top-level `def index` in two DSL blocks) now get different ids. The second
|
|
581
|
+
and later ones hash their position among those methods; the first keeps its
|
|
582
|
+
id.
|
|
583
|
+
- **The JSON report marks its id format** (schema `1.4`, additive).
|
|
584
|
+
`summary.id_format` is `2` for ids that include the file path.
|
|
585
|
+
`summary.legacy_id_matches` counts old-format `ignore:` entries (`ignore`) and
|
|
586
|
+
survivors matched only through an old-format baseline id (`baseline`). The
|
|
587
|
+
GitHub Action shows one warning annotation when either count is not zero.
|
|
588
|
+
|
|
589
|
+
### Deprecated
|
|
590
|
+
- **Old-format ids in `ignore:` and in baselines.** Matching on them is removed
|
|
591
|
+
in 2.0. Replace each old `ignore:` entry with the new ids from the warning
|
|
592
|
+
(only the intended ones when it over-matched several mutants).
|
|
593
|
+
Regenerate a baseline (`--format json`) only after every gate that reads it
|
|
594
|
+
runs this version or later (the Action's `version:` pin, your CI
|
|
595
|
+
`Gemfile.lock`). An older version treats every new-format survivor as new.
|
|
596
|
+
|
|
597
|
+
### Fixed
|
|
598
|
+
- **`module_function :name` and `module_function def name` promote only
|
|
599
|
+
their own module's methods** — a class or module in the same file with a
|
|
600
|
+
method of the same name kept an instance method in Ruby, but mutineer
|
|
601
|
+
named it as a class method. `--strategy redefine` then mutated a method the
|
|
602
|
+
tests never call, so a killable mutant falsely survived, and
|
|
603
|
+
`--only Class#name` selected nothing (#98). The affected methods now get
|
|
604
|
+
their correct names, so their mutant ids change: regenerate any ignore
|
|
605
|
+
entries or baseline survivors that pointed at them.
|
|
606
|
+
- **A root-anchored reopening names the top-level constant** — a
|
|
607
|
+
`module ::Root` or `class ::Solo` written inside another module now gives
|
|
608
|
+
`Root` and `Solo`, not `Outer::Root` and `Outer::Solo`. This applies to every
|
|
609
|
+
method in such a body, with or without `module_function`, so their mutant
|
|
610
|
+
ids change too. `--strategy redefine` rebuilds such a body's scope as
|
|
611
|
+
written (`module Outer` then `class ::Solo`), so a constant from `Outer`
|
|
612
|
+
still resolves in the mutated method, as it does under `reload`. Before,
|
|
613
|
+
the method raised NameError in the test, which counted as a false kill
|
|
614
|
+
(#145).
|
|
615
|
+
- **Old-format ids keep working, with a warning** (#126). An old-format
|
|
616
|
+
`ignore:` entry still suppresses the mutants it matched before. The run prints
|
|
617
|
+
one `[mutineer]` warning per entry with each new id, its file and its method.
|
|
618
|
+
When the entry matched several mutants (in different files, or same-named
|
|
619
|
+
methods in one file), the warning says it
|
|
620
|
+
over-matched and to keep only the ids for the mutant you meant to ignore. A
|
|
621
|
+
`--baseline` file without `summary.id_format` matches on new ids, or on old
|
|
622
|
+
ids from the same file, so no survivor reads as new or fixed only because its
|
|
623
|
+
id changed. A stored file outside the project root (a baseline written on
|
|
624
|
+
another machine) matches on the old id alone. The run prints one
|
|
625
|
+
`[mutineer]` warning to regenerate the baseline.
|
|
626
|
+
|
|
627
|
+
## [1.2.0] - 2026-09-28
|
|
628
|
+
|
|
629
|
+
### Added
|
|
630
|
+
- **Operand-removal operator** (Tier-2, opt-in via `--operators`):
|
|
631
|
+
`operand_removal` replaces `a && b` with `(a)` and with `(b)`, and does the
|
|
632
|
+
same for `||`, `and` and `or`. The mutant survives when no test needs the
|
|
633
|
+
operand that the mutant removes. The operator never keeps a jump operand
|
|
634
|
+
(`return`, `break`, `next`, `redo`, `retry`) alone, because a jump does not
|
|
635
|
+
parse in a value context. It never removes an operand that holds a heredoc,
|
|
636
|
+
because the heredoc body stays behind as code. It skips nested method
|
|
637
|
+
definitions, because mutineer mutates each one as its own method.
|
|
638
|
+
- **Array-literal operator** (Tier-2, opt-in via `--operators`):
|
|
639
|
+
`array_literal` replaces a non-empty array literal, such as `[a, b]` or
|
|
640
|
+
`%i[a b]`, with `[]`. The mutant survives when no test checks the contents
|
|
641
|
+
of the array. The operator skips an implicit array (`x = 1, 2`), an array
|
|
642
|
+
that holds a heredoc, and nested method definitions.
|
|
643
|
+
- **Sources also pair with Minitest's `test/**/test_*.rb` files** — after the
|
|
644
|
+
`_test.rb` forms, so existing projects pair as before. `lib/helper.rb` does
|
|
645
|
+
not pair with the `test/test_helper.rb` support file. A failed capture of a
|
|
646
|
+
`test_<name>.rb` file now marks `<name>.rb` uncapturable, as `<name>_test.rb`
|
|
647
|
+
does (#120). Without `framework:` set, a source with `spec/<name>_spec.rb`
|
|
648
|
+
and `test/test_<name>.rb` but no `<name>_test.rb` now pairs with the
|
|
649
|
+
Minitest file, as the "Minitest first" order says.
|
|
650
|
+
|
|
651
|
+
### Fixed
|
|
652
|
+
- **`reload` loads the mutant by an absolute path** — a relative source path
|
|
653
|
+
gave the mutant relative backtrace paths, so code that checks its own frames
|
|
654
|
+
by absolute path failed for every mutant, a false kill (#123).
|
|
655
|
+
- **`require "test_helper"` works without `RUBYOPT`** — a standalone run puts
|
|
656
|
+
`lib`, then each test file's `test_helper.rb` directory, on the load path,
|
|
657
|
+
as boot mode and `rake test` do. A run where no test records coverage because
|
|
658
|
+
captures failed now exits 1 instead of reporting N/A (#119).
|
|
659
|
+
- **A disable-line marker warns about an operator it does not know** — a
|
|
660
|
+
reason written without `--` became part of the operator name, so the marker
|
|
661
|
+
suppressed nothing and said nothing (#124). A marker followed only by spaces
|
|
662
|
+
or commas, such as `disable-line -- why`, now disables the whole line.
|
|
663
|
+
- **Coverage capture and the clean check run each source once** — they read
|
|
664
|
+
sources with `load`, so a test's own `require` ran them again: a `Struct`
|
|
665
|
+
superclass raised `superclass mismatch`, and load-time code ran twice (#122).
|
|
666
|
+
A mutant of such a class still errors under `--strategy reload`, which loads
|
|
667
|
+
the mutated file again; `--strategy redefine` runs it.
|
|
668
|
+
Code that guards itself to run once (`unless defined?(X)`) can now show as
|
|
669
|
+
covered, so its mutants run where they were `no_coverage` before.
|
|
670
|
+
- **A red unmutated suite now shows why it failed** — in a standalone run, the
|
|
671
|
+
Minitest summary or RSpec output of the failing test, with its failure
|
|
672
|
+
message, goes to stderr before the "not green" error. A passing run prints
|
|
673
|
+
nothing extra. Boot mode (`--rails`, `--boot`) is unchanged (#121).
|
|
674
|
+
|
|
675
|
+
## [1.1.0] - 2026-09-28
|
|
676
|
+
|
|
677
|
+
### Added
|
|
678
|
+
- **Safe-navigation operator** (Tier-2, opt-in via `--operators`):
|
|
679
|
+
`safe_navigation` replaces `&.` with `.`. The mutant survives when no test
|
|
680
|
+
passes `nil` to the call.
|
|
681
|
+
- **Range operator** (Tier-2, opt-in via `--operators`): `range` replaces
|
|
682
|
+
`..` with `...` and `...` with `..`. The `..` -> `...` mutant survives when
|
|
683
|
+
no test checks the last element of the range. Endless ranges (`1..`) are
|
|
684
|
+
skipped, because `(1..)` and `(1...)` give the same result for slicing,
|
|
685
|
+
`include?`, `===` and pattern matching.
|
|
686
|
+
- **Negation-removal operator** (Tier-2, opt-in via `--operators`):
|
|
687
|
+
`negation_removal` removes the `!` from `!x` and the `not` from `not x`.
|
|
688
|
+
The mutant survives when no test depends on the negated value. The
|
|
689
|
+
explicit form `x.!` is skipped, because `x.` does not parse.
|
|
690
|
+
|
|
691
|
+
### Changed
|
|
692
|
+
- **Stderr of tests and specs is visible** in the in-process and `--daemon`
|
|
693
|
+
runs. Mutineer silences stdout once per child process and no longer hides
|
|
694
|
+
stderr, so its own child diagnostics always reach you. `--test-command`
|
|
695
|
+
runs still capture stderr with stdout and show it under `--verbose`.
|
|
696
|
+
- **A mutant's test run stops at the first failing test** — one failure
|
|
697
|
+
already kills the mutant, so the forked child does not run the tests that
|
|
698
|
+
remain. Killed mutants cost less time, and survived mutants cost the same.
|
|
699
|
+
Under Minitest, when the outer reporter of the run records a failure or an
|
|
700
|
+
error (a skip does not count), each remaining test and each remaining test
|
|
701
|
+
class returns before it starts. A skipped class does not start its
|
|
702
|
+
class-level hooks. The run does not unwind: a class that is running
|
|
703
|
+
finishes normally, so its `after_all` hooks and a class-level
|
|
704
|
+
`transaction { super; raise ActiveRecord::Rollback }` still run. RSpec runs
|
|
705
|
+
with `--fail-fast`. This applies to the in-process backend only: coverage
|
|
706
|
+
capture and the clean checks still run every test, and the `--daemon` and
|
|
707
|
+
`--test-command` backends do not change. The CLI `--fail-fast` flag keeps
|
|
708
|
+
its meaning. On rack's `lib/rack/utils.rb` (`--jobs 1`), a full run takes
|
|
709
|
+
about 35–41 s instead of about 86–89 s. With the same coverage map, the
|
|
710
|
+
verdicts are the same.
|
|
711
|
+
- **The mutant run uses a fixed Minitest seed** — with the stop, the test
|
|
712
|
+
order can decide the verdict, so the child runs Minitest with seed `1`
|
|
713
|
+
unless the environment sets `SEED`. The same code then gives the same
|
|
714
|
+
verdict on each run. Coverage capture and the clean checks keep the random
|
|
715
|
+
seed, so the clean check runs the tests in a random order while each mutant
|
|
716
|
+
run uses the fixed order. RSpec keeps its configured order: a suite configured with
|
|
717
|
+
`config.order = :random` can still get a different verdict on each run for
|
|
718
|
+
the case below. In an order-dependent Minitest suite, the fixed seed makes
|
|
719
|
+
a false `killed` happen on every run or on no run, not on some runs.
|
|
720
|
+
- **A mutant whose failing test runs before a hanging test is now `killed`,
|
|
721
|
+
not `timeout`** — the run stops at the failure, before the hang. The tests
|
|
722
|
+
did detect the mutation, so `killed` is the correct verdict. If the hanging
|
|
723
|
+
test runs first in the fixed order, the verdict stays `timeout`. Compared
|
|
724
|
+
with a baseline from an earlier version, the score usually goes up. In an
|
|
725
|
+
order-dependent suite it can also go down: a mutant that a random order
|
|
726
|
+
killed on some runs can survive on every run in the fixed order. A change
|
|
727
|
+
to the tests can change the fixed order, so a later run can move such a
|
|
728
|
+
mutant from `killed` to `timeout`, and a `--baseline` gate then reports a
|
|
729
|
+
score drop.
|
|
730
|
+
- **Some runs still run most tests** — Minitest `parallelize_me!`, and Rails
|
|
731
|
+
`parallelize` above its threshold (by default more than 50 tests in the
|
|
732
|
+
child, or at any test count when `PARALLEL_WORKERS` is 2 or more in the
|
|
733
|
+
environment), queue their tests before the first result comes back, so the
|
|
734
|
+
queued tests still run. The verdict is the same as before. Below the
|
|
735
|
+
Rails threshold, the tests run one after the other in the child, and the
|
|
736
|
+
stop works.
|
|
737
|
+
|
|
738
|
+
### Fixed
|
|
739
|
+
- **Tests that reopen `$stdout`** (Minitest's `capture_subprocess_io`,
|
|
740
|
+
RSpec's `to_stdout_from_any_process`) no longer make a green suite
|
|
741
|
+
"not green" or count as false kills.
|
|
742
|
+
- **Test or source files that print while they load** no longer make coverage
|
|
743
|
+
capture fail with `invalid coverage output`. The capture subprocess now
|
|
744
|
+
sends its result over a separate pipe, not over stdout.
|
|
745
|
+
- **Chain-link operator** (Tier-2, opt-in via `--operators`):
|
|
746
|
+
`chain_link` drops one call from a chain, with its arguments and block
|
|
747
|
+
(`user.account.name` -> `user.name`). The mutant survives when no test tells
|
|
748
|
+
the chain apart from the same chain without that step. Conversions and copies
|
|
749
|
+
(`to_s`, `to_a`, `dup`, `freeze`, ...) and `new` are never dropped.
|
|
750
|
+
|
|
751
|
+
## [1.0.2] - 2026-09-21
|
|
752
|
+
|
|
753
|
+
### Added
|
|
754
|
+
- **AI-readable docs wiring**: HTML pages with Markdown twins now advertise
|
|
755
|
+
`rel="alternate" type="text/markdown"`, `index.md` is the landing/CLI
|
|
756
|
+
essentials twin, `skill.md` is listed under Optional in `llms.txt`, and
|
|
757
|
+
`sitemap.xml` is generated from the same catalog as `llms.txt` (#91).
|
|
758
|
+
- **Single-source CLI contract**: exit codes and `--threshold` live in
|
|
759
|
+
`docs/fragments/contract.yml`; `rake docs:generate` writes `llms-full.txt`,
|
|
760
|
+
`json-schema.html`, and the marked copies so they cannot drift (#82).
|
|
761
|
+
- **YARD API on Pages**: the current gem's YARD HTML is published at
|
|
762
|
+
`/api/`, linked from the docs site, and `rake yard:pages:check` keeps it
|
|
763
|
+
from lagging the shipped sources. `documentation_uri` stays the Pages
|
|
764
|
+
root (#92).
|
|
765
|
+
|
|
766
|
+
## [1.0.1] - 2026-09-18
|
|
767
|
+
|
|
768
|
+
### Added
|
|
769
|
+
- **RubyGems `documentation_uri`**: the published gem now points at the docs
|
|
770
|
+
site (`https://davidteren.github.io/mutineer/`) so gem-page discovery
|
|
771
|
+
reaches the Pages docs (#90).
|
|
772
|
+
|
|
773
|
+
### Fixed
|
|
774
|
+
- **A red unmutated suite can no longer pass a mutation gate** — coverage
|
|
775
|
+
capture now keeps the original Minitest/RSpec result, and a failing clean run
|
|
776
|
+
aborts with the existing smoke-check error (exit 1) instead of scoring those
|
|
777
|
+
assertion failures as killed mutants. A warm coverage cache re-checks the
|
|
778
|
+
current suite and cannot bypass this (#96).
|
|
779
|
+
- **Concurrent external runs no longer restore each other's source files** —
|
|
780
|
+
swap and orphan recovery share one exclusive OS lock per source, acquired
|
|
781
|
+
before reading or healing. A live owner's mutant and backup stay intact; a
|
|
782
|
+
dead owner's backup still restores the original bytes (#99).
|
|
783
|
+
- **Coverage cache now invalidates when a required test helper changes** —
|
|
784
|
+
a successful map records fingerprints of project-local loaded Ruby files,
|
|
785
|
+
old cache entries without that data rebuild, and a helper-only edit no
|
|
786
|
+
longer hides a new survivor behind a stale 100% score (#97).
|
|
787
|
+
- **Release version calculation ignores floating major tags** — `release-pr.yml`
|
|
788
|
+
selects the newest complete `vMAJOR.MINOR.PATCH` ancestor and validates the
|
|
789
|
+
next version before writing files, so a later `v1` tag can no longer produce
|
|
790
|
+
`v1..1` (#95).
|
|
791
|
+
|
|
792
|
+
## [1.0.0] - 2026-09-08
|
|
793
|
+
|
|
794
|
+
The GitHub Action's PR default changes in this release, which is why it is a
|
|
795
|
+
new major: workflows pinned to `davidteren/mutineer@v0` keep the old full-scan
|
|
796
|
+
behavior; upgrading to `@v1` opts into diff-scoped PR runs (details under
|
|
797
|
+
Changed).
|
|
798
|
+
|
|
799
|
+
### Added
|
|
800
|
+
- **The Action reports where CI readers look**: with the default JSON format it
|
|
801
|
+
writes a score/pass-fail table (plus the baseline delta, when `baseline` is
|
|
802
|
+
set) to the job step summary, and emits one `file=…,line=…` annotation per
|
|
803
|
+
surviving mutant (up to 50; the summary lists the first 20, the full set
|
|
804
|
+
stays in the JSON report) so results land on the PR diff instead of only in a
|
|
805
|
+
collapsed log group — `error` level when the gate failed, `warning` when it
|
|
806
|
+
passed. When `output` is unset the JSON report is routed to a temp file and
|
|
807
|
+
still printed to the log, and the `report` output now exposes the report
|
|
808
|
+
path in both cases so a later step can consume the JSON without scraping
|
|
809
|
+
the log (#86).
|
|
810
|
+
- **Progress during the run**: every backend prints `[mutineer] N/M mutants
|
|
811
|
+
(P%)` to stderr at each 10% step, so a long run is never silent between
|
|
812
|
+
config resolution and the report. Stdout stays byte-exact for `--format json`
|
|
813
|
+
and `--output`. `WorkerPool#run` gains an optional `on_result:` callback for
|
|
814
|
+
this (called in the parent per reaped result) (#86).
|
|
815
|
+
|
|
816
|
+
### Changed
|
|
817
|
+
- **PR runs scope themselves in the GitHub Action**: on `pull_request` events
|
|
818
|
+
the `since` input now defaults to the PR base, so the action grades just the
|
|
819
|
+
diff out of the box. **Migration note (the reason for the major bump)**: a
|
|
820
|
+
PR gate that previously full-scanned now scores only the PR's changed lines,
|
|
821
|
+
so `threshold` applies to fewer mutants. Stay on `@v0` to keep the old
|
|
822
|
+
default, or pass `since: none` on `@v1` for full scans. Workflows that
|
|
823
|
+
already pass a non-empty `since` are unchanged (an explicit empty string is
|
|
824
|
+
indistinguishable from unset and picks up the new default). A PR with no
|
|
825
|
+
changed source lines (docs- or test-only) scores zero mutants and passes the
|
|
826
|
+
scoped gate vacuously; keep a full-scan baseline refresh on main as the
|
|
827
|
+
backstop. With `use-bundler: true` the caller's Gemfile picks the gem, and
|
|
828
|
+
the new default needs mutineer >= 1.0.0 (the action enforces the floor with
|
|
829
|
+
a clear error). The action scopes to the PR's exact base commit from the
|
|
830
|
+
event payload (immune to the
|
|
831
|
+
base branch advancing mid-job), falling back to a fresh fetch of the base
|
|
832
|
+
branch tip; when neither can be resolved it warns and runs without an
|
|
833
|
+
action-provided `--since` (a `.mutineer.yml` `since:` key, if any, still
|
|
834
|
+
applies). The default deliberately does NOT fire on `pull_request_target`:
|
|
835
|
+
checkout there defaults to the base branch, so auto-scoping would diff the
|
|
836
|
+
base against itself and green the gate on an empty run (#86).
|
|
837
|
+
- **`--baseline` on a diff-scoped run gates on new survivors only**: a
|
|
838
|
+
`--since` run's score covers only the changed-line mutants, a different
|
|
839
|
+
denominator from a full-run baseline, so comparing the two scores
|
|
840
|
+
manufactured false regressions (a 3/4-mutant PR at 75% "dropped" from a
|
|
841
|
+
92% whole-repo baseline with zero new survivors). With `--since`, the
|
|
842
|
+
score-drop half of the baseline gate is skipped; new-survivor detection by
|
|
843
|
+
stable id (and the reported before/after scores) are unchanged. The JSON
|
|
844
|
+
report records the scope in a new additive `summary.scoped` key
|
|
845
|
+
(`schema_version` 1.3), and the `baseline` block records `score_comparable`
|
|
846
|
+
so a consumer knows when not to render the two scores as a comparison
|
|
847
|
+
(`fixed_survivors` is likewise empty under a scoped side: an out-of-scope
|
|
848
|
+
survivor was never re-tested, so absence does not mean fixed). The reverse
|
|
849
|
+
direction is a hard guard: a scoped report is refused as a baseline (exit 2
|
|
850
|
+
with a regenerate hint), because survivors outside its diff would all read
|
|
851
|
+
as new regressions. A new `--no-since` flag disables diff scoping explicitly: a
|
|
852
|
+
typed no beats a `.mutineer.yml` `since:` key, and the action's
|
|
853
|
+
`since: none` passes it through (#86).
|
|
854
|
+
|
|
855
|
+
## [0.11.4] - 2026-07-29
|
|
856
|
+
|
|
857
|
+
### Fixed
|
|
858
|
+
- **`--threshold` now gates on the run being complete, not just its score** — a
|
|
859
|
+
score is `killed / (killed + survived)`, so mutants that error, time out or come
|
|
860
|
+
back uncapturable are excluded from the denominator: a broken harness *raised*
|
|
861
|
+
the score instead of lowering it. Ninety errored mutants and ten that ran (nine
|
|
862
|
+
killed) reported 90% and exited 0, so CI could not tell a complete run from a
|
|
863
|
+
mostly-broken one. Past 10% of attempted mutants producing no verdict, a positive
|
|
864
|
+
`--threshold` now exits 1 whatever the score, and the report says which states
|
|
865
|
+
broke, on every `--format`. A single bad mutant never trips it, however small the
|
|
866
|
+
run, so a `--since` PR with a handful of mutants keeps its flake tolerance (#78).
|
|
867
|
+
|
|
868
|
+
### Added
|
|
869
|
+
- **`no_verdict[]` in the JSON report** (`schema_version` 1.2) — every attempted
|
|
870
|
+
mutant that produced no verdict, with its `status` and, where there is one, the
|
|
871
|
+
`details` explaining the cause. `Result#details` was built and rendered in no
|
|
872
|
+
format at all, so a daemon crash reached the user as nothing but a larger errored
|
|
873
|
+
count. `summary` gains `attempted` and `no_verdict`, the two figures the
|
|
874
|
+
completeness gate is computed from (#78).
|
|
875
|
+
|
|
876
|
+
## [0.11.3] - 2026-07-29
|
|
877
|
+
|
|
878
|
+
### Fixed
|
|
879
|
+
- **Nothing to mutate now costs nothing** — `--since` matching no changed line (a
|
|
880
|
+
docs-only PR, which README documents `--since origin/<base>` for) still did the
|
|
881
|
+
expensive part before discovering there was no work: `--daemon` booted the app
|
|
882
|
+
once for the coverage map and again for every `--jobs` worker, and
|
|
883
|
+
`--test-command` ran the whole suite for a smoke check that calibrates a timeout
|
|
884
|
+
no mutant would use. Both backends now return as soon as the job list is empty.
|
|
885
|
+
The in-process path is unchanged: it boots before collecting jobs, so it cannot
|
|
886
|
+
know the list is empty yet. One consequence worth stating: a zero-job `--daemon`
|
|
887
|
+
run no longer boots the app, so an app that fails to boot now exits 0 (nothing
|
|
888
|
+
to do) instead of exit 1 (#76).
|
|
889
|
+
- **A dead daemon ends the run instead of scoring the rest against it** — a
|
|
890
|
+
daemon that dies while running one mutant was already handled: `DaemonClient`
|
|
891
|
+
respawns and answers `error` for that mutant. But a daemon that could not come
|
|
892
|
+
back was not. `restart!` closes the pipes before respawning, so if the respawn
|
|
893
|
+
failed at the OS level (`EMFILE` or `ENOMEM` under `--jobs N`, `ENOENT` when
|
|
894
|
+
`bundle` does not resolve) the client was left permanently dead while raising
|
|
895
|
+
errors that read as ordinary per-mutant failures. Every remaining mutant scored
|
|
896
|
+
`error` against nothing, and Mutineer printed a mutation score built on the
|
|
897
|
+
fraction of mutants that ran before the daemon died. `DaemonClient` now raises
|
|
898
|
+
`DaemonBootError` whenever it is gone for good — a refused spawn, a failed boot
|
|
899
|
+
handshake on respawn, closed pipes, or `MAX_RESTARTS` crashes — and the CLI
|
|
900
|
+
reports it as a message and exit 1 rather than a backtrace. Under `--jobs N`
|
|
901
|
+
the remaining queue is dropped so sibling workers stop too. Errored mutants
|
|
902
|
+
still cannot fail the `--threshold` gate once any mutant is scored (#78).
|
|
903
|
+
|
|
904
|
+
### Changed
|
|
905
|
+
- **Daemon orchestration split out of `Runner`** — the persistent-daemon backend
|
|
906
|
+
(job fan-out, worker DBs, coverage-map build, boot config, verdict mapping) now
|
|
907
|
+
lives in `Mutineer::DaemonBackend`. `Runner` keeps job collection, coverage
|
|
908
|
+
selection and the single in-process mutant run, and drops from 662 to 439 lines.
|
|
909
|
+
The external backend's orchestration stays on `Runner` for now, so the two
|
|
910
|
+
backends are not yet symmetrical. No CLI or behaviour change (#58).
|
|
911
|
+
Internal-only removals: `Runner.execute_daemon`, `Runner.daemon_coverage_map`
|
|
912
|
+
and `Runner::DAEMON_TIMEOUT` no longer exist. They were never documented —
|
|
913
|
+
the supported programmatic contract is the JSON report, the stable mutant ids
|
|
914
|
+
and the exit codes — so only code calling `Runner` internals directly is
|
|
915
|
+
affected.
|
|
916
|
+
- **Comment diet on orchestration files** — present-tense YARD contracts; drop
|
|
917
|
+
ticket/phase/KTD history tags from runner, CLI, daemon pair, coverage map,
|
|
918
|
+
reporter, result, and related modules. Safety and score invariants kept (#57).
|
|
919
|
+
|
|
920
|
+
## [0.11.2] - 2026-07-24
|
|
921
|
+
|
|
922
|
+
### Fixed
|
|
923
|
+
- **`--test-command` under version managers** — scrub Mutineer's rbenv/asdf
|
|
924
|
+
version bins and bundler/gem env from the child so the suite can resolve the
|
|
925
|
+
app's Ruby via shims / `.ruby-version`. Smoke check prints a targeted hint on
|
|
926
|
+
`Bundler::RubyVersionMismatch` instead of only blaming DB/migrations. Docs
|
|
927
|
+
cover a wrapper recipe for stubborn setups (#32).
|
|
928
|
+
|
|
929
|
+
## [0.11.1] - 2026-07-23
|
|
930
|
+
|
|
931
|
+
### Fixed
|
|
932
|
+
- **`--daemon` score disclosure** — stop claiming “no coverage narrowing / lower
|
|
933
|
+
bound” on every run (narrowing already ships). Warn only when the coverage map
|
|
934
|
+
is unavailable and the run falls back to the full `--test` set (#48).
|
|
935
|
+
- **`--threshold` CI fidelity** — exit 1 when a positive threshold is set but the
|
|
936
|
+
run produced only errors/timeouts/uncapturable (nil score with a broken harness)
|
|
937
|
+
instead of silently green; pure no_coverage / all-ignored still skips the gate
|
|
938
|
+
(#49). Non-numeric `--threshold` / YAML `threshold` is a usage error (exit 2),
|
|
939
|
+
not a silent gate-off (#51).
|
|
940
|
+
- **`--daemon` contracts** — reject `--framework rspec` (exit 2); force
|
|
941
|
+
`--strategy reload` with a clear warning when redefine was requested (daemon
|
|
942
|
+
whole-file only) (#50). **In-process `--rails` always serial** unless
|
|
943
|
+
`--daemon` (only daemon has per-worker DB isolation) (#55).
|
|
944
|
+
- **Isolation timeout** — kill the child process group and honor a clean exit
|
|
945
|
+
that races the deadline (not always timeout) (#52). **External backend** —
|
|
946
|
+
signal death is `error`, not `killed` (#53).
|
|
947
|
+
- **`--fail-fast` is serial** on the in-process path (matches daemon) so the
|
|
948
|
+
survivor set is deterministic under any `--jobs` (#54).
|
|
949
|
+
- **Dry-run uses `collect_jobs`** so candidates match a real run (#59).
|
|
950
|
+
- **Daemon schema load once per worker** (not every mutant fork) (#56).
|
|
951
|
+
- **Nested `statement_removal`** visits inner statement lists (#62).
|
|
952
|
+
- Dead seams: remove unused `RailsWorkerDb.provision`, simplify parallel daemon
|
|
953
|
+
fail-fast branch, drop unused `validate_daemon!` arg (#60).
|
|
954
|
+
- Archive historical implementation spec under `docs/archive/` (#61).
|
|
955
|
+
|
|
956
|
+
## [0.11.0] - 2026-07-02
|
|
957
|
+
|
|
958
|
+
### Added
|
|
959
|
+
- **`--daemon` backend — fast, parallel-safe Rails mutation testing** (#26/#27
|
|
960
|
+
Phase 2). Boots the app **once** in a persistent daemon and forks per mutant
|
|
961
|
+
(restoring shared-boot speed), and gives **each parallel worker its own
|
|
962
|
+
database** so `--jobs N` is safe under Rails for the first time — parallel
|
|
963
|
+
verdicts are proven identical to serial (no fixture cross-talk). Coverage
|
|
964
|
+
narrowing is restored on this path (each mutant runs only its covering tests;
|
|
965
|
+
a mutant on an uncovered line is `no_coverage`), so the daemon score is
|
|
966
|
+
comparable to the in-process `--rails` score. Opt in with `--rails --daemon`
|
|
967
|
+
(also `daemon: true` in `.mutineer.yml`); `--daemon` can't be combined with
|
|
968
|
+
`--test-command`. **SQLite** today (hermetic, CI-proven); **Postgres**
|
|
969
|
+
per-worker provisioning is in progress (#34/#35). The gem core stays Prism +
|
|
970
|
+
stdlib, zero runtime dependencies — worker-DB routing uses the app's own
|
|
971
|
+
ActiveRecord.
|
|
972
|
+
|
|
973
|
+
## [0.10.0] - 2026-07-02
|
|
974
|
+
|
|
975
|
+
### Added
|
|
976
|
+
- **`--test-command` external backend** (#27) — mutation-test apps pinned to Ruby
|
|
977
|
+
< 3.4. Mutineer stays on ≥ 3.4 but runs your suite as a subprocess in the app's
|
|
978
|
+
own runtime via `--test-command "bundle exec rails test %{files}"` (`%{files}`
|
|
979
|
+
expands to the `--test` paths; env is inherited). The mutant is applied on disk
|
|
980
|
+
with crash-safe backup/restore (self-heals a hard-killed run on next startup); a
|
|
981
|
+
smoke check aborts before scoring if the unmutated suite isn't green. This path
|
|
982
|
+
is reload-only, serial (`--jobs` forced to 1), and does no coverage narrowing —
|
|
983
|
+
so its score is an upper bound, not comparable to an in-process `--rails` score
|
|
984
|
+
(Mutineer prints this caveat). Also settable as `test_command:` in `.mutineer.yml`.
|
|
985
|
+
Safe parallelism for this path is tracked in #26.
|
|
986
|
+
|
|
987
|
+
## [0.9.1] - 2026-07-01
|
|
988
|
+
|
|
989
|
+
### Fixed
|
|
990
|
+
- **Per-method uncapturable granularity** (#25) — the `:uncapturable` taint was
|
|
991
|
+
whole-file, so a method reachable only by a *failed* capture in an otherwise-
|
|
992
|
+
covered file was mislabeled `no_coverage`. It's now attributed per method (by
|
|
993
|
+
the method's body coverage), so only the affected method is tainted. Fully-
|
|
994
|
+
failed files are unchanged.
|
|
995
|
+
|
|
996
|
+
## [0.9.0] - 2026-06-30
|
|
997
|
+
|
|
998
|
+
### Added
|
|
999
|
+
- **`--fail-fast`** (#21) — stop at the first surviving mutant; in-flight workers
|
|
1000
|
+
drain, the rest are skipped. Fast red signal for PR gates.
|
|
1001
|
+
- **`--format html`** (#23) — a single self-contained HTML report (inline CSS, no
|
|
1002
|
+
external assets) with the score, per-source table, and a card per survivor
|
|
1003
|
+
(subject, file:line, operator, stable id, diff).
|
|
1004
|
+
- **String, regex, and collection-method operators** (#24, Tier-2, opt-in via
|
|
1005
|
+
`--operators`): `string_literal`, `regex`, `collection_method`
|
|
1006
|
+
(`map`↔`each`, `all?`↔`any?`, `first`↔`last`, `min`↔`max`, `select`↔`reject`).
|
|
1007
|
+
|
|
1008
|
+
### Changed
|
|
1009
|
+
- **`--dry-run` now honors suppression** (#22) — inline `# mutineer:disable-line`
|
|
1010
|
+
and `.mutineer.yml` `ignore:` entries are omitted from the preview and counted
|
|
1011
|
+
as "ignored (suppressed)", matching a real run.
|
|
1012
|
+
|
|
1013
|
+
## [0.8.0] - 2026-06-30
|
|
1014
|
+
|
|
1015
|
+
### Fixed
|
|
1016
|
+
- **Singleton methods are now mutated** (#20) — `class << self` and
|
|
1017
|
+
`module_function` methods were discovered but applied to the instance scope, so
|
|
1018
|
+
the mutant never ran on the singleton the caller dispatches to; every such
|
|
1019
|
+
mutant falsely survived and the file read a false 0%. `module_function` methods
|
|
1020
|
+
are now discovered as singletons, and the redefine strategy re-opens
|
|
1021
|
+
`class << self` so the mutation lands on the called method. (Scores for
|
|
1022
|
+
singleton-heavy files will rise to their true values.)
|
|
1023
|
+
- **Write-heavy Rails tests are capturable again** (#19) — capture/worker pipes
|
|
1024
|
+
are `binmode`: a binary Marshal payload over a text-mode pipe could raise an
|
|
1025
|
+
encoding error the child then swallowed → empty pipe → false `:uncapturable`.
|
|
1026
|
+
That was the root cause of the residual write-heavy failures too. A child that
|
|
1027
|
+
dies without writing now also reports how it died (exit status / signal), and
|
|
1028
|
+
`--verbose` always surfaces a real reason. Verified on a real Rails app: all 6
|
|
1029
|
+
previously-uncapturable interactors (incl. caxlsx + Google-client) now capture
|
|
1030
|
+
with real scores, 0 uncapturable.
|
|
1031
|
+
|
|
1032
|
+
## [0.7.1] - 2026-06-30
|
|
1033
|
+
|
|
1034
|
+
### Added
|
|
1035
|
+
- **GitHub Action** (`action.yml`, composite) wrapping the CLI for CI — gate a PR
|
|
1036
|
+
on new survivors / score drop with `sources`, `since`, `baseline`, `threshold`,
|
|
1037
|
+
etc. Inputs are passed via `env` (no `${{ }}` interpolation into the run script)
|
|
1038
|
+
for command-injection safety.
|
|
1039
|
+
- Docs site (GitHub Pages) with Open Graph / Twitter Card share image; YARD doc
|
|
1040
|
+
comments across the library.
|
|
1041
|
+
|
|
1042
|
+
## [0.7.0] - 2026-06-30
|
|
1043
|
+
|
|
1044
|
+
Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
|
|
1045
|
+
|
|
1046
|
+
### Added
|
|
1047
|
+
- **Equivalent-mutant suppression** (#10) — inline `# mutineer:disable-line [ops]`
|
|
1048
|
+
and a `.mutineer.yml` `ignore:` list keyed on a stable, offset-free mutant id;
|
|
1049
|
+
suppressed mutants are excluded from the score (100% reachable). The stable id
|
|
1050
|
+
(and readable token) is emitted per survivor in JSON.
|
|
1051
|
+
- **Source→test auto-pairing + multi-source runs** (#11) — pass a directory or
|
|
1052
|
+
several sources with `--test` omitted; tests are inferred by convention
|
|
1053
|
+
(`app/`,`lib/` → `test/…_test.rb` / `spec/…_spec.rb`) and run under one boot,
|
|
1054
|
+
with per-source results (human + JSON `per_source`).
|
|
1055
|
+
- **`--baseline <file.json>` CI gating** (#13) — diff against a prior run by stable
|
|
1056
|
+
id; exit 1 on new survivors or a score drop (with `--baseline-epsilon`), naming
|
|
1057
|
+
what regressed. Combines with `--threshold` via max exit code.
|
|
1058
|
+
- **`--verbose`/`--debug`** (#8) — surface the real error when a fork capture fails.
|
|
1059
|
+
- **`:uncapturable` status** (#9) — distinct from `no_coverage`; reported separately
|
|
1060
|
+
("tests failed to run" vs "genuinely uncovered"). Both excluded from the score.
|
|
1061
|
+
|
|
1062
|
+
### Fixed
|
|
1063
|
+
- **Fork capture no longer drops fixture transactions** (#8) — `reconnect` skips
|
|
1064
|
+
`clear_all_connections!` when a fixture transaction is open, and stops swallowing
|
|
1065
|
+
the child error; write-heavy Rails tests are mutation-testable again.
|
|
1066
|
+
|
|
1067
|
+
### Changed
|
|
1068
|
+
- JSON `schema_version` → `1.1` (additive: survivor `id`/`token`, `ignored`,
|
|
1069
|
+
`uncapturable`, `per_source`).
|
|
1070
|
+
|
|
1071
|
+
## [0.6.2] - 2026-06-29
|
|
1072
|
+
|
|
1073
|
+
### Fixed
|
|
1074
|
+
- **`--rails` defaults `RAILS_ENV` to `test`** (#7) — an unset `RAILS_ENV` booted
|
|
1075
|
+
development, where the suite isn't loaded, so every mutant was falsely reported
|
|
1076
|
+
`no_coverage` (score N/A, exit 0). An explicit `RAILS_ENV` is respected.
|
|
1077
|
+
- **`--rails` defaults to `--jobs 1`** (#12) — parallel mutant forks share one
|
|
1078
|
+
database and deadlock on transactional fixtures; explicit `--jobs N` opts back
|
|
1079
|
+
into parallelism.
|
|
1080
|
+
|
|
1081
|
+
### Added
|
|
1082
|
+
- **Tier-2 operator discoverability** (#14) — the human-format run summary now
|
|
1083
|
+
lists the available opt-in tier-2 operators and how to enable them.
|
|
1084
|
+
|
|
1085
|
+
## [0.6.1] - 2026-06-29
|
|
1086
|
+
|
|
1087
|
+
### Changed
|
|
1088
|
+
- **Removed `eval` entirely** — the redefine strategy now `load`s the wrapped
|
|
1089
|
+
method snippet from a tempfile instead of evaluating a string. Behavior is
|
|
1090
|
+
identical (top-level load rebuilds the same `Module.nesting`), but the gem no
|
|
1091
|
+
longer uses dynamic string execution, clearing supply-chain scanner flags.
|
|
1092
|
+
Zero runtime dependencies unchanged.
|
|
1093
|
+
|
|
1094
|
+
## [0.6.0] - 2026-06-28
|
|
1095
|
+
|
|
1096
|
+
### Added
|
|
1097
|
+
- **RSpec support** (#6) — `--framework rspec` (or auto-detected when most
|
|
1098
|
+
`--test` files end in `_spec.rb`) runs RSpec suites instead of Minitest, via a
|
|
1099
|
+
pluggable test-runner abstraction. Both frameworks are loaded lazily, so
|
|
1100
|
+
Mutineer keeps zero runtime gem dependencies and works in an rspec-only
|
|
1101
|
+
project; coverage selection works for both. `.mutineer.yml` accepts `framework:`.
|
|
1102
|
+
|
|
1103
|
+
### Fixed
|
|
1104
|
+
- **Redefine strategy keeps compact `class A::B` as a single nesting wrapper**
|
|
1105
|
+
(#5) — avoids a constant-resolution disagreement with the reload strategy.
|
|
1106
|
+
|
|
1107
|
+
## [0.5.0] - 2026-06-28
|
|
1108
|
+
|
|
1109
|
+
### Fixed
|
|
1110
|
+
- **`class << self` methods are now discovered and mutated** (#3) — previously
|
|
1111
|
+
they were treated as instance methods, so the redefine strategy mis-targeted
|
|
1112
|
+
them. `class << other_obj` blocks are skipped (not representable).
|
|
1113
|
+
- **Worker pool no longer deadlocks on large results** (#4) — pipes are drained
|
|
1114
|
+
with `IO.select` and children reaped on EOF, so a result bigger than the OS
|
|
1115
|
+
pipe buffer (~64KB) can't wedge the run.
|
|
1116
|
+
|
|
1117
|
+
## [0.4.0] - 2026-06-28
|
|
1118
|
+
|
|
1119
|
+
### Added
|
|
1120
|
+
- **`--since <git-ref>`** (#2) — mutate only the lines changed since a git ref
|
|
1121
|
+
(e.g. `--since origin/main`), so CI on a pull request mutation-tests just the
|
|
1122
|
+
new/changed code. Composes with coverage selection; `--dry-run --since`
|
|
1123
|
+
narrows the preview too. Unknown ref / not-a-git-repo exits 2.
|
|
1124
|
+
|
|
1125
|
+
## [0.3.0] - 2026-06-28
|
|
1126
|
+
|
|
1127
|
+
### Added
|
|
1128
|
+
- **Coverage-guided test selection in boot mode** (#1) — `--rails`/`--boot` now
|
|
1129
|
+
captures coverage by forking the booted app and runs only the test files that
|
|
1130
|
+
cover each mutant's line (uncovered lines report `no_coverage`), instead of
|
|
1131
|
+
running every `--test` file for every mutant. Cached like standalone mode.
|
|
1132
|
+
|
|
1133
|
+
## [0.2.0] - 2026-06-28
|
|
1134
|
+
|
|
1135
|
+
### Added
|
|
1136
|
+
- **Boot mode for Rails (and any app needing its environment booted)** —
|
|
1137
|
+
`--rails` boots `config/environment` once in the parent and forks per mutant
|
|
1138
|
+
(children inherit the booted app), defaults the strategy to `redefine`, and
|
|
1139
|
+
reconnects ActiveRecord in each fork for DB fork-safety. `--boot FILE` boots a
|
|
1140
|
+
custom entry point. Boot mode requires `--test` files and runs them for every
|
|
1141
|
+
mutant (coverage-guided selection in boot mode is future work). `.mutineer.yml`
|
|
1142
|
+
accepts `boot:` and `rails:`.
|
|
1143
|
+
- GitHub Actions CI (test suite + gem build on Ruby 3.4, ubuntu + macos).
|
|
1144
|
+
|
|
1145
|
+
### Changed
|
|
1146
|
+
- `--strategy` values are now `reload` / `redefine` (canonical); `7a` / `7b`
|
|
1147
|
+
remain accepted as deprecated aliases.
|
|
1148
|
+
|
|
1149
|
+
## [0.1.0] - 2026-06-28
|
|
1150
|
+
|
|
1151
|
+
### Added
|
|
1152
|
+
- Initial release of Mutineer — a clean-room, Prism-based mutation-testing tool
|
|
1153
|
+
for Ruby with zero runtime dependencies (Ruby ≥ 3.4).
|
|
1154
|
+
- Mutation operators: arithmetic, comparison, boolean-connector, boolean-literal,
|
|
1155
|
+
statement-removal (Tier 1, default); return-nil, literal-mutation,
|
|
1156
|
+
condition-negation (Tier 2, opt-in via `--operators`).
|
|
1157
|
+
- Coverage-guided test selection with a digest-keyed, auto-invalidating cache.
|
|
1158
|
+
- Fork-isolated, parallel execution (`--jobs`) with per-mutant timeouts.
|
|
1159
|
+
- Two application strategies: `reload` (whole-file, default) and `redefine`
|
|
1160
|
+
(surgical), verified to agree on namespaced multi-statement methods. (`7a`/`7b`
|
|
1161
|
+
accepted as deprecated aliases.)
|
|
1162
|
+
- `run`, `--dry-run`, `--threshold`, `--only`, `--operators`, `--strategy`,
|
|
1163
|
+
`--format human|json`, `--output`, `--list-operators`.
|
|
1164
|
+
- `.mutineer.yml` configuration (CLI > config > default precedence).
|
|
1165
|
+
- Byte-correct source handling for multibyte (UTF-8) sources.
|
|
1166
|
+
|
|
1167
|
+
[1.6.0]: https://github.com/davidteren/mutineer/releases/tag/v1.6.0
|
|
1168
|
+
[1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
|
|
1169
|
+
[1.4.0]: https://github.com/davidteren/mutineer/releases/tag/v1.4.0
|
|
1170
|
+
[1.3.0]: https://github.com/davidteren/mutineer/releases/tag/v1.3.0
|
|
1171
|
+
[1.2.0]: https://github.com/davidteren/mutineer/releases/tag/v1.2.0
|
|
1172
|
+
[1.1.0]: https://github.com/davidteren/mutineer/releases/tag/v1.1.0
|
|
1173
|
+
[1.0.2]: https://github.com/davidteren/mutineer/releases/tag/v1.0.2
|
|
1174
|
+
[1.0.1]: https://github.com/davidteren/mutineer/releases/tag/v1.0.1
|
|
1175
|
+
[1.0.0]: https://github.com/davidteren/mutineer/releases/tag/v1.0.0
|
|
1176
|
+
[0.11.4]: https://github.com/davidteren/mutineer/releases/tag/v0.11.4
|
|
1177
|
+
[0.11.3]: https://github.com/davidteren/mutineer/releases/tag/v0.11.3
|
|
1178
|
+
[0.11.2]: https://github.com/davidteren/mutineer/releases/tag/v0.11.2
|
|
1179
|
+
[0.11.1]: https://github.com/davidteren/mutineer/releases/tag/v0.11.1
|
|
1180
|
+
[0.11.0]: https://github.com/davidteren/mutineer/releases/tag/v0.11.0
|
|
1181
|
+
[0.10.0]: https://github.com/davidteren/mutineer/releases/tag/v0.10.0
|
|
1182
|
+
[0.9.1]: https://github.com/davidteren/mutineer/releases/tag/v0.9.1
|
|
1183
|
+
[0.9.0]: https://github.com/davidteren/mutineer/releases/tag/v0.9.0
|
|
1184
|
+
[0.8.0]: https://github.com/davidteren/mutineer/releases/tag/v0.8.0
|
|
1185
|
+
[0.7.1]: https://github.com/davidteren/mutineer/releases/tag/v0.7.1
|
|
1186
|
+
[0.7.0]: https://github.com/davidteren/mutineer/releases/tag/v0.7.0
|
|
1187
|
+
[0.6.2]: https://github.com/davidteren/mutineer/releases/tag/v0.6.2
|
|
1188
|
+
[0.6.1]: https://github.com/davidteren/mutineer/releases/tag/v0.6.1
|
|
1189
|
+
[0.6.0]: https://github.com/davidteren/mutineer/releases/tag/v0.6.0
|
|
1190
|
+
[0.5.0]: https://github.com/davidteren/mutineer/releases/tag/v0.5.0
|
|
1191
|
+
[0.4.0]: https://github.com/davidteren/mutineer/releases/tag/v0.4.0
|
|
1192
|
+
[0.3.0]: https://github.com/davidteren/mutineer/releases/tag/v0.3.0
|
|
1193
|
+
[0.2.0]: https://github.com/davidteren/mutineer/releases/tag/v0.2.0
|
|
1194
|
+
[0.1.0]: https://github.com/davidteren/mutineer/releases/tag/v0.1.0
|