mutineer 1.5.0 → 1.6.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 +225 -0
- data/README.md +28 -3
- data/lib/mutineer/cli.rb +5 -4
- data/lib/mutineer/coverage_map.rb +372 -86
- data/lib/mutineer/daemon_backend.rb +48 -33
- data/lib/mutineer/daemon_client.rb +42 -9
- data/lib/mutineer/daemon_server.rb +73 -27
- data/lib/mutineer/file_swap.rb +1 -1
- data/lib/mutineer/isolation.rb +1 -0
- data/lib/mutineer/job_plan.rb +352 -0
- data/lib/mutineer/minitest_integration.rb +48 -1
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/project.rb +337 -21
- data/lib/mutineer/rails_worker_db.rb +82 -8
- data/lib/mutineer/reporter.rb +20 -3
- data/lib/mutineer/result.rb +36 -4
- data/lib/mutineer/runner.rb +48 -284
- data/lib/mutineer/statement_lines.rb +40 -0
- data/lib/mutineer/subject.rb +6 -2
- data/lib/mutineer/test_runners/rspec.rb +39 -0
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eeac45268c75073f522daa5d26dd65fe425ae66b604cfb7543c0b529ab71f2e6
|
|
4
|
+
data.tar.gz: 834755a3b106f02013ef75b9515f18e692d1c6ac219bee42b837da8a6f696fc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '092252eef7f756e5fa117784b7c3696777127b0f1f1b96825c38d99a9f8d362a23416ad2351685e62bd780563a2b7fa2eab0e926c10bee3e293293ff88e0d28d'
|
|
7
|
+
data.tar.gz: 02344ad3f678a639a37f0272577e50e5f189e585a9246be0b0f1451202a03c63cb57e013181e349ab7b4db04f36fb2333e2de7d864063c4da92d4394f5dce6b3
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,230 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.6.0] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **An `unplaceable` mutant status.** Under `--strategy redefine`, a mutant in
|
|
14
|
+
a method whose class or module has no constant name is not run. It is left
|
|
15
|
+
out of the score and, unlike `uncapturable`, out of `no_verdict`, so it does
|
|
16
|
+
not fail `--threshold`. The human report gives it an `Unplaceable:` row, the
|
|
17
|
+
HTML report a count, and the JSON report `summary.unplaceable` and an
|
|
18
|
+
`unplaceable[]` list; `schema_version` is `1.7`.
|
|
19
|
+
- **The agent skill installs with `gh skill` and `npx skills` (#125).** It
|
|
20
|
+
moved from `docs/skill.md` to `skills/mutineer/SKILL.md`, the layout of the
|
|
21
|
+
Agent Skills specification, so `gh skill install davidteren/mutineer
|
|
22
|
+
mutineer` finds it and `npx skills add davidteren/mutineer --skill mutineer`
|
|
23
|
+
installs one `SKILL.md` instead of the whole `docs/` site. The site build
|
|
24
|
+
copies that file to `skill.md`, so its URL still works. The skill's
|
|
25
|
+
description now says when to use it, and the README shows how to install it.
|
|
26
|
+
- **A `ran_at_load` mutant status** (#187). A mutant on a method-body line
|
|
27
|
+
that ran while the app booted or its class loaded gets this status when it
|
|
28
|
+
survives, or when only the load ran the line. A kill stays a kill. It is
|
|
29
|
+
left out of the score and out of `no_verdict`, so it does not fail
|
|
30
|
+
`--threshold`. The human report gives it a `Ran at load:` row that points
|
|
31
|
+
to `--test-command` to verify these mutants, the HTML report a count, and
|
|
32
|
+
the JSON report `summary.ran_at_load` and a `ran_at_load[]` list, within
|
|
33
|
+
schema `1.7`.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Mutants no longer run under Coverage with `--boot` or `--rails` (#228).** Coverage
|
|
38
|
+
starts before the app boots so the coverage map can be built, and it used to
|
|
39
|
+
keep running in every mutant fork after that, though nothing read it. It is
|
|
40
|
+
now suspended once the map is built, from a fresh capture or from the cache.
|
|
41
|
+
Coverage that a host process started before the run is left running.
|
|
42
|
+
On a small app whose test calls tiny methods in a loop, the CPU time of a
|
|
43
|
+
whole in-process `--boot` run fell from 0.76s to 0.57s (median of five). On
|
|
44
|
+
the Rails fixture app the run is mostly boot, and the change is within noise
|
|
45
|
+
(1.61s before, 1.60s after). `--daemon` runs its mutants in worker daemons
|
|
46
|
+
that never start Coverage, so its time does not change (3.48s before, 3.44s
|
|
47
|
+
after); the map-building daemon now suspends Coverage after the map too.
|
|
48
|
+
- **A mutant runs its paired test files first, then the cheapest (#203).** The
|
|
49
|
+
covering test files used to run in the order the coverage map stored them,
|
|
50
|
+
so a mutant that one fast file kills could still score `timeout` when slow
|
|
51
|
+
files ran first. Now the files that pairing finds for the source run first,
|
|
52
|
+
then the rest from fastest to slowest by the time each file took during
|
|
53
|
+
coverage capture, less the fastest file's time (the startup cost each
|
|
54
|
+
capture pays). The order is fixed for a given cache. Costs compare in
|
|
55
|
+
doubling buckets, so files of close cost keep their path order when a
|
|
56
|
+
rebuilt cache measures them again; a file near a bucket edge can still move,
|
|
57
|
+
so keep `.mutineer/` between CI runs for the most stable `--baseline`. The
|
|
58
|
+
order applies to Minitest (after the seeded class shuffle, with
|
|
59
|
+
`parallelize_me!` classes still last) and to RSpec. `--daemon` runs every
|
|
60
|
+
covering test with no stop, so the order does not change its verdicts. The
|
|
61
|
+
coverage cache now saves these timings, so a cache from an earlier version
|
|
62
|
+
rebuilds once. A complete run gives the same verdicts and `--matrix` rows,
|
|
63
|
+
but the new order can change which mutants time out, so a `--baseline` gate
|
|
64
|
+
can see a one-time change on upgrade.
|
|
65
|
+
- **Job selection moved from `Mutineer::Runner` to a new `Mutineer::JobPlan`
|
|
66
|
+
module (#75).** The in-process runner, the `--daemon` backend and
|
|
67
|
+
`--dry-run` share it, so each file now requires what it uses and no
|
|
68
|
+
`require_relative` cycle exists in `lib/mutineer`. The moved methods are
|
|
69
|
+
`collect_jobs`, `filter_since`, `coverage_selection`, `ran_at_load?`,
|
|
70
|
+
`load_verdict`, `abort_if_unclean!`, `test_load_roots`, `source_dirs`,
|
|
71
|
+
`sweep_orphans`, `suppress_map`, `suppressed?` and `result_keys`. These were
|
|
72
|
+
internal methods, so `Runner` keeps no copies. Runs, reports and caches do
|
|
73
|
+
not change.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **An endless method that only the tests call is no longer `no_coverage`
|
|
78
|
+
under `--rails`, `--boot` or `--daemon` (#209).** Ruby counts no line when
|
|
79
|
+
an endless method such as `def half(x) = x / 2` runs, only its `def` line
|
|
80
|
+
when it is defined. The boot defines it before any test runs, so the forked
|
|
81
|
+
coverage capture never saw that line run. Coverage now also
|
|
82
|
+
counts methods (`Coverage.start(lines: true, methods: true)`), and a test
|
|
83
|
+
that calls such a method covers its `def` line. Coverage caches from earlier
|
|
84
|
+
versions rebuild once.
|
|
85
|
+
- **`module_function :name` in a reopened module inside `class << self`
|
|
86
|
+
promotes the method from the earlier opening (#216).** Ruby treats both
|
|
87
|
+
openings as one module, but Mutineer matched them only within one body, so
|
|
88
|
+
it named the method `#<Class:App>::M#c` instead of `#<Class:App>::M.c`.
|
|
89
|
+
The two openings now match by the module's full name. A module whose name
|
|
90
|
+
Mutineer cannot be sure of still matches only within its own body: a path
|
|
91
|
+
such as `self::X` or `Foo::X` written directly inside `class << self`, a
|
|
92
|
+
name under an anonymous class, or a module in a `class << self` inside a
|
|
93
|
+
method or a `class_eval` block. A `module self::X` in a block, or a
|
|
94
|
+
`self::X = ...` in a `class_eval` block, matches only openings in the same
|
|
95
|
+
block. A `self::X` like these now also stays apart from a
|
|
96
|
+
module of the same name outside `class << self`, where a `module_function`
|
|
97
|
+
in that module used to promote its methods.
|
|
98
|
+
- **A `module self::X` or `class self::X` in a block is `unplaceable` under
|
|
99
|
+
`--strategy redefine` (#229).** In a block, `self` is not always the
|
|
100
|
+
enclosing class: in `Other.class_eval do` it is `Other`, and in
|
|
101
|
+
`Foo = Class.new do` it is the new class. Mutineer gave these modules a
|
|
102
|
+
known owner on the enclosing namespace, so redefine loaded the mutant onto
|
|
103
|
+
a module the tests never use, and the mutant could falsely survive or
|
|
104
|
+
score `error`. Such a module, everything nested in it, and a
|
|
105
|
+
`self::X = ...` in a block that does not build a class now have
|
|
106
|
+
their owner unknown: redefine reports their mutants as `unplaceable`, and
|
|
107
|
+
`--strategy reload` still runs them. In a `Data.define`, `Struct.new`,
|
|
108
|
+
`Class.new` or `Module.new` block, the module is now named under the
|
|
109
|
+
builder's constant (`Outer::Foo::X#c`, was `Outer::X#c`), so the subject
|
|
110
|
+
names and mutant ids of those methods change. `self::X` directly in a class
|
|
111
|
+
or module body is unchanged.
|
|
112
|
+
|
|
113
|
+
- **`--daemon` worker databases start as a copy of the test database
|
|
114
|
+
(#222).** Each worker slot's SQLite database used to start from
|
|
115
|
+
`db/schema.rb` alone. Rows the daemon wrote while it booted, in
|
|
116
|
+
initializers or `--require` files, were only in the base test database, so
|
|
117
|
+
a test that read them passed in-process but failed under `--daemon`. On its
|
|
118
|
+
first use, a worker database is now a copy of the base test database (schema
|
|
119
|
+
and rows, made with the SQLite online backup API). The worker still loads
|
|
120
|
+
`db/schema.rb` when the copy's schema differs from it (another schema
|
|
121
|
+
version, or another or no `schema_sha1` checksum in `ar_internal_metadata`),
|
|
122
|
+
for example when the test database is empty, out of date, or set up with a
|
|
123
|
+
plain `load "db/schema.rb"`. That load drops the
|
|
124
|
+
copied rows of the tables it defines.
|
|
125
|
+
- **Standalone coverage capture loads the `--require` files (#217).** A run
|
|
126
|
+
without `--boot` requires the sources and then each `--require` (or
|
|
127
|
+
`require:`) file before it forks the mutants, but the coverage capture
|
|
128
|
+
loaded only the sources. A source method that a `--require` file called
|
|
129
|
+
while it loaded was then not seen as run at load, so its mutant was a false
|
|
130
|
+
`survived` instead of `ran_at_load`. The capture and the clean-suite check
|
|
131
|
+
now load the `--require` files after the sources. With or without
|
|
132
|
+
`--boot`, the coverage cache rebuilds when one of them changes, is added,
|
|
133
|
+
is removed, or moves in the load order.
|
|
134
|
+
- **`--daemon` loads the `--require` files (#220).** The daemon booted the app
|
|
135
|
+
but never loaded the `--require` (or `require:`) files, so a test that
|
|
136
|
+
needs one failed under `--daemon` and passed in-process. A source method
|
|
137
|
+
that such a file called while it loaded was also not seen as run at load,
|
|
138
|
+
so its mutant could be a false `survived` instead of `ran_at_load`. The
|
|
139
|
+
daemon now loads the files after the app boots, as an in-process run does,
|
|
140
|
+
and the daemon's coverage cache rebuilds when one of them changes.
|
|
141
|
+
- **The release workflow publishes only stable tags** (#161). Its trigger,
|
|
142
|
+
`v*.*.*`, also matches a prerelease tag such as `v1.3.1-rc1`, and the only
|
|
143
|
+
guard checked that the tag matched `Mutineer::VERSION`. A prerelease tag
|
|
144
|
+
could then publish the gem and move the floating major tag (`v1`) that
|
|
145
|
+
Action users pin. The workflow now stops with a clear error before any
|
|
146
|
+
publish step unless the tag is a stable `vMAJOR.MINOR.PATCH`.
|
|
147
|
+
- **Coverage capture keeps to `capture_timeout` from start to finish.** Under
|
|
148
|
+
`--boot` and `--daemon`, a hung test file ignored the timeout and blocked
|
|
149
|
+
the run; it is now skipped as "timed out", like a standalone capture. A
|
|
150
|
+
process that a test started and left running could keep the result pipe
|
|
151
|
+
open and make a boot-mode capture wait for it to exit. Each capture now runs
|
|
152
|
+
in its own process group, and a timeout stops the whole group, not only the
|
|
153
|
+
test process. A capture whose test finished just before the deadline could
|
|
154
|
+
also lose its valid result and report "invalid coverage output"; it now
|
|
155
|
+
gets a short grace period to read the result. (#101, #129)
|
|
156
|
+
- **`--daemon` no longer waits forever on a daemon that stops answering**
|
|
157
|
+
(#101). A mutant whose verdict has not arrived 30 seconds after its own
|
|
158
|
+
timeout is scored `error`, and the daemon is killed and respawned, as after
|
|
159
|
+
a crash. A boot that has not finished after 600 seconds ends the run with a
|
|
160
|
+
message that says so (it is not retried by a second daemon), and shutdown no longer waits on a stuck daemon.
|
|
161
|
+
|
|
162
|
+
- **A method in a `Data.define`, `Struct.new`, `Class.new` or `Module.new`
|
|
163
|
+
block belongs to the class or module the block builds.** In
|
|
164
|
+
`class App; Argo = Data.define(:url) do def self.load = ...; end; end` the
|
|
165
|
+
subject is now `App::Argo.load`, not `App.load`; at the top level it was
|
|
166
|
+
`.load`, with no owner. Under `--strategy redefine` (the `--rails` default)
|
|
167
|
+
the mutated method was loaded into the wrong class, so the real method never
|
|
168
|
+
changed and every such mutant falsely survived. The class or module is
|
|
169
|
+
named after the constant it is assigned to with `=`, `||=` or `&&=`, through
|
|
170
|
+
parentheses or `begin`, resolved as Ruby resolves the assignment: `X` in the
|
|
171
|
+
current namespace, `::X` and any path at the top level from `Object`,
|
|
172
|
+
`self::X` under the current class. When it cannot be named statically (no
|
|
173
|
+
constant, a relative path such as `User::Permission` inside a namespace, or
|
|
174
|
+
a constant assigned in `class << self`), redefine reports its mutants with
|
|
175
|
+
a new status, `unplaceable`, rather than load them onto the wrong class;
|
|
176
|
+
reload still runs them. `module_function :name` in a `Module.new` block
|
|
177
|
+
promotes that module's method. Mutant ids for these subjects change:
|
|
178
|
+
regenerate `ignore:` entries and baselines that name them.
|
|
179
|
+
- **The automated release PR gets its required CI checks without a token
|
|
180
|
+
secret (#85).** `release-pr.yml` pushes the release branch with the default
|
|
181
|
+
`GITHUB_TOKEN`, which starts no other workflow, so the four required checks
|
|
182
|
+
never ran and the PR stayed blocked until a person closed and reopened it.
|
|
183
|
+
The job now dispatches `ci.yml` on the release branch (CI gained a
|
|
184
|
+
`workflow_dispatch` trigger), and those runs report the required checks on
|
|
185
|
+
the PR's head commit. With a `RELEASE_PR_TOKEN` secret set, the push
|
|
186
|
+
triggers CI as before and no dispatch is made.
|
|
187
|
+
- **Code that runs while the app boots or a class loads no longer gives a false
|
|
188
|
+
`survived` or `no_coverage`** (#187). A class body such as
|
|
189
|
+
`ALL = [price(3)].freeze` runs `price` before any mutant is applied. Under
|
|
190
|
+
`--strategy redefine` (the `--rails` default) the load never runs again, so
|
|
191
|
+
a test that checks `ALL` saw the original value and the mutant falsely
|
|
192
|
+
survived. Under `--rails` or `--boot` no test was credited with lines that
|
|
193
|
+
ran during boot, so those mutants were falsely `no_coverage`. Mutineer now
|
|
194
|
+
reads the lines that ran at load (from the boot, or from requiring the
|
|
195
|
+
sources in standalone capture) and reports these mutants as `ran_at_load`,
|
|
196
|
+
under both strategies and with `--daemon`: reload runs the file's class body
|
|
197
|
+
again but not an initializer or another file's code, so a survivor there is
|
|
198
|
+
not trusted either. Under `--boot` with redefine, a
|
|
199
|
+
lazily loaded class (Zeitwerk, `autoload`) is loaded before the boot lines
|
|
200
|
+
are read, so its class body counts as load too. A baseline survivor that is
|
|
201
|
+
now `ran_at_load` is listed as fixed, not as a regression. Standalone
|
|
202
|
+
coverage caches from earlier versions rebuild once. A one-line or endless
|
|
203
|
+
`def` keeps its body on the `def` line, which Ruby counts when the method is
|
|
204
|
+
defined, so Mutineer reads the method's call count at load for it instead
|
|
205
|
+
(#209).
|
|
206
|
+
- **A boot file that prints to stdout no longer breaks the daemon handshake
|
|
207
|
+
(#102).** Output from `puts`, `STDOUT`, `$stdout`, a direct fd 1 write, or a
|
|
208
|
+
subprocess reached the JSON channel before the ready message, so the daemon
|
|
209
|
+
failed with "daemon exited before the handshake". The daemon now keeps a
|
|
210
|
+
private copy of its original stdout for the protocol and points the app's
|
|
211
|
+
stdout at stderr, where that output stays visible.
|
|
212
|
+
- **A class or module opened inside `class << self` belongs to the singleton
|
|
213
|
+
class (#208).** In `class App; class << self; class Q; def q1 ...` the
|
|
214
|
+
subject was `App::Q.q1`, a class method of a constant Ruby never defines:
|
|
215
|
+
`Q` is `App.singleton_class::Q`, and `q1` is an instance method. It is now
|
|
216
|
+
`#<Class:App>::Q#q1` with no known owner, as is a class or module opened in
|
|
217
|
+
a builder block there, and anything nested in either. Inside a builder
|
|
218
|
+
block, `class << self` opens the built class's singleton class, so a class
|
|
219
|
+
or constant there is named under it, for example `#<Class:App::P>::W`. Under
|
|
220
|
+
`--strategy redefine` loading the mutant raised `NameError` and scored
|
|
221
|
+
`error`; redefine now reports these mutants as `unplaceable`, and reload
|
|
222
|
+
still runs them. A compact `class Foo::X` and a top-level `class ::X` or
|
|
223
|
+
`::X = Class.new do` there are named as written (`::X` as `X`, now with
|
|
224
|
+
instance methods) and are owner-unknown too: redefine reopens them without
|
|
225
|
+
the singleton class, so constants their bodies look up through it would not
|
|
226
|
+
resolve. `module_function :name` in a body whose owner is unknown now
|
|
227
|
+
promotes that body's own methods, wherever it is: a `Foo::X = Module.new`
|
|
228
|
+
block's `c` is `Foo::X.c`, not `Foo::X#c`, and a def in an anonymous
|
|
229
|
+
`Class.new` block is no longer promoted by a `module_function` of the module
|
|
230
|
+
around it. Mutant ids for these subjects change: regenerate `ignore:`
|
|
231
|
+
entries and baselines that name them.
|
|
232
|
+
|
|
9
233
|
## [1.5.0] - 2026-10-06
|
|
10
234
|
|
|
11
235
|
### Added
|
|
@@ -926,6 +1150,7 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
|
|
|
926
1150
|
- `.mutineer.yml` configuration (CLI > config > default precedence).
|
|
927
1151
|
- Byte-correct source handling for multibyte (UTF-8) sources.
|
|
928
1152
|
|
|
1153
|
+
[1.6.0]: https://github.com/davidteren/mutineer/releases/tag/v1.6.0
|
|
929
1154
|
[1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
|
|
930
1155
|
[1.4.0]: https://github.com/davidteren/mutineer/releases/tag/v1.4.0
|
|
931
1156
|
[1.3.0]: https://github.com/davidteren/mutineer/releases/tag/v1.3.0
|
data/README.md
CHANGED
|
@@ -21,7 +21,9 @@ for how the two tools differ.
|
|
|
21
21
|
- **Coverage-guided** — each mutant runs only the test files that cover its line.
|
|
22
22
|
- **Stops at the first failing test** — in-process runs (not `--daemon` or
|
|
23
23
|
`--test-command`) stop a mutant's test run at the first failure, unless
|
|
24
|
-
`--matrix` asks for every covering test.
|
|
24
|
+
`--matrix` asks for every covering test. The source's paired test files run
|
|
25
|
+
first, then the rest from fastest to slowest in doubling steps (under 1 s,
|
|
26
|
+
1 to 3 s, 3 to 7 s, and so on); files in one step run in path order.
|
|
25
27
|
|
|
26
28
|
📖 **[mutineer.github.io →](https://davidteren.github.io/mutineer/)** — overview, operators, and usage.
|
|
27
29
|
|
|
@@ -119,6 +121,18 @@ different entry point. Boot mode requires at least one `--test` file and is
|
|
|
119
121
|
coverage-guided — each mutant runs only the test files that exercise its line
|
|
120
122
|
(coverage is captured by forking the booted app, then cached).
|
|
121
123
|
|
|
124
|
+
Some code runs while the app boots or a class loads, for example a class body
|
|
125
|
+
that calls a method to build a constant, a `to_prepare` initializer, or a
|
|
126
|
+
`--require` file that calls a source method. That run
|
|
127
|
+
happens before the mutant is applied, and the forked test does not repeat it.
|
|
128
|
+
A mutant on such a line is `ran_at_load`, not `survived` or `no_coverage`. It is
|
|
129
|
+
left out of the score and does not fail `--threshold`; a kill still counts.
|
|
130
|
+
This holds under both strategies: `reload` runs the mutated file's class body
|
|
131
|
+
again, but not an initializer or another file's code, so a survivor on such a
|
|
132
|
+
line is not trusted there either.
|
|
133
|
+
Run those mutants with `--test-command`, which boots a fresh process per mutant,
|
|
134
|
+
to get a verdict.
|
|
135
|
+
|
|
122
136
|
Add Mutineer to your Gemfile's test group:
|
|
123
137
|
|
|
124
138
|
```ruby
|
|
@@ -144,7 +158,14 @@ RAILS_ENV=test bundle exec mutineer run \
|
|
|
144
158
|
a mutant on an uncovered line is `no_coverage`, so the score stays comparable to
|
|
145
159
|
the in-process `--rails` score.
|
|
146
160
|
- **Safe `--jobs N`** — each worker routes to its own copy of the test database, so
|
|
147
|
-
parallel verdicts equal serial (no fixture cross-talk).
|
|
161
|
+
parallel verdicts equal serial (no fixture cross-talk). On first use, a
|
|
162
|
+
worker's database is a copy of the test database after the app boots, so rows
|
|
163
|
+
written by initializers or `--require` files are there, as in-process. When
|
|
164
|
+
the copy's schema differs from `db/schema.rb` (another schema version or
|
|
165
|
+
`schema_sha1` checksum, or no stored checksum, as in an empty or out-of-date
|
|
166
|
+
test database, or one set up with a plain `load "db/schema.rb"`), the
|
|
167
|
+
worker loads `db/schema.rb` over it, which drops the copied rows of the
|
|
168
|
+
tables it defines.
|
|
148
169
|
- **One backend at a time** — `--daemon` can't be combined with `--test-command`
|
|
149
170
|
(choose one), and it needs an app to boot (`--rails` or `--boot`).
|
|
150
171
|
|
|
@@ -227,7 +248,7 @@ Tradeoffs — this path is correct but not free:
|
|
|
227
248
|
|
|
228
249
|
The mutation score is `killed / (killed + survived)`. A mutant whose tests run
|
|
229
250
|
past `--timeout` is a `timeout`. It is neither killed nor survived, so it is
|
|
230
|
-
left out of the score, like `no_coverage`, `uncapturable`, `errored`, skipped
|
|
251
|
+
left out of the score, like `no_coverage`, `uncapturable`, `unplaceable`, `ran_at_load`, `errored`, skipped
|
|
231
252
|
and ignored mutants. A timeout is not counted as a kill, because a hang the
|
|
232
253
|
mutant caused and a suite that is just slow look the same. The human report
|
|
233
254
|
shows the count in its `Timeout:` row, and the JSON report in `summary.timeout`.
|
|
@@ -435,6 +456,10 @@ structured exit codes, and diff-scoped runs. See:
|
|
|
435
456
|
[source](https://davidteren.github.io/mutineer/json-schema.md)
|
|
436
457
|
- **Ruby API (YARD)** — class reference for the shipped gem:
|
|
437
458
|
[https://davidteren.github.io/mutineer/api/](https://davidteren.github.io/mutineer/api/)
|
|
459
|
+
- **Agent skill** ([`skills/mutineer/SKILL.md`](https://github.com/davidteren/mutineer/blob/main/skills/mutineer/SKILL.md)): a short
|
|
460
|
+
card for coding agents with install, the agent loop, and exit codes. Install it
|
|
461
|
+
with `gh skill install davidteren/mutineer mutineer` or
|
|
462
|
+
`npx skills add davidteren/mutineer --skill mutineer`.
|
|
438
463
|
|
|
439
464
|
## Configuration
|
|
440
465
|
|
data/lib/mutineer/cli.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "project"
|
|
|
9
9
|
require_relative "pairing"
|
|
10
10
|
require_relative "changed_lines"
|
|
11
11
|
require_relative "runner"
|
|
12
|
+
require_relative "job_plan"
|
|
12
13
|
require_relative "reporter"
|
|
13
14
|
require_relative "kill_matrix"
|
|
14
15
|
require_relative "baseline"
|
|
@@ -639,20 +640,20 @@ module Mutineer
|
|
|
639
640
|
"it runs this mutineer version or later."
|
|
640
641
|
end
|
|
641
642
|
|
|
642
|
-
# Runs dry-run mode. Reuses
|
|
643
|
+
# Runs dry-run mode. Reuses JobPlan.collect_jobs (+ filter_since) so the
|
|
643
644
|
# candidate list cannot drift from a real run's job selection.
|
|
644
645
|
#
|
|
645
646
|
# @param config [Mutineer::Config] run configuration.
|
|
646
647
|
# @return [void]
|
|
647
648
|
def self.dry_run(config)
|
|
648
649
|
operator_classes = MutatorRegistry.resolve(config.operators || MutatorRegistry::DEFAULT_NAMES)
|
|
649
|
-
jobs, ignored_results, source_map, extras =
|
|
650
|
+
jobs, ignored_results, source_map, extras = JobPlan.collect_jobs(config, operator_classes)
|
|
650
651
|
warn_legacy_ignore_matches(extras[:legacy_ignore_matches])
|
|
651
652
|
# Narrow jobs and ignored the same way so the summary matches the printed list.
|
|
652
653
|
if config.since
|
|
653
|
-
jobs =
|
|
654
|
+
jobs = JobPlan.filter_since(jobs, source_map, config)
|
|
654
655
|
ignored_jobs = ignored_results.map { |r| [r.subject, r.mutation, r.id] }
|
|
655
|
-
ignored =
|
|
656
|
+
ignored = JobPlan.filter_since(ignored_jobs, source_map, config).size
|
|
656
657
|
else
|
|
657
658
|
ignored = ignored_results.size
|
|
658
659
|
end
|