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/README.md
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
# Mutineer Mongoid
|
|
2
|
+
|
|
3
|
+
**mutineer-mongoid** is a fork of [Mutineer](https://github.com/davidteren/mutineer)
|
|
4
|
+
adding parallel mutation testing for Rails applications using Mongoid and RSpec.
|
|
5
|
+
It retains upstream Minitest, RSpec, and SQLite functionality; Mongoid support is
|
|
6
|
+
an addition, not a replacement for those backends. This fork is based on v1.6.0.
|
|
7
|
+
It adds Mongoid 9 / RSpec execution with `--rails --daemon --jobs N` and keeps the
|
|
8
|
+
`mutineer` executable and Ruby namespace. The original MIT license and author
|
|
9
|
+
attribution are preserved. Development is maintained by Justin Allen Marsh.
|
|
10
|
+
|
|
11
|
+
## Migrating from mutineer-gaia
|
|
12
|
+
|
|
13
|
+
Version 2.0.0 renames the gem, repository, and action to `mutineer-mongoid`.
|
|
14
|
+
Replace `mutineer-gaia` in your Gemfile, run `bundle install`, and update action
|
|
15
|
+
references to `justinallenmarsh/mutineer-mongoid@v2.0.0`. The command remains
|
|
16
|
+
`bundle exec mutineer`; configuration and behavior are unchanged. The existing
|
|
17
|
+
v1.7.0 tag and its `mutineer-gaia-1.7.0.gem` release asset remain available.
|
|
18
|
+
GAIA is the application where this integration was developed and tested; the
|
|
19
|
+
fork contains no GAIA application code and can be used by other applications.
|
|
20
|
+
|
|
21
|
+
The new action release uses `v2`; `v1` remains on the original release.
|
|
22
|
+
GitHub does not redirect action calls after a repository rename: update the
|
|
23
|
+
repository path in every workflow, including workflows pinned to v1.7.0 or a SHA.
|
|
24
|
+
|
|
25
|
+
## Compatibility
|
|
26
|
+
|
|
27
|
+
| Configuration | Serial execution | Parallel daemon execution |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| Plain Ruby / Minitest 5 and 6 | Supported | Requires an application boot entry point |
|
|
30
|
+
| Plain Ruby / RSpec 3 | Supported | Mongoid applications only |
|
|
31
|
+
| Rails / ActiveRecord / SQLite / Minitest | Supported | Supported; inherited upstream integration tests |
|
|
32
|
+
| Rails / Mongoid 9 / RSpec 3 | Supported | Supported; Mongoid 9.0 and 9.1 integration tests |
|
|
33
|
+
| Rails / ActiveRecord / SQLite / RSpec | Supported | Not supported |
|
|
34
|
+
| Rails / ActiveRecord / PostgreSQL or MySQL | Serial backend available; adapter not integration-tested here | Not supported |
|
|
35
|
+
|
|
36
|
+
Ruby 3.4+ and Linux or macOS are required for the Mutineer process. The
|
|
37
|
+
`--test-command` backend can run suites under an older application Ruby.
|
|
38
|
+
`--matrix` works with Minitest and RSpec on the serial in-process backend;
|
|
39
|
+
it cannot be combined with `--daemon` or `--test-command`.
|
|
40
|
+
The supported Mongoid isolation scope and other limitations are listed below.
|
|
41
|
+
|
|
42
|
+
## Install the fork
|
|
43
|
+
|
|
44
|
+
Use this gem in place of `mutineer` or `mutineer-gaia`; install only one of them
|
|
45
|
+
because they provide the same `mutineer` executable and Ruby namespace:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
gem "mutineer-mongoid", git: "https://github.com/justinallenmarsh/mutineer-mongoid.git", tag: "v2.0.0", require: false
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
bundle exec mutineer run app/models/example.rb --rails --daemon --jobs 2
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The gem is available as an attachment on the [GitHub release](https://github.com/justinallenmarsh/mutineer-mongoid/releases/tag/v2.0.0).
|
|
56
|
+
It has not been published to RubyGems. The GitHub action builds and installs the
|
|
57
|
+
source at the action ref, so it needs no separate gem publication:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- uses: actions/checkout@v7
|
|
61
|
+
- uses: ruby/setup-ruby@v1
|
|
62
|
+
with:
|
|
63
|
+
ruby-version: "3.4"
|
|
64
|
+
bundler-cache: true
|
|
65
|
+
- uses: justinallenmarsh/mutineer-mongoid@v2.0.0
|
|
66
|
+
with:
|
|
67
|
+
sources: app/models/example.rb
|
|
68
|
+
test: spec/models/example_spec.rb
|
|
69
|
+
framework: rspec
|
|
70
|
+
rails: "true"
|
|
71
|
+
daemon: "true"
|
|
72
|
+
jobs: "2"
|
|
73
|
+
output: mutation-report.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Start MongoDB and configure the application's test client before this step.
|
|
77
|
+
For example, a GitHub-hosted Linux job can provide `mongo:8.2` through a service
|
|
78
|
+
container exposing port 27017. Use `use-bundler: "true"` when the application's
|
|
79
|
+
Gemfile includes this fork. The default standalone mode also supports a daemon
|
|
80
|
+
booting a separate application bundle. Pin an immutable commit SHA instead of a
|
|
81
|
+
tag when your dependency policy requires it.
|
|
82
|
+
|
|
83
|
+
Mongoid daemon runs capture the boot-time test database's documents, collection
|
|
84
|
+
options, and indexes in memory. Each coverage check and mutant gets a fresh,
|
|
85
|
+
randomly named database. The parent drops it after reaping the child, including
|
|
86
|
+
on timeout or error. The source database is read only. Coverage caches include the seed snapshot.
|
|
87
|
+
MongoDB connection failures inside examples or suite hooks are reported as errors,
|
|
88
|
+
not caught mutations. Mongoid clients disconnect
|
|
89
|
+
before forks and reconnect in children. Credentials must allow creation and
|
|
90
|
+
deletion of the disposable databases. A hard kill of the parent can leave one
|
|
91
|
+
behind; these have names matching `mutineer_<8 hex source hash>_<16 hex run token>_<child pid>`.
|
|
92
|
+
|
|
93
|
+
Supported scope: one default Mongoid client, ordinary collections, and
|
|
94
|
+
`RAILS_ENV=test`. Multiple clients, views, time-series collections, and mixed
|
|
95
|
+
ActiveRecord/Mongoid applications fail explicitly. Raw `Mongo::Client` instances
|
|
96
|
+
outside Mongoid, cached database handles created during boot, and specs that
|
|
97
|
+
reconfigure Mongoid are outside this isolation contract. Redis, object storage,
|
|
98
|
+
and other external resources keep the application's existing isolation rules.
|
|
99
|
+
`--matrix` remains available on the serial in-process backend. Daemon execution
|
|
100
|
+
still reloads whole source files, with upstream's callback and concern limitations.
|
|
101
|
+
|
|
102
|
+
To check the MongoDB integration independently of an application:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
BUNDLE_GEMFILE=test/fixtures/mongoid_app/Gemfile bundle install
|
|
106
|
+
# MongoDB must be running; set MUTINEER_MONGO_URL if it requires credentials.
|
|
107
|
+
bundle exec rake test:mongoid
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The integration tests check concurrent writes, seed data, unique indexes, cleanup
|
|
111
|
+
on timeout and error, source-data preservation, and serial/parallel report parity
|
|
112
|
+
with both killed and surviving mutations. Mongoid is only in the fixture bundle;
|
|
113
|
+
the gem still has zero runtime dependencies.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## General Mutineer usage
|
|
118
|
+
|
|
119
|
+
The following reference retains upstream behavior except where the compatibility
|
|
120
|
+
table and Mongoid notes above describe this fork's additions.
|
|
121
|
+
|
|
122
|
+
A clean-room mutation-testing tool for Ruby. Mutineer mutates your source one
|
|
123
|
+
change at a time, runs your test suite (Minitest or RSpec) against each mutant, and reports the
|
|
124
|
+
ones your tests failed to catch — the gaps where your suite isn't actually
|
|
125
|
+
testing anything.
|
|
126
|
+
|
|
127
|
+
"Clean-room" means Mutineer was written from scratch. It contains no code from
|
|
128
|
+
[Mutant](https://github.com/mbj/mutant) or any other mutation-testing tool.
|
|
129
|
+
Mutineer's own code is licensed under MIT. See
|
|
130
|
+
[Mutineer and Mutant](https://github.com/davidteren/mutineer#mutineer-and-mutant)
|
|
131
|
+
for how the two tools differ.
|
|
132
|
+
|
|
133
|
+
- **Prism + stdlib only** — zero runtime dependencies (Ruby ≥ 3.4).
|
|
134
|
+
- **One mutation per mutant**, validity-checked by re-parsing.
|
|
135
|
+
- **Fork-isolated**, parallel execution (Linux + macOS).
|
|
136
|
+
- **Coverage-guided** — each mutant runs only the test files that cover its line.
|
|
137
|
+
- **Stops at the first failing test** — in-process runs (not `--daemon` or
|
|
138
|
+
`--test-command`) stop a mutant's test run at the first failure, unless
|
|
139
|
+
`--matrix` asks for every covering test. The source's paired test files run
|
|
140
|
+
first, then the rest from fastest to slowest in doubling steps (under 1 s,
|
|
141
|
+
1 to 3 s, 3 to 7 s, and so on); files in one step run in path order.
|
|
142
|
+
|
|
143
|
+
📖 **[mutineer.github.io →](https://davidteren.github.io/mutineer/)** — overview, operators, and usage.
|
|
144
|
+
|
|
145
|
+
## Usage
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
mutineer run <source...> --test <test> [--test <test>...] [options]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Mutate `lib/calculator.rb`, checking it against its test, and fail CI if the
|
|
152
|
+
mutation score drops below 90%:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Options
|
|
159
|
+
|
|
160
|
+
| Flag | Meaning |
|
|
161
|
+
|------|---------|
|
|
162
|
+
| `--test FILE` | Test file covering the sources; one file per flag, so repeat it for each (`--test a_test.rb --test b_test.rb`) |
|
|
163
|
+
| `--operators LIST` | Comma-separated operator names (default: the Tier-1 set) |
|
|
164
|
+
| `--threshold FLOAT` | Exit 1 when the score is below FLOAT, or when nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted (default: 0 = off) |
|
|
165
|
+
| `--only NAME` | Restrict to one fully-qualified subject, e.g. `Calculator#add` |
|
|
166
|
+
| `--framework NAME` | `minitest` (default) or `rspec`; auto-detected as rspec when most `--test` files end in `_spec.rb` |
|
|
167
|
+
| `--since REF` | Only mutate lines changed since git `REF` (e.g. `origin/main`). Untracked files are scored in full unless Git ignores them. |
|
|
168
|
+
| `--no-since` | Disable diff scoping; a typed no beats a `.mutineer.yml` `since:` key |
|
|
169
|
+
| `--baseline FILE` | Compare against a prior `--format json` run; exit 1 on new survivors / score drop (score drop is skipped under `--since`, whose score covers a different denominator; see [CI](https://github.com/davidteren/mutineer#ci-gating)) |
|
|
170
|
+
| `--baseline-epsilon FLOAT` | Score-drop tolerance for `--baseline` (default: 0) |
|
|
171
|
+
| `--jobs N` | Parallel worker count (default: processor count); forced to `1` by `--test-command`, `--fail-fast`, or `--rails` without `--daemon` |
|
|
172
|
+
| `--boot FILE` | Require an app entry point once before forking; select at least one test file |
|
|
173
|
+
| `--rails` | Boot `config/environment` and reconnect ActiveRecord per fork; without `--daemon`, defaults to `redefine` and runs serially |
|
|
174
|
+
| `--verbose` | Surface the real error when a fork capture fails (alias `--debug`) |
|
|
175
|
+
| `--strategy NAME` | Mutation application: `reload` whole-file (default) or `redefine` surgical (`7a`/`7b` accepted as deprecated aliases) |
|
|
176
|
+
| `--timeout SECONDS` | Per-mutant time limit for in-process runs, in whole seconds (default: 10). A mutant whose tests run longer is a `timeout`; see [Timeouts and the score](https://github.com/davidteren/mutineer#timeouts-and-the-score). `--daemon` and `--test-command` keep their own limits |
|
|
177
|
+
| `--capture-timeout SECONDS` | Time limit for each in-process coverage-capture subprocess and for the clean run of the unmutated tests, in whole seconds (default: 120). A suite slower than this stops the run as not green. `--daemon` uses it for its coverage capture; `--test-command` ignores it and says so |
|
|
178
|
+
| `--cache-dir DIR` | Directory for the coverage cache (default: `.mutineer`). Give two runs from the same project root different directories so they do not share `coverage.json`. `--test-command` builds no cache, so it ignores it and says so |
|
|
179
|
+
| `--test-command CMD` | Run the suite as a subprocess in the app's own runtime (for apps on Ruby < 3.4); `CMD` must contain `%{files}`. See [Apps on Ruby < 3.4](https://github.com/davidteren/mutineer#apps-on-ruby--34) |
|
|
180
|
+
| `--daemon` | Boot the app once in a persistent daemon and fork per mutant, with per-worker DB isolation so `--jobs N` is safe under Rails (needs `--rails`/`--boot`; not with `--test-command`). See [the daemon backend](https://github.com/davidteren/mutineer#faster-parallel-safe-rails-the---daemon-backend) |
|
|
181
|
+
| `--format human\|json\|html` | Report format (default: human; `html` is a self-contained file) |
|
|
182
|
+
| `--output FILE` | Write the report to FILE instead of stdout |
|
|
183
|
+
| `--dry-run` | List candidate mutations without executing (honors suppression) |
|
|
184
|
+
| `--fail-fast` | Stop at the first surviving mutant (`--no-fail-fast` beats a `.mutineer.yml` `fail_fast:` key) |
|
|
185
|
+
| `--matrix` | Run every covering test for each mutant and report the blind and redundant tests; the JSON report also lists each mutant's killers. In-process only; exits 2 with `--daemon`, `--test-command`, `--fail-fast` or `--dry-run`. RSpec needs 3.3 or later. `--no-matrix` beats a `.mutineer.yml` `matrix:` key. See [Kill matrix](https://github.com/davidteren/mutineer#kill-matrix) |
|
|
186
|
+
| `--list-operators` | List available operators (default vs optional) and exit |
|
|
187
|
+
| `--version`, `--help` | Print version / usage and exit |
|
|
188
|
+
|
|
189
|
+
### Exit codes
|
|
190
|
+
|
|
191
|
+
<!-- contract:exit-codes -->
|
|
192
|
+
| Code | Meaning |
|
|
193
|
+
|------|---------|
|
|
194
|
+
| `0` | Score ≥ threshold (or no gate) **and** no baseline regression. |
|
|
195
|
+
| `1` | Score below `--threshold`, OR nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted, OR a `--baseline` regression, OR a runtime error. |
|
|
196
|
+
| `2` | Usage / invalid-flag error (mistyped flag, bad path, unreadable baseline). |
|
|
197
|
+
<!-- /contract:exit-codes -->
|
|
198
|
+
|
|
199
|
+
### Operators
|
|
200
|
+
|
|
201
|
+
Run `mutineer --list-operators` to see them. Default (Tier 1): `arithmetic`,
|
|
202
|
+
`comparison`, `boolean_connector`, `boolean_literal`, `statement_removal`.
|
|
203
|
+
Available but off by default (Tier 2, enable via `--operators`): `return_nil`,
|
|
204
|
+
`literal_mutation`, `condition_negation`, `string_literal`, `regex`,
|
|
205
|
+
`collection_method`, `safe_navigation`, `range`, `negation_removal`, `chain_link`,
|
|
206
|
+
`operand_removal`, `array_literal`, `condition_true`,
|
|
207
|
+
`condition_false`, `operator_assignment`.
|
|
208
|
+
|
|
209
|
+
## Rails apps
|
|
210
|
+
|
|
211
|
+
Rails code needs its environment booted before the suite runs, so point Mutineer
|
|
212
|
+
at your app with `--rails` and run it inside the project's bundle:
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
RAILS_ENV=test bundle exec mutineer run \
|
|
216
|
+
app/models/order.rb --test test/models/order_test.rb --rails
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`--rails` boots `config/environment` once in the parent process (every mutant
|
|
220
|
+
then forks and inherits it), defaults `--strategy` to `redefine` without `--daemon`
|
|
221
|
+
(surgical — it avoids reloading files into the app tree; `--daemon` keeps `reload`), and reconnects ActiveRecord in each
|
|
222
|
+
fork so the database connection is fork-safe. Use `--boot FILE` to boot a
|
|
223
|
+
different entry point. Boot mode requires at least one `--test` file and is
|
|
224
|
+
coverage-guided — each mutant runs only the test files that exercise its line
|
|
225
|
+
(coverage is captured by forking the booted app, then cached).
|
|
226
|
+
|
|
227
|
+
Some code runs while the app boots or a class loads, for example a class body
|
|
228
|
+
that calls a method to build a constant, a `to_prepare` initializer, or a
|
|
229
|
+
`--require` file that calls a source method. That run
|
|
230
|
+
happens before the mutant is applied, and the forked test does not repeat it.
|
|
231
|
+
A mutant on such a line is `ran_at_load`, not `survived` or `no_coverage`. It is
|
|
232
|
+
left out of the score and does not fail `--threshold`; a kill still counts.
|
|
233
|
+
This holds under both strategies: `reload` runs the mutated file's class body
|
|
234
|
+
again, but not an initializer or another file's code, so a survivor on such a
|
|
235
|
+
line is not trusted there either.
|
|
236
|
+
Run those mutants with `--test-command`, which boots a fresh process per mutant,
|
|
237
|
+
to get a verdict.
|
|
238
|
+
|
|
239
|
+
Add Mutineer to your Gemfile's test group:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
gem "mutineer-mongoid", git: "https://github.com/justinallenmarsh/mutineer-mongoid.git", tag: "v2.0.0", group: :test, require: false
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Faster, parallel-safe Rails (the `--daemon` backend)
|
|
246
|
+
|
|
247
|
+
`--rails` boots your app once but runs mutants **serially** — parallel `--jobs`
|
|
248
|
+
under Rails is unsafe, because every worker shares one test database and clobbers
|
|
249
|
+
the others' fixtures. `--daemon` fixes both: it boots the app once in a persistent
|
|
250
|
+
helper and forks per mutant, and gives **each parallel worker its own database**,
|
|
251
|
+
so `--jobs N` is safe and its verdicts are proven identical to a serial run.
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
RAILS_ENV=test bundle exec mutineer run \
|
|
255
|
+
app/models/order.rb --test test/models/order_test.rb \
|
|
256
|
+
--rails --daemon --jobs 4
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- **One boot, forked per mutant** — restores the shared-boot speed.
|
|
260
|
+
- **Coverage-guided** — each mutant runs only its covering tests (like `--rails`);
|
|
261
|
+
a mutant on an uncovered line is `no_coverage`, so the score stays comparable to
|
|
262
|
+
the in-process `--rails` score.
|
|
263
|
+
- **Safe `--jobs N`** — each worker routes to its own copy of the test database, so
|
|
264
|
+
parallel verdicts equal serial (no fixture cross-talk). On first use, a
|
|
265
|
+
worker's database is a copy of the test database after the app boots, so rows
|
|
266
|
+
written by initializers or `--require` files are there, as in-process. When
|
|
267
|
+
the copy's schema differs from `db/schema.rb` (another schema version or
|
|
268
|
+
`schema_sha1` checksum, or no stored checksum, as in an empty or out-of-date
|
|
269
|
+
test database, or one set up with a plain `load "db/schema.rb"`), the
|
|
270
|
+
worker loads `db/schema.rb` over it, which drops the copied rows of the
|
|
271
|
+
tables it defines.
|
|
272
|
+
- **One backend at a time** — `--daemon` can't be combined with `--test-command`
|
|
273
|
+
(choose one), and it needs an app to boot (`--rails` or `--boot`).
|
|
274
|
+
|
|
275
|
+
ActiveRecord daemon status: **SQLite with Minitest** (hermetic, CI-proven).
|
|
276
|
+
This fork also supports Mongoid 9 with RSpec as described above.
|
|
277
|
+
Per-worker provisioning for
|
|
278
|
+
**Postgres** and other adapters is not supported: on those, `--daemon` scores
|
|
279
|
+
every mutant as `error`. Use `--daemon` with a SQLite test database, or drop
|
|
280
|
+
`--daemon` to run serially on other adapters.
|
|
281
|
+
|
|
282
|
+
### Apps on Ruby < 3.4
|
|
283
|
+
|
|
284
|
+
Mutineer's own process needs Ruby ≥ 3.4 (it parses with stdlib Prism), and the
|
|
285
|
+
`--rails` path above boots your app *inside Mutineer's process* — so it can't run
|
|
286
|
+
against an app pinned to an older Ruby (`ruby "3.1.6"` in the Gemfile), where the
|
|
287
|
+
bundle rejects 3.4.
|
|
288
|
+
|
|
289
|
+
`--test-command` decouples the two: Mutineer stays on ≥ 3.4, but your suite runs
|
|
290
|
+
as a **subprocess in your app's own runtime** (whatever Ruby its bundle resolves
|
|
291
|
+
to). Run Mutineer with a 3.4+ Ruby and hand it the command that runs your tests:
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
RAILS_ENV=test mutineer run app/models/order.rb \
|
|
295
|
+
--test test/models/order_test.rb \
|
|
296
|
+
--test-command "bundle exec rails test %{files}"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
- **`%{files}`** is required; it expands to the `--test` paths as separate
|
|
300
|
+
arguments (a path with a space stays one argument — there is no shell).
|
|
301
|
+
- **Environment:** vars like `RAILS_ENV` / `DATABASE_URL` set on the Mutineer
|
|
302
|
+
command are inherited. Mutineer **unsets** `BUNDLE_*`, `GEM_*`, `RUBY*`,
|
|
303
|
+
`RBENV_VERSION`, `ASDF_RUBY_VERSION`, and `RBENV_DIR` in the child (so
|
|
304
|
+
Mutineer's own Ruby cannot pin the suite), and drops version-manager
|
|
305
|
+
**version bins** (e.g. `~/.rbenv/versions/3.4.x/bin`) from `PATH`, then
|
|
306
|
+
prepends rbenv/asdf shims when a pin was scrubbed. Do not rely on
|
|
307
|
+
`RBENV_VERSION=…` on the Mutineer command for the suite; use `.ruby-version`
|
|
308
|
+
or a wrapper. Don't put `KEY=val` prefixes *inside* `--test-command` (no shell;
|
|
309
|
+
that would be treated as the program name).
|
|
310
|
+
|
|
311
|
+
#### Under a version manager (rbenv / asdf / chruby)
|
|
312
|
+
|
|
313
|
+
Automatic scrub targets **rbenv** and **asdf** (shims + version bins). **chruby**
|
|
314
|
+
has no shims: Mutineer still strips `…/rubies/…/bin` so it cannot leave Mutineer's
|
|
315
|
+
Ruby pinned, but you need a wrapper that sources chruby and selects the app
|
|
316
|
+
version. If the smoke check still reports a **Ruby version mismatch**, wrap the
|
|
317
|
+
suite. Example rbenv wrapper (`bin/mutineer-test` in the app):
|
|
318
|
+
|
|
319
|
+
```sh
|
|
320
|
+
#!/usr/bin/env bash
|
|
321
|
+
set -euo pipefail
|
|
322
|
+
cd "$(dirname "$0")/.."
|
|
323
|
+
unset GEM_HOME GEM_PATH RUBYLIB RUBYOPT BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLER_VERSION
|
|
324
|
+
export RBENV_VERSION="$(cat .ruby-version 2>/dev/null || true)"
|
|
325
|
+
export RAILS_ENV="${RAILS_ENV:-test}"
|
|
326
|
+
export PATH="${HOME}/.rbenv/shims:${PATH}"
|
|
327
|
+
exec bundle exec rails test "$@"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
# Mutineer on 3.4+; suite on the app's Ruby via the wrapper
|
|
332
|
+
mutineer run app/models/order.rb \
|
|
333
|
+
--test test/models/order_test.rb \
|
|
334
|
+
--test-command "bin/mutineer-test %{files}"
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Mutineer also surfaces a targeted smoke-check message when Bundler prints
|
|
338
|
+
`RubyVersionMismatch`, instead of only blaming DB/migrations.
|
|
339
|
+
|
|
340
|
+
Tradeoffs — this path is correct but not free:
|
|
341
|
+
|
|
342
|
+
- **Slower:** your app re-boots for every mutant (no shared boot yet).
|
|
343
|
+
- **No coverage narrowing:** every mutant runs the *full* `--test` set, so the
|
|
344
|
+
score is an **upper bound and not comparable to an in-process (`--rails`)
|
|
345
|
+
score** — uncovered mutants count as survivors, and an infrastructure failure
|
|
346
|
+
is scored as a kill. Mutineer prints this caveat on every run and aborts up
|
|
347
|
+
front (a "smoke check") if your unmutated suite isn't green.
|
|
348
|
+
- **Reload strategy only** (`--strategy redefine` is rejected on this path) and
|
|
349
|
+
**serial** (`--jobs` is forced to 1). For apps on Ruby ≥ 3.4, `--daemon` gives
|
|
350
|
+
safe parallelism instead (see [the daemon backend](https://github.com/davidteren/mutineer#faster-parallel-safe-rails-the---daemon-backend)).
|
|
351
|
+
|
|
352
|
+
## Timeouts and the score
|
|
353
|
+
|
|
354
|
+
The mutation score is `killed / (killed + survived)`. A mutant whose tests run
|
|
355
|
+
past `--timeout` is a `timeout`. It is neither killed nor survived, so it is
|
|
356
|
+
left out of the score, like `no_coverage`, `uncapturable`, `unplaceable`, `ran_at_load`, `errored`, skipped
|
|
357
|
+
and ignored mutants. A timeout is not counted as a kill, because a hang the
|
|
358
|
+
mutant caused and a suite that is just slow look the same. The human report
|
|
359
|
+
shows the count in its `Timeout:` row, and the JSON report in `summary.timeout`.
|
|
360
|
+
|
|
361
|
+
Under `--threshold`, a timeout is a mutant with no verdict. The run exits 1 when
|
|
362
|
+
more than one mutant produced no verdict and they exceed 10% of those attempted,
|
|
363
|
+
or when nothing could be scored and something broke. If a slow suite times out
|
|
364
|
+
mutants that its tests would catch, raise `--timeout`. A limit set below what
|
|
365
|
+
the suite needs does not raise the score: it turns those mutants into timeouts,
|
|
366
|
+
and once they pass 10% of the attempted mutants the run fails.
|
|
367
|
+
|
|
368
|
+
## Suppressing equivalent mutants
|
|
369
|
+
|
|
370
|
+
Some mutants are equivalent (behaviour-identical) and survive forever — keeping a
|
|
371
|
+
file off 100%. Suppress them so the score and `--threshold` gate stay meaningful:
|
|
372
|
+
|
|
373
|
+
- **Inline:** `some_line # mutineer:disable-line` (or scope it: `# mutineer:disable-line comparison`). Put a reason after `--`: `# mutineer:disable-line comparison -- the test checks only 20`.
|
|
374
|
+
- **Config:** a `.mutineer.yml` `ignore:` list of mutant ids. Each survivor's
|
|
375
|
+
`id` is printed in the JSON report, so copy it straight into `ignore:`.
|
|
376
|
+
|
|
377
|
+
Suppressed mutants are excluded from the score (so 100% becomes reachable).
|
|
378
|
+
|
|
379
|
+
## Mutant ids
|
|
380
|
+
|
|
381
|
+
A mutant id is 12 hex characters. It hashes the file path (relative to the
|
|
382
|
+
project root), the method's qualified name, the operator, the mutated code, and
|
|
383
|
+
the mutant's position among identical mutants in that method. When one file has
|
|
384
|
+
two methods with the same qualified name (for example two top-level `def index`
|
|
385
|
+
in two DSL blocks), the second and later ones also hash their position among
|
|
386
|
+
those methods, so their ids differ. The first one's id does not change. An edit
|
|
387
|
+
outside the method does not change the id. Moving or renaming the file,
|
|
388
|
+
renaming the method or its class, or adding an identical mutant earlier in the
|
|
389
|
+
method does. Adding a method with the same name earlier in the same file also
|
|
390
|
+
does.
|
|
391
|
+
|
|
392
|
+
- The project root is the directory mutineer runs from (in the Action, the
|
|
393
|
+
`working-directory`). Run from the same root to get the same ids.
|
|
394
|
+
- A source outside the project root uses its absolute path, so its ids differ
|
|
395
|
+
between machines.
|
|
396
|
+
|
|
397
|
+
**Migrating from ids without the file path.** Before 1.3, ids did not include
|
|
398
|
+
the file path, so two files could share an id (#126). Old-format ids keep
|
|
399
|
+
working in this fork, with a migration warning:
|
|
400
|
+
|
|
401
|
+
- **`ignore:`** An old entry still suppresses its mutants. The run prints the
|
|
402
|
+
new ids for each old entry, each with its file and method. When it names one
|
|
403
|
+
mutant, replace the entry with that id. When it names several (in different
|
|
404
|
+
files, or same-named methods in one file), the old entry over-matched: it also
|
|
405
|
+
hid mutants you did not mean to ignore. The warning says so. Keep only the ids for the mutant you meant to
|
|
406
|
+
ignore, not all of them. The list covers only the sources and operators in
|
|
407
|
+
that run, so run over every source with every operator set you use (for
|
|
408
|
+
example your Tier-2 `--operators`) for the full list.
|
|
409
|
+
- **`--baseline`** An old baseline still matches: a survivor matches a stored
|
|
410
|
+
one with the same old id in the same file. A stored file that is an absolute
|
|
411
|
+
path outside the project root (a baseline written on another machine)
|
|
412
|
+
matches on the old id alone. The run tells you to
|
|
413
|
+
regenerate it. Regenerate it with `--format json`, but only after every gate
|
|
414
|
+
that reads it runs 1.3 or later (the Action's `version:` pin, your CI
|
|
415
|
+
`Gemfile.lock`). An older version treats every new-format survivor as new.
|
|
416
|
+
|
|
417
|
+
The JSON report's `summary.id_format` is `2` for the new format.
|
|
418
|
+
`summary.legacy_id_matches.ignore` counts the old-format ignore entries a run
|
|
419
|
+
matched, and `summary.legacy_id_matches.baseline` counts the survivors matched
|
|
420
|
+
only through an old baseline id.
|
|
421
|
+
|
|
422
|
+
Ids are relative to the directory you run mutineer from. mutineer finds
|
|
423
|
+
`.mutineer.yml` by walking up. When the file it loads is in a parent directory
|
|
424
|
+
(other than your home directory), it warns that the ignore ids will not match
|
|
425
|
+
and tells you which directory to run from.
|
|
426
|
+
|
|
427
|
+
## Kill matrix
|
|
428
|
+
|
|
429
|
+
`--matrix` reports which tests kill each mutant, and from that, which tests
|
|
430
|
+
add nothing to the suite.
|
|
431
|
+
|
|
432
|
+
```sh
|
|
433
|
+
mutineer run lib/calculator.rb --test test/calculator_test.rb --matrix
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
For each mutant, Mutineer runs every test in its covering files instead of
|
|
437
|
+
stopping at the first failure, and records which tests failed. Coverage is
|
|
438
|
+
recorded per test file, so that set also holds tests that never reach the
|
|
439
|
+
mutated line; they pass and count as having run. A mutant's row is complete
|
|
440
|
+
when its whole covering set ran and the suite returned normally (under
|
|
441
|
+
Minitest, an `Interrupt` that cut the run short does not count as returning
|
|
442
|
+
normally). The report
|
|
443
|
+
names two kinds of test:
|
|
444
|
+
|
|
445
|
+
- **Blind:** the test ran in at least one complete row and killed no mutant
|
|
446
|
+
in any row. It would still pass with this run's mutations of the code, so
|
|
447
|
+
it guards none of them. It can still check behavior outside this run's
|
|
448
|
+
mutants (code in other files, or edits no operator makes), so look at what
|
|
449
|
+
it asserts before you delete it, or give it an assertion that can fail.
|
|
450
|
+
- **Redundant:** every mutant the test kills, another test kills too. That
|
|
451
|
+
makes it a candidate for deletion, one test at a time: two redundant tests
|
|
452
|
+
can be the only killers of one mutant, so re-run after each deletion.
|
|
453
|
+
|
|
454
|
+
The answers cover this run's mutants only. A test of code outside the sources
|
|
455
|
+
you passed kills nothing here, so mutate the code a test exercises before you
|
|
456
|
+
call the test blind.
|
|
457
|
+
|
|
458
|
+
The matrix changes no verdict, score or exit code: each mutant gets the
|
|
459
|
+
verdict a run without `--matrix` gives. That run skips every later test,
|
|
460
|
+
test class and example group after the first failure, so when a later one
|
|
461
|
+
exits the process, crashes or runs into the time limit, the mutant is still
|
|
462
|
+
`killed`. The run without `--matrix` still runs the code around the failing
|
|
463
|
+
test (the rest of its Minitest class `run` wrapper, the `after(:all)` hooks of
|
|
464
|
+
its RSpec groups) and the suite's own cleanup (RSpec's `after(:suite)`), so an
|
|
465
|
+
end there keeps the exit status. A failure in a Minitest `parallelize_me!`
|
|
466
|
+
class also keeps it: those run after every serial class and cannot be stopped
|
|
467
|
+
once queued, while a failure in a serial class still stops the run.
|
|
468
|
+
|
|
469
|
+
Under RSpec, `--matrix` needs RSpec 3.3 or later, whose example ids tell
|
|
470
|
+
apart examples on one line. With an older RSpec every row is incomplete.
|
|
471
|
+
|
|
472
|
+
The human report lists up to 20 blind and 20 redundant tests, and the HTML
|
|
473
|
+
report lists them all. `--format json` adds a `matrix` block with every test
|
|
474
|
+
and each mutant's killers (see the
|
|
475
|
+
[JSON schema](https://davidteren.github.io/mutineer/json-schema.html#matrix)).
|
|
476
|
+
A test is its file and its id. The name is `CalculatorTest#test_add` under
|
|
477
|
+
Minitest, or the example's full description under RSpec, where the example id
|
|
478
|
+
(`./spec/calc_spec.rb[1:2]`) tells apart examples that share a description
|
|
479
|
+
and keeps an example whose generated description changes with the mutant as
|
|
480
|
+
one test.
|
|
481
|
+
|
|
482
|
+
Each mutant runs its whole covering set, so every mutant costs what a survivor
|
|
483
|
+
costs. In 1.4, stopping at the first failure cut a full run of rack's
|
|
484
|
+
`lib/rack/utils.rb` from 86 to 89 seconds down to 35 to 41, so expect a matrix
|
|
485
|
+
run to take 2.1 to 2.5 times as long as a normal one. A mutant that reaches the
|
|
486
|
+
per-mutant time limit (`--timeout`, 10 seconds by default) after a test already failed stays `killed`
|
|
487
|
+
with its row marked incomplete. The report names each incomplete row and warns
|
|
488
|
+
that a blind test may have killed one of those mutants, so do not delete a
|
|
489
|
+
blind test until its rows are complete. Raise `--timeout` (or `timeout:` in
|
|
490
|
+
`.mutineer.yml`) to get complete rows.
|
|
491
|
+
|
|
492
|
+
`--matrix` runs on the in-process backend only. It exits 2 with `--daemon`,
|
|
493
|
+
`--test-command`, `--fail-fast` or `--dry-run`, and the message says whether
|
|
494
|
+
each setting came from the command line or `.mutineer.yml`. To run one of
|
|
495
|
+
those with `matrix: true` in the file, pass `--no-matrix`; to run `--matrix`
|
|
496
|
+
with `fail_fast: true` in the file, pass `--no-fail-fast`.
|
|
497
|
+
|
|
498
|
+
## CI gating
|
|
499
|
+
|
|
500
|
+
Store a JSON run as a baseline, then fail the build only when a PR makes things
|
|
501
|
+
worse:
|
|
502
|
+
|
|
503
|
+
```sh
|
|
504
|
+
mutineer run app/ --baseline .mutineer/baseline.json # exit 1 on NEW survivors or a score drop
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
`--baseline` reports which survivors are new (by [mutant id](https://github.com/davidteren/mutineer#mutant-ids)) and any score drop. It
|
|
508
|
+
combines with `--threshold` (the worse of the two sets the exit code). Pass a
|
|
509
|
+
directory (or several sources) to audit a whole layer in one boot — tests are
|
|
510
|
+
auto-paired by convention and the report breaks down per source. A source
|
|
511
|
+
`app/foo/bar.rb` pairs with `test/foo/bar_test.rb` and with unclaimed
|
|
512
|
+
`test/foo/bar_*_test.rb` files in that directory, such as `bar_upsert_test.rb`.
|
|
513
|
+
It does not take `user_session_test.rb` when `user_session.rb` exists in the same directory.
|
|
514
|
+
It does not take `bar_upsert_guards_test.rb` when `bar_upsert.rb` exists.
|
|
515
|
+
A spec that already pairs is left as that one file.
|
|
516
|
+
|
|
517
|
+
### GitHub Action
|
|
518
|
+
|
|
519
|
+
This repo ships a composite action (`action.yml`) that wraps the CLI for CI:
|
|
520
|
+
|
|
521
|
+
```yaml
|
|
522
|
+
- uses: actions/checkout@v4
|
|
523
|
+
- uses: ruby/setup-ruby@v1
|
|
524
|
+
with: { ruby-version: "3.4", bundler-cache: true }
|
|
525
|
+
- uses: justinallenmarsh/mutineer-mongoid@v2
|
|
526
|
+
with:
|
|
527
|
+
sources: app/
|
|
528
|
+
baseline: .mutineer/baseline.json
|
|
529
|
+
threshold: "90"
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Default change:** on `pull_request` events (not `pull_request_target`) the
|
|
533
|
+
action scopes the run to the PR's changed lines, diffing against the PR's exact
|
|
534
|
+
base commit (fetched by the action itself when the checkout is shallow; falls
|
|
535
|
+
back to the base branch tip). Pass `since: none` for a full scan, or an
|
|
536
|
+
explicit `since:` ref (which needs `fetch-depth: 0` on checkout).
|
|
537
|
+
|
|
538
|
+
With the default JSON format the action also:
|
|
539
|
+
|
|
540
|
+
- writes a score summary to the job's step summary;
|
|
541
|
+
- annotates surviving mutants on the PR diff, up to 50 (`error` level when the
|
|
542
|
+
gate failed, `warning` when it passed);
|
|
543
|
+
- exposes the report path via the `report` output for later steps (with
|
|
544
|
+
`format: human`/`html` this needs the `output` input).
|
|
545
|
+
|
|
546
|
+
The CLI prints a progress line to the log at every 10% of the run, whatever
|
|
547
|
+
the format.
|
|
548
|
+
|
|
549
|
+
## For AI agents & pipelines
|
|
550
|
+
|
|
551
|
+
Mutineer is built for programmatic use — versioned JSON, [mutant ids](https://github.com/davidteren/mutineer#mutant-ids) that survive unrelated edits,
|
|
552
|
+
structured exit codes, and diff-scoped runs. See:
|
|
553
|
+
|
|
554
|
+
- **AI agents & CI recipes** — the agent inner-loop and CI-gate recipes (and how
|
|
555
|
+
to avoid infinite loops on equivalent mutants):
|
|
556
|
+
[rendered](https://davidteren.github.io/mutineer/agentic-coding.html) ·
|
|
557
|
+
[source](https://davidteren.github.io/mutineer/agentic-coding.md)
|
|
558
|
+
- **JSON schema reference** — the `--format json` schema and its versioning
|
|
559
|
+
contract:
|
|
560
|
+
[rendered](https://davidteren.github.io/mutineer/json-schema.html) ·
|
|
561
|
+
[source](https://davidteren.github.io/mutineer/json-schema.md)
|
|
562
|
+
- **Ruby API (YARD)** — class reference for the shipped gem:
|
|
563
|
+
[https://davidteren.github.io/mutineer/api/](https://davidteren.github.io/mutineer/api/)
|
|
564
|
+
- **Agent skill** ([`skills/mutineer/SKILL.md`](https://github.com/justinallenmarsh/mutineer-mongoid/blob/main/skills/mutineer/SKILL.md)): a short
|
|
565
|
+
card for coding agents with install, the agent loop, and exit codes. Install it
|
|
566
|
+
with `gh skill install justinallenmarsh/mutineer-mongoid mutineer` or
|
|
567
|
+
`npx skills add justinallenmarsh/mutineer-mongoid --skill mutineer`.
|
|
568
|
+
|
|
569
|
+
## Configuration
|
|
570
|
+
|
|
571
|
+
Mutineer reads an optional `.mutineer.yml` from the project root (nearest one,
|
|
572
|
+
walking up). CLI flags override config; config overrides defaults.
|
|
573
|
+
|
|
574
|
+
Sources are positional CLI arguments and test files come from `--test`. The
|
|
575
|
+
config file accepts these keys:
|
|
576
|
+
|
|
577
|
+
| Key | Value and purpose |
|
|
578
|
+
|-----|-------------------|
|
|
579
|
+
| `operators` | An operator name or list of names; defaults to the Tier-1 set |
|
|
580
|
+
| `threshold` | A number from 0 to 100; 0 turns the score gate off |
|
|
581
|
+
| `jobs` | A positive integer; the default is the processor count. `test_command`, `fail_fast`, or `--rails` without `--daemon` forces 1. |
|
|
582
|
+
| `only` | A fully-qualified subject name, such as `Calculator#add` |
|
|
583
|
+
| `require` | A path or list of extra files to load before mutating |
|
|
584
|
+
| `boot` | The app entry point to require once before forking |
|
|
585
|
+
| `rails` | `true` or `false`; enables the Rails boot defaults |
|
|
586
|
+
| `since` | A nonblank git ref, or `false` to disable diff scoping |
|
|
587
|
+
| `framework` | `minitest` or `rspec`; an explicit value is kept during test pairing |
|
|
588
|
+
| `verbose` | `true` or `false`; shows capture diagnostics |
|
|
589
|
+
| `ignore` | A mutant id or list of ids to suppress |
|
|
590
|
+
| `baseline` | The path to a prior JSON report |
|
|
591
|
+
| `fail_fast` | `true` or `false`; stops scheduling after the first survivor |
|
|
592
|
+
| `test_command` | The external-runtime suite command, including `%{files}`; see [Apps on Ruby < 3.4](https://github.com/davidteren/mutineer#apps-on-ruby--34) |
|
|
593
|
+
| `daemon` | `true` or `false`; uses the persistent app daemon with worker DB isolation |
|
|
594
|
+
| `timeout` | A positive integer; the per-mutant time limit in seconds (default 10) |
|
|
595
|
+
| `capture_timeout` | A positive integer; the coverage-capture time limit in seconds (default 120) |
|
|
596
|
+
| `cache_dir` | The coverage cache directory (default `.mutineer`) |
|
|
597
|
+
| `matrix` | `true` or `false`; runs every covering test for each mutant and adds the kill matrix to the report |
|
|
598
|
+
|
|
599
|
+
Invalid values for known scalar keys, and a blank `operators` list, exit 2
|
|
600
|
+
with a message naming the file and key. An unknown operator name warns and
|
|
601
|
+
is skipped. If none of the names are known, the run exits 2. An empty
|
|
602
|
+
`require` or `ignore` list is valid. Boolean keys take `true` or `false` (quoted forms also work), not `"yes"`.
|
|
603
|
+
`jobs` must be positive; a string value contains digits only. String values for
|
|
604
|
+
`threshold` and the CLI-only `--baseline-epsilon` use plain decimals such as `90`
|
|
605
|
+
or `0.5`, not `+2`, `1e2`, or `1_0`. String options such as `only` and `baseline`
|
|
606
|
+
cannot be null or boolean. A blank `since` is invalid; use `since: false` to turn
|
|
607
|
+
scoping off. Unknown keys warn and are ignored. Unknown operator names warn and are skipped, and the run exits 2 when none remain. `--operators` replaces a blank or unknown file list.
|
|
608
|
+
|
|
609
|
+
`format`, `strategy`, `output`, `baseline_epsilon`, and `dry_run` are CLI-only.
|
|
610
|
+
For JSON output, use `--format json`, not a `format:` config key. To select RSpec
|
|
611
|
+
in the file, add `framework: rspec`.
|
|
612
|
+
|
|
613
|
+
```yaml
|
|
614
|
+
# .mutineer.yml
|
|
615
|
+
operators: [arithmetic, comparison, boolean_connector, boolean_literal, statement_removal]
|
|
616
|
+
threshold: 90
|
|
617
|
+
jobs: 4
|
|
618
|
+
require:
|
|
619
|
+
- config/environment
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Coverage results are cached in `.mutineer/coverage.json` (digest-keyed; rebuilt
|
|
623
|
+
automatically when sources change). Add `.mutineer/` to your `.gitignore`, and
|
|
624
|
+
the directory you set with `--cache-dir` if it is inside the project. Keep
|
|
625
|
+
`.mutineer/` ignored even then: `--test-command` writes lock files to
|
|
626
|
+
`.mutineer/` beside each source file.
|
|
627
|
+
|
|
628
|
+
## Mutineer and Mutant
|
|
629
|
+
|
|
630
|
+
[Mutant](https://github.com/mbj/mutant), by Markus Schirp, is the established
|
|
631
|
+
mutation-testing tool for Ruby. It has been in development since 2012 and is
|
|
632
|
+
the subject of published research. Mutineer is a separate tool, and it shares
|
|
633
|
+
no code with Mutant.
|
|
634
|
+
|
|
635
|
+
| Aspect | Mutineer | Mutant |
|
|
636
|
+
|---|---|---|
|
|
637
|
+
| License | MIT, for every use | Free for open source (`--usage opensource`); commercial use needs a paid subscription |
|
|
638
|
+
| Runtime dependencies | None (Prism + stdlib) | `parser`, `unparser`, `regexp_parser`, `sorbet-runtime` and others |
|
|
639
|
+
| Ruby | 3.4 and later to run Mutineer; an app on an older Ruby works through `--test-command` (see [Apps on Ruby < 3.4](https://github.com/davidteren/mutineer#apps-on-ruby--34)) | 3.3 and later |
|
|
640
|
+
| Test frameworks | Minitest and RSpec, in one gem | RSpec, Minitest and Test::Unit, one integration gem each |
|
|
641
|
+
| What you mutate | Files (`mutineer run lib/foo.rb --test test/foo_test.rb`), narrowed to one method with `--only 'Foo#bar'` | Subjects named by expression (`mutant run 'Foo#bar'`, `'Foo*'`) |
|
|
642
|
+
| Which tests run | The test files whose coverage reaches the mutated line | The tests that declare the subject (an RSpec description or a Minitest `cover`), or the tests that ran it in a per-test coverage recording |
|
|
643
|
+
| Operators | 20 (5 by default, 15 opt-in) | A larger set; the default `light` set applies almost all of it (`full` adds `#==` to `#eql?`) |
|
|
644
|
+
| CI gating | `--threshold`, `--baseline` deltas, `--since`, and a GitHub Action | Incremental mode (`--since`) and a recorded session history |
|
|
645
|
+
| Machine-readable output | Versioned JSON report schema | Session JSON schema |
|
|
646
|
+
|
|
647
|
+
Choose Mutineer when you want an MIT-licensed tool with no extra gems in your
|
|
648
|
+
bundle, file-based runs, and a JSON contract built for CI gates and AI agents.
|
|
649
|
+
Choose Mutant when you need its deeper operator set, Test::Unit support, or
|
|
650
|
+
subject expressions that select whole namespaces. The two tools do not
|
|
651
|
+
conflict, so you can run both on one project.
|
|
652
|
+
|
|
653
|
+
This comparison describes Mutant 0.17 (checked on 2026-10-06). Check
|
|
654
|
+
[Mutant's README](https://github.com/mbj/mutant#readme) for its current
|
|
655
|
+
license terms and features.
|
|
656
|
+
|
|
657
|
+
## License
|
|
658
|
+
|
|
659
|
+
MIT — see [LICENSE](https://github.com/davidteren/mutineer/blob/main/LICENSE).
|