mutation_tester 1.4.2 → 1.5.1

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.
data/readme.md CHANGED
@@ -2,24 +2,54 @@
2
2
 
3
3
  [![CI](https://github.com/Oxyconit/mutation_tester/actions/workflows/ci.yml/badge.svg)](https://github.com/Oxyconit/mutation_tester/actions/workflows/ci.yml)
4
4
 
5
- Simple mutation testing framework for Ruby applications with RSpec or Minitest. Improve your AI workflow and test
6
- quality by identifying weak spots in your test suite through code mutations.
5
+ Simple mutation testing framework for Ruby applications with RSpec or Minitest. It mutates your code, runs your tests
6
+ against each mutant, and reports every change your tests failed to detect, so you (or your AI agent) know exactly which
7
+ test gaps to close.
8
+
9
+ ## Quick Start
7
10
 
8
11
  ```bash
9
- # Install, then mutation-test a file (its spec is found by convention):
12
+ # Install
10
13
  bundle add mutation_tester
14
+
15
+ # Test a file: its spec is found by convention (lib/X.rb -> spec/X_spec.rb)
11
16
  bundle exec mutation_test lib/calculator.rb
17
+
18
+ # Test several files in one aggregated run
19
+ bundle exec mutation_test lib/calculator.rb lib/parser.rb
20
+
21
+ # Test exactly what you have staged in git (the "test what I changed" flow)
22
+ bundle exec mutation_test --staged
23
+
24
+ # Minitest layout under test/
25
+ bundle exec mutation_test --spec-glob 'test/{name}_test.rb' lib/calculator.rb
26
+
27
+ # Or point at the test file explicitly
28
+ bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
12
29
  ```
13
30
 
14
- See [Getting Started](#getting-started) for the full quick start.
31
+ The gem will:
32
+
33
+ - ✅ Run your original tests to make sure they pass
34
+ - ✅ Generate mutations of your code
35
+ - ✅ Run tests against each mutation
36
+ - ✅ Generate reports showing which mutations survived
37
+
38
+ Press Ctrl+C at any time to stop early: the run cleans up its worker processes and temporary workspaces, prints a single
39
+ interruption line (no backtrace), and exits with status 130.
15
40
 
16
41
  ## Table of Contents
17
42
 
18
43
  - [Features](#features)
19
44
  - [Requirements and Compatibility](#requirements-and-compatibility)
20
45
  - [Installation](#installation)
21
- - [Getting Started](#getting-started)
22
46
  - [Usage](#usage)
47
+ - [Choosing what to test](#choosing-what-to-test)
48
+ - [Mapping sources to specs](#mapping-sources-to-specs)
49
+ - [CLI options](#cli-options)
50
+ - [Exit codes](#exit-codes)
51
+ - [Running with rake](#running-with-rake)
52
+ - [Programmatic usage](#programmatic-usage)
23
53
  - [Configuration](#configuration)
24
54
  - [Execution model](#execution-model)
25
55
  - [Mutation types](#mutation-types)
@@ -33,8 +63,8 @@ See [Getting Started](#getting-started) for the full quick start.
33
63
  - [License](#license)
34
64
 
35
65
  Reference material lives under [`docs/`](docs): the full
36
- [mutation catalog](docs/mutation-types.md), the [JSON report schema](docs/json-schema.md),
37
- and the [CI/CD recipes](docs/ci.md).
66
+ [mutation catalog](docs/mutation-types.md), the [execution runner internals](docs/execution-runners.md),
67
+ the [JSON report schema](docs/json-schema.md), and the [CI/CD recipes](docs/ci.md).
38
68
 
39
69
  ## Features
40
70
 
@@ -74,9 +104,6 @@ Notes:
74
104
 
75
105
  ## Installation
76
106
 
77
- MutationTester requires Ruby >= 3.0 (see [Requirements and Compatibility](#requirements-and-compatibility)). There are
78
- two supported ways to install it.
79
-
80
107
  ### In your project's bundle (recommended)
81
108
 
82
109
  Add it to your application's `Gemfile` and install in one step:
@@ -85,335 +112,149 @@ Add it to your application's `Gemfile` and install in one step:
85
112
  bundle add mutation_tester
86
113
  ```
87
114
 
88
- (this writes `gem "mutation_tester"` into your `Gemfile` and runs `bundle install`).
89
-
90
115
  Then run it through Bundler so it uses your project's locked dependency versions:
91
116
 
92
117
  ```bash
93
- bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
118
+ bundle exec mutation_test lib/calculator.rb
94
119
  ```
95
120
 
96
121
  ### As a global gem
97
122
 
98
- Install it once, system-wide:
123
+ Install it once, system-wide, and run `mutation_test` directly (no `bundle exec`):
99
124
 
100
125
  ```bash
101
126
  gem install mutation_tester
127
+ mutation_test lib/calculator.rb
102
128
  ```
103
129
 
104
- Then run the `mutation_test` command directly (no `bundle exec`):
105
-
106
- ```bash
107
- mutation_test app/models/user.rb spec/models/user_spec.rb
108
- ```
109
-
110
- This is convenient for a project that does not list the gem in its `Gemfile`.
111
- The CLI then cannot load itself from that project bundle, so it loads the
112
- globally installed gem *outside* the bundle and prints one line to stderr:
130
+ This is convenient for a project that does not list the gem in its `Gemfile`. The CLI then cannot load itself from that
131
+ project bundle, so it loads the globally installed gem *outside* the bundle and prints one line to stderr:
113
132
 
114
133
  ```
115
134
  mutation_tester loaded outside the project bundle
116
135
  ```
117
136
 
118
- In this fallback the gem and its own dependencies (parser, unparser, parallel,
119
- rainbow) come from the global install, so their versions may differ from your
120
- project's `Gemfile.lock`. Your own tests are unaffected: when your project has a
121
- `Gemfile`, each mutant still runs through `bundle exec`, in your project's
122
- environment. Running in a directory with no `Gemfile` at all works too and
123
- prints no notice. If your project *does* list `mutation_tester`, prefer
124
- `bundle exec mutation_test ...`, which runs fully inside your bundle with no
125
- notice.
126
-
127
- ## Getting Started
128
-
129
- Install the gem, then point `mutation_test` at one or more source files. Each
130
- file is mapped to its spec by convention (`lib/X.rb` -> `spec/X_spec.rb`,
131
- override with `--spec-glob`):
132
-
133
- ```bash
134
- # Install
135
- bundle add mutation_tester
136
-
137
- # Test a file: the spec is found by convention (lib/X.rb -> spec/X_spec.rb)
138
- bundle exec mutation_test lib/calculator.rb
139
-
140
- # Test several files in one aggregated run
141
- bundle exec mutation_test lib/calculator.rb lib/parser.rb
142
-
143
- # Test exactly what you have staged in git (the "test what I changed" flow)
144
- bundle exec mutation_test --staged
145
-
146
- # Minitest layout under test/
147
- bundle exec mutation_test --spec-glob 'test/{name}_test.rb' lib/calculator.rb
148
-
149
- # Or point at the test file explicitly: exactly two arguments, the second a test file
150
- bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
151
- bundle exec mutation_test lib/calculator.rb test/calculator_test.rb
152
-
153
- # Or with rake (non-Rails: first add `require 'mutation_tester/rake_task'` to your Rakefile)
154
- bundle exec rake "mutation_test[app/models/user.rb,spec/models/user_spec.rb]"
155
- ```
156
-
157
- That's it! 🎉 The gem will:
158
-
159
- - ✅ Run your original tests to make sure they pass
160
- - ✅ Generate mutations of your code
161
- - ✅ Run tests against each mutation
162
- - ✅ Generate reports showing which mutations survived
163
-
164
- Press Ctrl+C at any time to stop early: the run cleans up its worker processes and temporary workspaces, prints a single
165
- interruption line (no backtrace), and exits with status 130.
166
-
167
- See [Usage](#usage) for the full command reference and [Configuration](#configuration) to tune it.
137
+ In this fallback the gem and its own dependencies (parser, unparser, parallel, rainbow) come from the global install, so
138
+ their versions may differ from your project's `Gemfile.lock`. Your own tests are unaffected: when your project has a
139
+ `Gemfile`, each mutant still runs through `bundle exec`, in your project's environment. Running in a directory with no
140
+ `Gemfile` at all works too and prints no notice. If your project *does* list `mutation_tester`, prefer
141
+ `bundle exec mutation_test ...`, which runs fully inside your bundle with no notice.
168
142
 
169
143
  ## Usage
170
144
 
171
- `mutation_test` is the main CLI executable for running mutation tests. Pass one
172
- or more source files (specs mapped by convention), the git staging area, an
173
- explicit pair, or a glob:
174
-
175
145
  ```bash
176
- # Main interface: one or more source files, specs mapped by convention
146
+ # One or more source files, specs mapped by convention (the main interface)
177
147
  mutation_test [OPTIONS] FILE...
178
148
 
179
- # The files currently staged in git (see File lists and --staged below)
149
+ # The files currently staged in git
180
150
  mutation_test [OPTIONS] --staged
181
151
 
182
152
  # Explicit pair: exactly two arguments where the second is a test file
183
153
  mutation_test [OPTIONS] SOURCE_FILE TEST_FILE
184
154
 
185
- # Many files in one run: select sources with a glob (see Batch mode below)
155
+ # Many files in one run: select sources with a glob
186
156
  mutation_test [OPTIONS] --glob 'lib/**/*.rb'
187
157
  ```
188
158
 
189
- **Arguments:**
159
+ ### Choosing what to test
190
160
 
191
- - `FILE...` - Ruby source files to mutate; each is mapped to its spec by convention (`lib/X.rb` -> `spec/X_spec.rb`,
192
- override with `--spec-glob`). Files that cannot be mutated are reported as skipped with a reason (
193
- see [File lists and --staged](#file-lists-and---staged-test-what-you-changed)).
194
- - `SOURCE_FILE TEST_FILE` - Explicit pair: with exactly two arguments where the second is recognized as a test file (
195
- `*_spec.rb`, `*.spec.rb`, `*_test.rb`, `test_*.rb`, or a file requiring minitest), the second is used as the test file
196
- directly.
161
+ **File list (`FILE...`)** is the main interface. Each source file is mapped to its spec by convention (`lib/X.rb` ->
162
+ `spec/X_spec.rb`; see [Mapping sources to specs](#mapping-sources-to-specs) to override), and the whole list runs as one
163
+ aggregated batch: every file is processed, the summary shows one `PASS`/`FAIL` line per file, reports land in per-file
164
+ subdirectories under `--output-dir`, and the exit code reflects the whole run.
197
165
 
198
- ### CLI options
199
-
200
- | Flag | Description |
201
- |---|---|
202
- | `-p, --parallel N` | Run with N parallel processes (default: auto, derived from the CPU core count with a cap of 8; `-p 1` forces serial execution). |
203
- | `--runner MODE` | Mutant execution runner: `auto` (default) tries `in_memory` first (`Process.fork` available and a passing unmutated-source probe), then falls back to `fork`, then `spawn`, announcing every step down on stderr with its reason; `fork` (preloaded environment, on platforms with `Process.fork`), `spawn` (one full process per mutant) and `in_memory` (mutations applied in child-process memory, zero file writes per mutant) force the specific mode. See [Execution runners](#execution-runners-fork-spawn-in-memory). |
204
- | `--staged` | Mutation-test the files staged in git (`git diff --cached --name-only`; files staged as deleted are ignored), mapping each to its spec like a positional `FILE` list. Cannot be combined with positional arguments or `--glob`. See [File lists and --staged](#file-lists-and---staged-test-what-you-changed). |
205
- | `--glob PATTERN` | Batch mode: mutation-test every source file matching `PATTERN`, mapping each to its spec by convention (see [Batch mode](#batch-mode-run-many-files-in-one-command)). |
206
- | `--spec-glob TEMPLATE` | Spec-mapping template with a `{name}` placeholder (default: `spec/{name}_spec.rb`). Requires a positional `FILE` list, `--staged`, or `--glob`. |
207
- | `--spec-map RULE` | Spec-mapping rule `'PATTERN=>REPLACEMENT'`: a regular expression applied to the whole source path to build the whole spec path, for layouts a `{name}` template cannot express (Rails `app/` -> `test/`, engines, Packwerk packs). Repeatable, first matching rule wins, a source matching no rule falls back to `--spec-glob`. Requires a positional `FILE` list, `--staged`, or `--glob`. See [Mapping sources to specs](#mapping-sources-to-specs). |
208
- | `--minimum-score N` | Mutation score percentage a file must reach to pass (default: 80). Drives the `PASS`/`FAIL` verdict and the exit code. |
209
- | `--since REV` | Incremental batch mode: mutate only the files matched by `--glob` that changed since git revision `REV` (new files count as changed). Requires `--glob`. See [Incremental mode](#incremental-mode-mutate-only-what-changed). |
210
- | `--fail-fast` | Stop the run at the first surviving mutant and finish with a failing status. Works in single-file mode and with `--glob`. |
211
- | `--timeout-factor N` | Per-mutant timeout budget as `N` times the measured baseline test run, never below 5 s (default: 5, must be > 0). Ignored when `config.timeout` is set explicitly, which keeps a fixed budget. See [Configuration](#configuration). |
212
- | `--timeout-policy MODE` | Scoring policy for timed-out mutants: `killed` (default) counts a timeout as a kill; `separate` keeps timeouts out of the score entirely (`killed / (killed + survived)`) and reports them only as their own category in the console, JSON and HTML reports. |
213
- | `--worker-env NAME` | Set environment variable `NAME` to a distinct per-worker value before each parallel worker boots (`parallel_tests` `TEST_ENV_NUMBER` convention: worker 0 -> `""`, worker N -> `N+1`), so a `parallel_tests`-style `database.yml` selects a per-worker database. You provision the databases (e.g. `rake parallel:prepare`). Not supported by the `in_memory` runner (it falls back to `fork`). See [Making parallelism work with Rails](#making-parallelism-work-with-rails). |
214
- | `--strict-equality` | Enable the opt-in strict-equality probes (`==` → `eql?` and `==` → `equal?`). Default off; expect noise on code that does not distinguish numeric types or object identity. See [Strict Equality Mutations](docs/mutation-types.md#strict-equality-mutations-opt-in). |
215
- | `-h, --help` | Show help message. |
216
- | `-v, --version` | Show version. |
217
- | `--verbose` | Show a per-mutation warning for every skipped mutation (quiet by default; the "Generated N mutations, skipped M" summary always prints when mutants are dropped). |
218
- | `--no-progress` | Disable progress display. |
219
- | `--no-test-selection` | Disable two-phase test selection and always run the full test file for every mutant. See [Test selection](#test-selection-fast-kill-with-full-file-confirmation). |
220
- | `--json` | Machine mode: print ONLY the JSON report to stdout (banner, progress and colours go to stderr). A single file prints the per-file report; a multi-file run (`FILE` list with more than one file, `--staged`, `--glob`) prints one aggregate envelope with a condensed `survivors` list. See [Reports and output](#reports-and-output). |
221
- | `--reporters LIST` | Comma-separated reporters to run: `console`, `html`, `json` (default: `console,html,json`). An unknown name errors and exits 1. |
222
- | `--output-dir PATH` | Directory for the generated report files (default: `tmp/mutation_reports`). In batch mode each file writes to its own subdirectory under this path. |
166
+ **`--staged`** reads the file list from the git staging area (`git diff --cached --name-only`), so it is the natural
167
+ "test what I changed" flow:
223
168
 
224
169
  ```bash
225
- # Choose which reporters run and where their files land
226
- bundle exec mutation_test --reporters json,html --output-dir build/mutation \
227
- app/models/user.rb spec/models/user_spec.rb
170
+ bundle exec mutation_test --staged
171
+ # equivalent, from the repository root:
172
+ bundle exec mutation_test $(git diff --cached --name-only)
228
173
  ```
229
174
 
230
- ### Exit codes (single-file mode)
231
-
232
- - `0` - the run passed (score met the threshold, or `fail_on_threshold` is disabled).
233
- - `1` - the mutation score is below the threshold, or the input is unusable (missing
234
- file, unknown reporter, source with a syntax error).
235
- - `2` - a usage error (conflicting flags; see the batch sections below).
236
- - `3` - the run aborted or degraded before reaching a verdict: the shadow workspace
237
- was unreliable (the unmutated source failed there, or the workspace copy of the
238
- source turned out not to be the code the tests execute), or every mutant ended as
239
- `error`/`stillborn` so nothing was scored.
240
- This signals an infrastructure or runner problem, not a test-quality gap, so CI
241
- hooks can distinguish it from a genuine threshold failure.
242
- - `130` - interrupted with Ctrl+C.
243
-
244
- Batch modes (`FILE...` lists, `--staged`, `--glob`) keep the exit codes documented in
245
- their sections below (`0`/`1`/`2`); a degraded file is named explicitly in the batch
246
- summary instead of being blamed on the threshold.
247
-
248
- ### Running with rake
249
-
250
- You can also run mutation tests through rake tasks.
251
-
252
- In a **non-Rails** project, require the tasks from your `Rakefile`:
253
-
254
- ```ruby
255
- # Rakefile
256
- require 'mutation_tester/rake_task'
257
- ```
175
+ Files staged as deleted are ignored. Outside a git repository, or when nothing is staged, the CLI prints a readable
176
+ error instead of running. `--staged` cannot be combined with positional arguments or `--glob`.
258
177
 
259
- In a **Rails** app the tasks load automatically through the gem's railtie, so no
260
- Rakefile change is needed.
178
+ **Explicit pair (`SOURCE_FILE TEST_FILE`)**: with exactly two arguments where the second is recognized as a test file
179
+ (`*_spec.rb`, `*.spec.rb`, `*_test.rb`, `test_*.rb`, or a file requiring minitest), the second is used as the test file
180
+ directly. Two source files enter list mode instead. An RSpec test file with an unconventional name (no `_spec.rb`
181
+ suffix) is not recognized, so that pair is treated as a file list; rename the test or use the conventional layout to get
182
+ the explicit pair.
261
183
 
262
- Then run either task. Rake takes the file arguments inside brackets (not
263
- space-separated), so quote the invocation for your shell:
184
+ **`--glob PATTERN`** mutation-tests every source file the pattern matches in a single run, so you do not need to script
185
+ a loop around `mutation_test`:
264
186
 
265
187
  ```bash
266
- # Top-level task
267
- bundle exec rake "mutation_test[app/models/user.rb,spec/models/user_spec.rb]"
268
-
269
- # Namespaced task
270
- bundle exec rake "mutation:test[app/models/user.rb,spec/models/user_spec.rb]"
188
+ # Minitest project, JSON report per file, custom output directory
189
+ bundle exec mutation_test --glob 'lib/**/*.rb' --spec-glob 'test/{name}_test.rb' \
190
+ --reporters json --output-dir build/mutation
271
191
  ```
272
192
 
273
- ### Programmatic usage
274
-
275
- You can also run the gem directly from Ruby with `MutationTester.run(source, test)`:
276
-
277
- ```ruby
278
- # RSpec
279
- MutationTester.run('examples/calculator.rb', 'examples/calculator_spec.rb')
193
+ **`--since REV`** narrows a `--glob` batch to the files that changed since a git revision, which is how you keep
194
+ mutation testing affordable on pull requests:
280
195
 
281
- # Minitest
282
- MutationTester.run('examples/calculator.rb', 'examples/calculator_minitest.rb')
196
+ ```bash
197
+ bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
283
198
  ```
284
199
 
285
- ### File lists and --staged: test what you changed
286
-
287
- Passing one or more source files is the main interface. Each file is mapped to
288
- its spec by convention (`lib/X.rb` -> `spec/X_spec.rb`, override with
289
- `--spec-glob`), and the whole list runs as one aggregated batch: every file is
290
- processed, the summary shows one `PASS`/`FAIL` line per file, reports land in
291
- per-file subdirectories, and the exit code reflects the whole run.
200
+ A matched file counts as changed when `git diff --name-only REV` lists it; new files the revision does not know about
201
+ (committed or still untracked) also count as changed. Unchanged matched files are reported as
202
+ `SKIPPED (unchanged since REV)` and never mutated. When nothing changed, the run succeeds with a "Nothing to mutate"
203
+ message and exit code `0`, so a PR that does not touch your sources does not fail the gate.
292
204
 
293
- ```bash
294
- bundle exec mutation_test lib/calculator.rb lib/parser.rb
205
+ **`--fail-fast`** turns the run into a cheap gate: the run stops as soon as one mutant survives, the reports contain the
206
+ results obtained up to that point with a clear interruption notice, and the process finishes with exit code `1`. It
207
+ works in single-file mode, with `--glob` (the batch stops and remaining files are not run), and in parallel mode.
295
208
 
296
- # The natural "test what I changed" flow:
297
- bundle exec mutation_test --staged
298
- bundle exec mutation_test $(git diff --cached --name-only)
299
- ```
209
+ #### Batch behaviour and skipped files
300
210
 
301
- `--staged` reads the list from the git staging area (`git diff --cached
302
- --name-only`), so the two commands above are equivalent when run from the
303
- repository root. Files staged as deleted are ignored. Outside a git repository,
304
- or when nothing is staged, the CLI prints a readable error instead of running.
305
- `--staged` cannot be combined with positional arguments or `--glob`.
306
-
307
- When any mutant survives, the aggregate summary ends with a survivors section:
308
- one `file:line original -> mutated` line per surviving mutant. A surviving
309
- mutant is a change to your code that your tests do not detect, so each line is
310
- a concrete test gap to close. With zero survivors the section is absent.
311
-
312
- The list may contain anything a real `git diff` produces; unmutable entries are
313
- reported as `SKIPPED` with an explicit reason and never count as a success:
314
-
315
- - **file not found** - the path does not exist.
316
- - **not a Ruby source file** - e.g. a staged `.md` or config file.
317
- - **a test file, not a mutable source** - a test file passed directly
318
- (`*_spec.rb`, `*.spec.rb`, `*_test.rb`, `test_*.rb`, or minitest content).
319
- - **no matching spec file** - the convention (or `--spec-glob` / `--spec-map`)
320
- points at a spec that does not exist; the expected path is printed.
321
-
322
- Exit codes: `0` when at least one file was processed and every processed file
323
- met the threshold; `1` when any processed file was below threshold or when
324
- every listed file was skipped (nothing was actually mutation-tested); `2` for
325
- usage errors (flag conflicts, `--staged` outside a git repository).
326
-
327
- **Legacy pair heuristic:** exactly two arguments where the second is recognized
328
- as a test file (by the name patterns above or by minitest content) keep the
329
- original `SOURCE_FILE TEST_FILE` behavior. Two source files enter list mode. An
330
- RSpec test file with an unconventional name (no `_spec.rb` suffix) is not
331
- recognized, so that pair is treated as a file list; rename the test or use the
332
- conventional layout to get the explicit pair.
333
-
334
- ### Batch mode: run many files in one command
335
-
336
- `--glob PATTERN` mutation-tests every source file the pattern matches in a single
337
- run, so you no longer need to script a loop around `mutation_test` or depend on
338
- the Rails-only `rake mutation:test_models` task.
339
-
340
- Each matched source file is mapped to its spec by convention: `lib/X.rb` becomes
341
- `spec/X_spec.rb`. See [Mapping sources to specs](#mapping-sources-to-specs) for
342
- the two ways to override that convention.
343
-
344
- Behaviour:
345
-
346
- - **Every file is processed.** A file whose score is below the threshold does not
347
- abort the batch; the run continues to the next file (the `mutation:test_models`
348
- pattern).
349
- - **Reports never overwrite each other.** Each processed file writes its reporter
350
- output to its own subdirectory under `--output-dir` (a slug derived from the
351
- source path), so per-file HTML/JSON reports coexist.
352
- - **A console aggregate summary** is printed at the end: one line per processed
353
- file with its score and `PASS`/`FAIL` against the threshold, followed by a
354
- clearly separated `SKIPPED` list.
355
- - **A source file with no matching spec is `SKIPPED`**, reported explicitly and
356
- never counted as a success. A single skipped file next to processed ones does
357
- not by itself fail the run, but a run that skipped *every* matched file
358
- measured nothing and fails (see the exit codes below).
359
-
360
- Exit codes:
361
-
362
- - `0` - at least one file was mutation-tested and every processed file met the
363
- mutation score threshold. A `--since` run where nothing changed also exits `0`
364
- (see below).
365
- - `1` - at least one processed file was below threshold, the glob matched no
366
- source files at all, every matched file was skipped so nothing was actually
367
- mutation-tested, or `--fail-fast` stopped the run at a surviving mutant.
368
- - `2` - a usage error: `--spec-glob` or `--spec-map` with an explicit
369
- `SOURCE_FILE TEST_FILE` pair, a malformed `--spec-map` rule, `--since` given
370
- without `--glob`, `--staged` combined with positional arguments or `--glob`,
371
- or `--since`/`--staged` used outside a git repository (for `--since` also an
372
- unknown revision).
373
-
374
- The "every matched file was skipped" case is deliberate: a typo in
375
- `--spec-glob`/`--spec-map`, or a refactor that moves the test directory, would
376
- otherwise leave a green CI step that measured nothing.
211
+ All multi-file modes (`FILE...` lists, `--staged`, `--glob`) behave the same way:
377
212
 
378
- ```bash
379
- # Minitest project, JSON report per file, custom output directory
380
- bundle exec mutation_test --glob 'lib/**/*.rb' --spec-glob 'test/{name}_test.rb' \
381
- --reporters json --output-dir build/mutation
382
- ```
213
+ - **Every file is processed.** A file whose score is below the threshold does not abort the batch (unless
214
+ `--fail-fast`); the run continues to the next file.
215
+ - **Reports never overwrite each other.** Each processed file writes its reporter output to its own subdirectory under
216
+ `--output-dir` (a slug derived from the source path).
217
+ - **A console aggregate summary** is printed at the end: one line per processed file with its score and `PASS`/`FAIL`,
218
+ followed by a clearly separated `SKIPPED` list. When any mutant survives, the summary ends with a survivors section:
219
+ one `file:line original -> mutated` line per surviving mutant, each a concrete test gap to close.
220
+ - **Unmutable entries are `SKIPPED` with an explicit reason** and never count as a success:
221
+ - **file not found** - the path does not exist.
222
+ - **not a Ruby source file** - e.g. a staged `.md` or config file.
223
+ - **a test file, not a mutable source** - a test file passed directly.
224
+ - **no matching spec file** - the convention (or `--spec-glob` / `--spec-map`) points at a spec that does not exist;
225
+ the expected path is printed.
226
+ - **A run that skipped every file fails** (exit code `1`): a typo in `--spec-glob`/`--spec-map`, or a refactor that
227
+ moves the test directory, would otherwise leave a green CI step that measured nothing.
383
228
 
384
229
  ### Mapping sources to specs
385
230
 
386
- Every mode that takes more than an explicit `SOURCE_FILE TEST_FILE` pair (a
387
- positional `FILE` list, `--staged`, `--glob`) derives the test path from the
388
- source path. Two mechanisms do that, checked in this order:
231
+ Every mode except the explicit `SOURCE_FILE TEST_FILE` pair derives the test path from the source path. Two mechanisms
232
+ do that, checked in this order:
389
233
 
390
234
  1. `--spec-map 'PATTERN=>REPLACEMENT'` - regular-expression rules.
391
235
  2. `--spec-glob TEMPLATE` - a `{name}` template (default `spec/{name}_spec.rb`).
392
236
 
393
- **`--spec-glob TEMPLATE`** substitutes `{name}`, which is the source path with a
394
- leading `lib/` segment removed and the `.rb` extension stripped, subdirectories
395
- preserved (`lib/foo/bar.rb` -> `spec/foo/bar_spec.rb`). Because `{name}` is one
396
- value, a template can only add a prefix and a suffix around the source path. For
397
- a Minitest project laid out under `test/` that is enough:
237
+ **`--spec-glob TEMPLATE`** substitutes `{name}`, which is the source path with a leading `lib/` segment removed and the
238
+ `.rb` extension stripped, subdirectories preserved (`lib/foo/bar.rb` -> `spec/foo/bar_spec.rb`). Because `{name}` is one
239
+ value, a template can only add a prefix and a suffix around the source path. For a Minitest project laid out under
240
+ `test/` that is enough:
398
241
 
399
242
  ```bash
400
243
  bundle exec mutation_test --glob 'lib/**/*.rb' --spec-glob 'test/{name}_test.rb'
401
244
  ```
402
245
 
403
- **`--spec-map 'PATTERN=>REPLACEMENT'`** covers the layouts a template cannot
404
- express: those that substitute *inside* the path, after a variable-length
405
- prefix. `PATTERN` is a Ruby regular expression matched against the whole source
406
- path (a leading `./` removed); the first `=>` separates it from `REPLACEMENT`,
407
- which may use `\1`, `\2`, ... backreferences and produces the whole spec path.
408
- Only the first match in the path is replaced.
246
+ **`--spec-map 'PATTERN=>REPLACEMENT'`** covers the layouts a template cannot express: those that substitute *inside*
247
+ the path, after a variable-length prefix. `PATTERN` is a Ruby regular expression matched against the whole source path
248
+ (a leading `./` removed); the first `=>` separates it from `REPLACEMENT`, which may use `\1`, `\2`, ... backreferences
249
+ and produces the whole spec path. Only the first match in the path is replaced.
409
250
 
410
251
  - The flag is repeatable and the first matching rule wins.
411
- - A source that matches no rule falls back to `--spec-glob` (or the default
412
- convention), so one command can cover `app/` and `lib/` at once.
252
+ - A source that matches no rule falls back to `--spec-glob` (or the default convention), so one command can cover
253
+ `app/` and `lib/` at once.
413
254
  - Quote the rule in single quotes so the shell leaves the backslashes alone.
414
255
 
415
- Rails and Rails-shaped layouts, where the rule is "replace the `app/` segment
416
- with `test/`, keep whatever prefix comes before it":
256
+ Rails and Rails-shaped layouts, where the rule is "replace the `app/` segment with `test/`, keep whatever prefix comes
257
+ before it":
417
258
 
418
259
  ```bash
419
260
  # Plain Rails, Minitest: app/models/current.rb -> test/models/current_test.rb
@@ -437,43 +278,78 @@ bundle exec mutation_test --glob '{app,lib}/**/*.rb' \
437
278
  --spec-glob 'test/{name}_test.rb'
438
279
  ```
439
280
 
440
- When a rule produces a path that does not exist, the file is reported as
441
- `SKIPPED (no matching spec file)` with the expected path printed, and a run in
442
- which *every* file was skipped that way fails with exit code `1`.
281
+ When a rule produces a path that does not exist, the file is reported as `SKIPPED (no matching spec file)` with the
282
+ expected path printed.
283
+
284
+ ### CLI options
285
+
286
+ | Flag | Description |
287
+ |---|---|
288
+ | `-p, --parallel N` | Run with N parallel processes (default: auto from CPU cores, capped at 8; `-p 1` forces serial). See [Parallel execution](#parallel-execution-on-by-default). |
289
+ | `--runner MODE` | Mutant execution runner: `auto` (default), `in_memory`, `fork`, or `spawn`. See [Execution runners](#execution-runners-fork-spawn-in-memory). |
290
+ | `--staged` | Mutation-test the files staged in git. See [Choosing what to test](#choosing-what-to-test). |
291
+ | `--glob PATTERN` | Mutation-test every source file matching `PATTERN`. See [Choosing what to test](#choosing-what-to-test). |
292
+ | `--since REV` | With `--glob`: mutate only the files that changed since git revision `REV`. See [Choosing what to test](#choosing-what-to-test). |
293
+ | `--spec-glob TEMPLATE` | Spec-mapping template with a `{name}` placeholder (default: `spec/{name}_spec.rb`). See [Mapping sources to specs](#mapping-sources-to-specs). |
294
+ | `--spec-map RULE` | Spec-mapping regex rule `'PATTERN=>REPLACEMENT'`; repeatable, first match wins. See [Mapping sources to specs](#mapping-sources-to-specs). |
295
+ | `--minimum-score N` | Mutation score percentage a file must reach to pass (default: 80). Drives the `PASS`/`FAIL` verdict and the exit code. |
296
+ | `--fail-fast` | Stop the run at the first surviving mutant and finish with a failing status. |
297
+ | `--timeout-factor N` | Per-mutant timeout budget as `N` times the measured baseline test run, never below 5 s (default: 5, must be > 0). Ignored when `config.timeout` is set explicitly. |
298
+ | `--timeout-policy MODE` | Scoring policy for timed-out mutants: `killed` (default) counts a timeout as a kill; `separate` keeps timeouts out of the score and reports them as their own category. |
299
+ | `--worker-env NAME` | Per-worker database isolation via the `parallel_tests` `TEST_ENV_NUMBER` convention. See [Making parallelism work with Rails](#making-parallelism-work-with-rails). |
300
+ | `--after-fork FILE` | Ruby file loaded inside each in-memory clone right after it forks, to re-establish per-worker state. See [Making parallelism work with Rails](#making-parallelism-work-with-rails). |
301
+ | `--strict-equality` | Enable the opt-in strict-equality probes (`==` to `eql?`/`equal?`). Default off; expect noise on code that does not distinguish numeric types or object identity. See [Strict Equality Mutations](docs/mutation-types.md#strict-equality-mutations-opt-in). |
302
+ | `--no-test-selection` | Disable two-phase test selection and always run the full test file for every mutant. See [Test selection](#test-selection-fast-kill-with-full-file-confirmation). |
303
+ | `--json` | Machine mode: print ONLY the JSON report to stdout (everything else goes to stderr). See [JSON report](#json-report-and-machine-readable-output). |
304
+ | `--reporters LIST` | Comma-separated reporters to run: `console`, `html`, `json` (default: all three). An unknown name errors and exits 1. |
305
+ | `--output-dir PATH` | Directory for the generated report files (default: `tmp/mutation_reports`). In batch mode each file writes to its own subdirectory. |
306
+ | `--verbose` | Show a per-mutation warning for every skipped mutation (quiet by default; the "Generated N mutations, skipped M" summary always prints when mutants are dropped). |
307
+ | `--no-progress` | Disable progress display. |
308
+ | `-h, --help` | Show help message. |
309
+ | `-v, --version` | Show version. |
443
310
 
444
- ### Incremental mode: mutate only what changed
311
+ ### Exit codes
445
312
 
446
- `--since REV` narrows a `--glob` batch to the files that changed since a git
447
- revision, which is how you keep mutation testing affordable on pull requests:
313
+ | Code | Meaning |
314
+ |---|---|
315
+ | `0` | The run passed: every processed file met the threshold (or `fail_on_threshold` is disabled). A `--since` run where nothing changed also exits `0`. |
316
+ | `1` | Below threshold, unusable input (missing file, unknown reporter, source with a syntax error), a glob that matched nothing, a batch where every file was skipped, or a `--fail-fast` stop. |
317
+ | `2` | A usage error: conflicting flags (`--spec-glob`/`--spec-map` with an explicit pair, `--staged` with positional arguments or `--glob`, `--since` without `--glob`), a malformed `--spec-map` rule, or `--since`/`--staged` outside a git repository (for `--since` also an unknown revision). |
318
+ | `3` | Single-file mode only: the run aborted or degraded before reaching a verdict. The shadow workspace was unreliable (the unmutated source failed there, or the workspace copy of the source turned out not to be the code the tests execute), or every mutant ended as `error`/`stillborn` so nothing was scored. This signals an infrastructure or runner problem, not a test-quality gap, so CI hooks can distinguish it from a genuine threshold failure. In batch modes a degraded file is named explicitly in the batch summary instead. |
319
+ | `130` | Interrupted with Ctrl+C. |
448
320
 
449
- ```bash
450
- bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
451
- ```
321
+ ### Running with rake
452
322
 
453
- Behaviour:
323
+ In a **non-Rails** project, require the tasks from your `Rakefile`:
454
324
 
455
- - A matched file counts as **changed** when `git diff --name-only REV` lists it;
456
- new files the revision does not know about (committed or still untracked) also
457
- count as changed.
458
- - Unchanged matched files are reported as skipped in the batch summary
459
- (`SKIPPED (unchanged since REV)`), never mutated.
460
- - When nothing matched by the glob changed since `REV`, the run succeeds with a
461
- "Nothing to mutate" message and exit code `0`, so a PR that does not touch
462
- your sources does not fail the gate.
463
- - Outside a git repository (or without a `git` executable), or with a revision
464
- the repository does not know, the CLI prints a readable error and exits `2`
465
- before any mutation runs.
325
+ ```ruby
326
+ # Rakefile
327
+ require 'mutation_tester/rake_task'
328
+ ```
466
329
 
467
- ### Fail fast: stop at the first surviving mutant
330
+ In a **Rails** app the tasks load automatically through the gem's railtie, so no Rakefile change is needed.
468
331
 
469
- `--fail-fast` turns the run into a cheap gate: the run stops as soon as one
470
- mutant survives, the reports contain the results obtained up to that point with
471
- a clear interruption notice, and the process finishes with exit code `1`. It
472
- works in single-file mode, with `--glob` (the batch stops and remaining files
473
- are not run), and in parallel mode:
332
+ Then run either task. Rake takes the file arguments inside brackets (not space-separated), so quote the invocation for
333
+ your shell:
474
334
 
475
335
  ```bash
476
- bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main --fail-fast
336
+ # Top-level task
337
+ bundle exec rake "mutation_test[app/models/user.rb,spec/models/user_spec.rb]"
338
+
339
+ # Namespaced task
340
+ bundle exec rake "mutation:test[app/models/user.rb,spec/models/user_spec.rb]"
341
+ ```
342
+
343
+ ### Programmatic usage
344
+
345
+ Run the gem directly from Ruby with `MutationTester.run(source, test)`:
346
+
347
+ ```ruby
348
+ # RSpec
349
+ MutationTester.run('examples/calculator.rb', 'examples/calculator_spec.rb')
350
+
351
+ # Minitest
352
+ MutationTester.run('examples/calculator.rb', 'examples/calculator_minitest.rb')
477
353
  ```
478
354
 
479
355
  ## Configuration
@@ -510,6 +386,19 @@ MutationTester.configure do |config|
510
386
  # setting a specific mode forces it. See the Execution model section below.
511
387
  config.runner = :auto
512
388
 
389
+ # Per-worker database isolation (parallel_tests TEST_ENV_NUMBER convention).
390
+ # Set to an environment variable name to give each parallel worker a distinct
391
+ # value before it boots its test environment. Equivalent to the --worker-env
392
+ # CLI flag. See Making parallelism work with Rails below.
393
+ # config.worker_env_var = "TEST_ENV_NUMBER"
394
+
395
+ # Ruby file loaded inside each preloaded in-memory clone right after it forks
396
+ # and receives its per-worker environment, so the app can re-establish
397
+ # per-worker state such as its database connection. Keeps the in_memory
398
+ # runner available in parallel worker-env runs. Equivalent to the
399
+ # --after-fork CLI flag.
400
+ # config.after_fork_file = "db/mutation_after_fork.rb"
401
+
513
402
  # Two-phase test selection (RSpec only): fast-kill on a matching example
514
403
  # subset, always confirmed by the full file before a mutant is reported as
515
404
  # survived. Set to false to always run the full file. See Execution model.
@@ -558,14 +447,12 @@ end
558
447
 
559
448
  ## Execution model
560
449
 
561
- MutationTester runs in parallel by default and picks the fastest safe execution
562
- runner automatically. This section covers when to override those defaults, how
563
- the runners differ, and how two-phase test selection speeds up kills.
450
+ MutationTester runs in parallel by default and picks the fastest safe execution runner automatically. This section
451
+ covers when to override those defaults, how the runners differ, and how the gem speeds up kills.
564
452
 
565
453
  ### Parallel execution (on by default)
566
454
 
567
- By default, mutation_tester runs in **parallel**: the process count is derived
568
- from the number of CPU cores (`Etc.nprocessors`), capped at 8 and never below 1.
455
+ By default the process count is derived from the number of CPU cores (`Etc.nprocessors`), capped at 8 and never below 1.
569
456
  Force a specific count, or serial execution, when your tests need it:
570
457
 
571
458
  ```bash
@@ -579,48 +466,33 @@ bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb -p 1
579
466
  MUTATION_TESTER_PARALLEL_PROCESSES=4 bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
580
467
  ```
581
468
 
582
- Precedence is: an explicit `--parallel/-p` flag overrides `MUTATION_TESTER_PARALLEL_PROCESSES`, which overrides the
469
+ Precedence: an explicit `--parallel/-p` flag overrides `MUTATION_TESTER_PARALLEL_PROCESSES`, which overrides the
583
470
  auto-derived core count; an explicit `config.parallel_processes` assignment in Ruby also replaces the auto default. An
584
- invalid value (less than 1, or non-numeric) falls back to 1 with a warning on stderr. `-p 1` forces serial execution.
585
-
586
- **⚠️ Force serial execution with `-p 1` if your tests share database state!**
587
-
588
- In parallel mode each mutant runs in an isolated shadow workspace. Every `.rb`
589
- file is a physical copy (non-Ruby files stay symlinks for speed), so mutations
590
- apply correctly even when a spec loads the source indirectly (e.g. via
591
- `spec_helper`), and `$LOAD_PATH` entries pointing into the project resolve
592
- inside the workspace, so a test file that reaches its source through
593
- `require "test_helper"` gets the mutated copy too. The parallel mutation score
594
- therefore matches serial.
595
-
596
- Before the first mutant, the run proves this in the workspace itself: the
597
- unmutated source must pass there, and the same suite must fail once that copy of
598
- the source is replaced by a `raise`. A run whose tests pass even then is aborted
599
- as an infrastructure failure (exit code `3`) rather than reported as a 0.0%
600
- score, because the mutated file is demonstrably not the code being executed.
601
-
602
- ### When to use serial vs parallel execution
603
-
604
- Use **parallel execution** (the default) for:
471
+ invalid value (less than 1, or non-numeric) falls back to 1 with a warning on stderr.
605
472
 
606
- - **Pure Ruby classes** - No database, no shared state
607
- - **Unit tests with mocks** - Fast and independent tests
608
- - **Large codebases** - Significant time savings
609
- - **CI/CD with powerful machines** - Make use of available resources
473
+ **When to force serial execution with `-p 1`**: Rails apps whose tests share one test database, tests that share any
474
+ other state, the first time you try mutation testing (easier to debug), or machines with limited memory/CPU. Pure Ruby
475
+ classes, unit tests with mocks, and CI machines with many cores all benefit from the parallel default. To keep
476
+ parallelism on a Rails app instead of dropping to serial, see
477
+ [Making parallelism work with Rails](#making-parallelism-work-with-rails).
610
478
 
611
- Force **serial execution with `-p 1`** for:
479
+ In parallel mode each mutant runs in an isolated shadow workspace. Every `.rb` file is a physical copy (non-Ruby files
480
+ stay symlinks for speed), so mutations apply correctly even when a spec loads the source indirectly (e.g. via
481
+ `spec_helper`), and `$LOAD_PATH` entries pointing into the project resolve inside the workspace, so a test file that
482
+ reaches its source through `require "test_helper"` gets the mutated copy too. The parallel mutation score therefore
483
+ matches serial.
612
484
 
613
- - **Rails applications with database** - Avoids conflicts
614
- - **Tests that share state** - No interference between test runs
615
- - **First time using mutation testing** - Easier to debug
616
- - **Limited system resources** - Less memory/CPU usage
485
+ Before the first mutant, the run proves this in the workspace itself: the unmutated source must pass there, and the
486
+ same suite must fail once that copy of the source is replaced by a `raise`. A run whose tests pass even then is aborted
487
+ as an infrastructure failure (exit code `3`) rather than reported as a 0.0% score, because the mutated file is
488
+ demonstrably not the code being executed.
617
489
 
618
490
  ### Making parallelism work with Rails
619
491
 
620
- Parallel workers get an isolated filesystem (each mutant runs in its own shadow workspace), but they share one
621
- **database** unless you give each worker its own. If your app is already set up for `parallel_tests` (a `database.yml`
622
- keyed on `TEST_ENV_NUMBER` and per-worker databases created with `rake parallel:prepare`), `--worker-env` bridges the
623
- gem to that setup so you can run parallel instead of serial.
492
+ Parallel workers get an isolated filesystem, but they share one **database** unless you give each worker its own. If
493
+ your app is already set up for `parallel_tests` (a `database.yml` keyed on `TEST_ENV_NUMBER` and per-worker databases
494
+ created with `rake parallel:prepare`), `--worker-env` bridges the gem to that setup so you can run parallel instead of
495
+ serial.
624
496
 
625
497
  **`--worker-env NAME`** sets the environment variable `NAME` to a distinct value in each worker before it boots its
626
498
  test environment, following the `parallel_tests` `TEST_ENV_NUMBER` convention:
@@ -651,10 +523,41 @@ bundle exec mutation_test --glob 'app/models/**/*.rb' \
651
523
 
652
524
  `MUTATION_TESTER_WORKER_ENV=TEST_ENV_NUMBER` is equivalent to passing the flag.
653
525
 
654
- **Runner support.** `--worker-env` works with the `fork` and `spawn` runners, where each mutant boots its test
655
- environment freshly and picks up the variable. The `in_memory` runner clones a single preloaded worker that has already
656
- connected to one database, so it cannot isolate a per-worker database; when `--worker-env` is set the runner selection
657
- skips `in_memory` and uses `fork`, announcing the reason on stderr.
526
+ **Runner support.** `--worker-env` works with the `fork` and `spawn` runners out of the box, because each mutant boots
527
+ its test environment freshly and picks up the variable. The `in_memory` runner clones a single preloaded worker that
528
+ has already connected to one database, so setting the variable alone cannot re-point an existing connection; in a
529
+ parallel run without `--after-fork` the runner selection therefore skips `in_memory` and uses `fork`, announcing the
530
+ reason on stderr. A serial run (`-p 1`) has only one worker and stays in memory.
531
+
532
+ **Keeping the in-memory runner with `--after-fork`.** Pass `--after-fork FILE` (or
533
+ `MUTATION_TESTER_AFTER_FORK=FILE`) to keep the `in_memory` runner in parallel worker-env runs. Each preloaded clone
534
+ then receives its per-worker value of the `--worker-env` variable and loads `FILE` right after forking, and that file
535
+ is where your app re-establishes its per-worker state. For a Rails app with a `TEST_ENV_NUMBER`-keyed `database.yml`
536
+ that usually means reconnecting ActiveRecord:
537
+
538
+ ```ruby
539
+ # db/mutation_after_fork.rb
540
+ ActiveRecord::Base.establish_connection(
541
+ ActiveRecord::Base.configurations
542
+ .configs_for(env_name: 'test', name: 'primary')
543
+ .configuration_hash
544
+ .merge(database: "myapp_test#{ENV['TEST_ENV_NUMBER']}")
545
+ )
546
+ ```
547
+
548
+ ```bash
549
+ bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb \
550
+ -p 4 --worker-env TEST_ENV_NUMBER --after-fork db/mutation_after_fork.rb
551
+ ```
552
+
553
+ The file runs once per clone, inside the clone only (never in the primary preloaded worker or in your shell process).
554
+ If it raises, the clone reports the error on stderr and is dropped, and its worker decides its share of mutants through
555
+ the file-based path; if the file does not exist, the whole run falls back to file-based execution with a warning.
556
+
557
+ **Serial runs never mutate your checkout.** With `--worker-env` set, a `-p 1` run on the file-based runners decides
558
+ each mutant in a shadow workspace instead of writing mutants into the real source file, so a killed process cannot
559
+ leave a mutated file behind. (Without `--worker-env`, a serial file-based run still uses in-place mutation with a
560
+ `.mutation_backup` file that the next run restores automatically.)
658
561
 
659
562
  **Still simplest without a parallel database setup:** if you have not provisioned per-worker databases, keep Rails
660
563
  model runs on serial `-p 1`. `--worker-env` is only useful once the databases exist.
@@ -667,39 +570,15 @@ model runs on serial `-p 1`. `--worker-env` is only useful once the databases ex
667
570
 
668
571
  ### Execution runners (fork, spawn, in-memory)
669
572
 
670
- Every mutant is executed by one of three runners, and `auto` (the default) picks
671
- the fastest safe one, announcing every fallback on stderr:
672
-
673
- - **in_memory** (default where supported): re-evaluates the mutated source in the
674
- memory of a fresh fork of a preloaded process, with zero file writes per mutant
675
- and no shadow workspaces. RSpec and Minitest; the fastest path. Mutations that only
676
- take effect at class-load time (constants consumed by macros, `validates`/`has_many`/
677
- `before_save`/`scope`/`attribute`, anything inside an `included do` block) cannot
678
- be observed by re-evaluating source in a preloaded process, so those mutants are
679
- routed automatically to the file-based path and the rest still run in memory (see
680
- below); the combined score matches a full `fork` run.
681
- - **fork**: preloads the environment once (RubyGems, Bundler, the test framework)
682
- and forks a fresh child per mutant. RSpec and Minitest on platforms with
683
- `Process.fork`; removes most of the fixed per-mutant boot cost.
684
- - **spawn**: starts one full process per mutant (`bundle exec rspec ...` or
685
- `bundle exec ruby test_file.rb`). Slower per mutant, but works everywhere
686
- (the only runner on platforms without `Process.fork`).
687
-
688
- `auto` tries `in_memory`, then `fork`, then `spawn`; every step down prints one
689
- stderr warning with its reason, so a fallback is never silent. All runners
690
- produce identical scores and per-mutant statuses and enforce the same hard
691
- per-mutant timeout (monotonic deadline plus a process-group kill).
692
-
693
- **Load-time mutants under in-memory (Rails).** The in-memory runner classifies each
694
- mutation by its AST context. A mutation inside a method body defined directly in a
695
- class/module is re-appliable in memory and runs there (fast). A mutation on a
696
- class/module-body statement (a constant, a `validates`/`has_many`/`before_save`/
697
- `scope`/`attribute` macro, or anything inside `included do ... end`) is decided
698
- file-based within the same run, because re-evaluating the source does not re-run
699
- those class-load registrations. This keeps in-memory speed for the common case
700
- while matching a full `fork` score on Rails concerns and models. When at least one
701
- mutant is routed this way, the run prints one stderr notice. It is automatic; you
702
- do not need to pick `--runner fork` for correctness on load-time code.
573
+ Every mutant is executed by one of three runners, and `auto` (the default) picks the fastest safe one, announcing every
574
+ fallback on stderr:
575
+
576
+ - **in_memory** (default where supported): re-evaluates the mutated source in the memory of a fresh fork of a preloaded
577
+ process, with zero file writes per mutant and no shadow workspaces. RSpec and Minitest; the fastest path.
578
+ - **fork**: preloads the environment once (RubyGems, Bundler, the test framework) and forks a fresh child per mutant.
579
+ RSpec and Minitest on platforms with `Process.fork`; removes most of the fixed per-mutant boot cost.
580
+ - **spawn**: starts one full process per mutant (`bundle exec rspec ...` or `bundle exec ruby test_file.rb`). Slower
581
+ per mutant, but works everywhere (the only runner on platforms without `Process.fork`).
703
582
 
704
583
  | Mode | Picked by `auto` when | Falls back to |
705
584
  |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
@@ -707,84 +586,78 @@ do not need to pick `--runner fork` for correctness on load-time code.
707
586
  | `fork` | `Process.fork` is available, but in-memory is unavailable (each reason is printed) | `spawn`, with a stderr warning, when the helper process fails to preload the environment |
708
587
  | `spawn` | the platform has no `Process.fork` | nothing; it works everywhere |
709
588
 
710
- Force a specific runner (skipping the auto attempts) with the `--runner
711
- fork|spawn|in_memory` flag, the `MUTATION_TESTER_RUNNER` environment variable, or
712
- `config.runner`:
589
+ All runners produce identical scores and per-mutant statuses and enforce the same hard per-mutant timeout (monotonic
590
+ deadline plus a process-group kill).
591
+
592
+ **Load-time mutants under in-memory (Rails).** The in-memory runner classifies each mutation by its AST context. A
593
+ mutation inside a method body defined directly in a class/module is re-appliable in memory and runs there (fast). A
594
+ mutation on a class/module-body statement (a constant, a `validates`/`has_many`/`before_save`/`scope`/`attribute`
595
+ macro, or anything inside `included do ... end`) cannot be observed by re-evaluating source in a preloaded process, so
596
+ it is decided file-based within the same run. This keeps in-memory speed for the common case while matching a full
597
+ `fork` score on Rails concerns and models. When at least one mutant is routed this way, the run prints one stderr
598
+ notice. It is automatic; you do not need to pick `--runner fork` for correctness on load-time code.
599
+
600
+ Force a specific runner (skipping the auto attempts) with the `--runner fork|spawn|in_memory` flag, the
601
+ `MUTATION_TESTER_RUNNER` environment variable, or `config.runner`:
713
602
 
714
603
  ```bash
715
604
  mutation_test --runner spawn lib/calculator.rb spec/calculator_spec.rb
716
605
  ```
717
606
 
718
- Reach for `--runner spawn` when you want maximum isolation or are debugging a
719
- suspicious result from a preloaded runner, and `--runner fork` when your source
720
- is not cleanly re-evaluable in memory but you still want the preloaded-environment
721
- speed. For the full per-runner mechanics, when to force each one, and the complete
722
- fork and in-memory limitation lists (frozen classes, load-time `defined?` guards,
723
- worker-death fallback, `require_relative` idempotency), see
607
+ Reach for `--runner spawn` when you want maximum isolation or are debugging a suspicious result from a preloaded
608
+ runner, and `--runner fork` when your source is not cleanly re-evaluable in memory but you still want the
609
+ preloaded-environment speed. For the full per-runner mechanics and the complete fork and in-memory limitation lists
610
+ (frozen classes, load-time `defined?` guards, worker-death fallback, `require_relative` idempotency), see
724
611
  [docs/execution-runners.md](docs/execution-runners.md#execution-runners-fork-spawn-in-memory).
725
612
 
726
613
  ### Stopping a mutant at its first failing test
727
614
 
728
- A mutant only needs one failing test to be killed, so every mutant run stops at
729
- its first failure: RSpec mutant runs get `--fail-fast`, and Minitest mutant runs
730
- get a preloaded reporter that aborts the run the same way (both on the file-based
731
- runners and inside the preloaded fork worker). This never changes a verdict, only
732
- the work done to reach it: a run that stops early had already failed, and a run
733
- with no failure is unaffected and still executes every test.
734
-
735
- It matters most for a mutant that breaks something every test touches (a broken
736
- class body, a constant every example reads). Such a mutant used to pay the full
737
- test file once per mutant, which on a large test file can exceed the per-mutant
738
- deadline and turn a decided kill into a reported timeout. Adding tests to the file
739
- then made the score worse. The baseline run and the shadow sanity check are
615
+ A mutant only needs one failing test to be killed, so every mutant run stops at its first failure: RSpec mutant runs
616
+ get `--fail-fast`, and Minitest mutant runs get a preloaded reporter that aborts the run the same way (both on the
617
+ file-based runners and inside the preloaded fork worker). This never changes a verdict, only the work done to reach it:
618
+ a run that stops early had already failed, and a run with no failure is unaffected and still executes every test.
619
+
620
+ It matters most for a mutant that breaks something every test touches (a broken class body, a constant every example
621
+ reads). Such a mutant used to pay the full test file once per mutant, which on a large test file can exceed the
622
+ per-mutant deadline and turn a decided kill into a reported timeout. The baseline run and the shadow sanity check are
740
623
  unaffected: they are expected to pass, and a passing run runs every test.
741
624
 
742
625
  ### Test selection (fast kill with full-file confirmation)
743
626
 
744
- For RSpec suites on the file-based runners, MutationTester runs each mutant in
745
- two phases: it first runs only the examples whose group matches the mutated
746
- method (`rspec spec_file -e '#foo' -e '.foo'`) to kill it fast, then confirms a
747
- passing subset against the full spec file before a mutant can be reported as
748
- survived, so selection never introduces false survivors. It degrades to the full
749
- file when the mutant is not inside a method, the spec has no matching group, or
750
- the suite is Minitest; the in-memory runner skips selection entirely (its
751
- examples are already loaded). Disable it with `--no-test-selection` or
752
- `config.test_selection = false`. See
753
- [docs/execution-runners.md](docs/execution-runners.md#test-selection-fast-kill-with-full-file-confirmation)
754
- for the full behavior.
627
+ For RSpec suites on the file-based runners, MutationTester runs each mutant in two phases: it first runs only the
628
+ examples whose group matches the mutated method (`rspec spec_file -e '#foo' -e '.foo'`) to kill it fast, then confirms
629
+ a passing subset against the full spec file before a mutant can be reported as survived, so selection never introduces
630
+ false survivors. It degrades to the full file when the mutant is not inside a method, the spec has no matching group,
631
+ or the suite is Minitest; the in-memory runner skips selection entirely (its examples are already loaded). Disable it
632
+ with `--no-test-selection` or `config.test_selection = false`. See
633
+ [docs/execution-runners.md](docs/execution-runners.md#test-selection-fast-kill-with-full-file-confirmation) for the
634
+ full behavior.
755
635
 
756
636
  ## Mutation types
757
637
 
758
- MutationTester generates several families of mutations, enabled by default:
759
- arithmetic, bitwise compound-assignment, comparison, logical, boolean, number,
760
- string, conditional, call-removal, nil-injection and argument mutations. A
761
- strict-equality family is opt-in. Enable or disable individual families through
762
- `config.mutation_types` (see [Configuration](#configuration)) or turn
763
- strict-equality on with `--strict-equality`.
638
+ MutationTester generates several families of mutations, enabled by default: arithmetic, bitwise compound-assignment,
639
+ comparison, logical, boolean, number, string, conditional, call-removal, nil-injection and argument mutations. A
640
+ strict-equality family is opt-in. Enable or disable individual families through `config.mutation_types`
641
+ (see [Configuration](#configuration)) or turn strict-equality on with `--strict-equality`.
764
642
 
765
- See [docs/mutation-types.md](docs/mutation-types.md) for the full catalog: every
766
- operator swap and structural mutation each family generates, its reported `type`,
767
- and the constructs each family intentionally leaves alone.
643
+ See [docs/mutation-types.md](docs/mutation-types.md) for the full catalog: every operator swap and structural mutation
644
+ each family generates, its reported `type`, and the constructs each family intentionally leaves alone.
768
645
 
769
646
  ## Equivalent mutants
770
647
 
771
- A mutation score of 100% is not always achievable, and a surviving mutation is
772
- not always a gap in your tests. Some mutations produce code that behaves
773
- **identically** to the original for every possible input, an *equivalent
774
- mutant*, and no test can ever kill it. For example, in a `max` implementation the
775
- original `a > b ? a : b` and the mutant `a >= b ? a : b` differ only when
776
- `a == b`, and both return the same value there, so the mutant survives no matter
777
- how thorough your tests are. Because equivalence is undecidable in the general
778
- case, treat survivors as *candidates* to review rather than guaranteed test gaps;
779
- once you confirm a survivor is equivalent, it is reasonable to accept a score
648
+ A mutation score of 100% is not always achievable, and a surviving mutation is not always a gap in your tests. Some
649
+ mutations produce code that behaves **identically** to the original for every possible input, an *equivalent mutant*,
650
+ and no test can ever kill it. For example, in a `max` implementation the original `a > b ? a : b` and the mutant
651
+ `a >= b ? a : b` differ only when `a == b`, and both return the same value there, so the mutant survives no matter how
652
+ thorough your tests are. Because equivalence is undecidable in the general case, treat survivors as *candidates* to
653
+ review rather than guaranteed test gaps; once you confirm a survivor is equivalent, it is reasonable to accept a score
780
654
  below 100%.
781
655
 
782
656
  ### Excluding a line with `# mutation_tester:disable`
783
657
 
784
- Once you have confirmed that a survivor is equivalent, annotate its line with a
785
- trailing `# mutation_tester:disable` comment (the same style as
786
- `# rubocop:disable`) so the mutator skips every mutation on that line and the
787
- excluded line drops out of the score and the report:
658
+ Once you have confirmed that a survivor is equivalent, annotate its line with a trailing `# mutation_tester:disable`
659
+ comment (the same style as `# rubocop:disable`) so the mutator skips every mutation on that line and the excluded line
660
+ drops out of the score and the report:
788
661
 
789
662
  ```ruby
790
663
  def max(a, b)
@@ -792,19 +665,24 @@ def max(a, b)
792
665
  end
793
666
  ```
794
667
 
795
- The marker is honoured only inside a real comment (never inside a string
796
- literal), and only on the line it sits on. See
797
- [docs/mutation-types.md](docs/mutation-types.md#excluding-a-line-with--mutation_testerdisable)
798
- for the console output and the current limits (no block ranges or per-type
799
- exclusion yet).
668
+ The marker is honoured only inside a real comment (never inside a string literal), and only on the line it sits on. See
669
+ [docs/mutation-types.md](docs/mutation-types.md#excluding-a-line-with--mutation_testerdisable) for the console output
670
+ and the current limits (no block ranges or per-type exclusion yet).
800
671
 
801
672
  ## Reports and output
802
673
 
674
+ Three reporters are available (`console`, `html`, `json`; all on by default). Choose which run with `--reporters` and
675
+ where their files land with `--output-dir`:
676
+
677
+ ```bash
678
+ bundle exec mutation_test --reporters json,html --output-dir build/mutation \
679
+ app/models/user.rb spec/models/user_spec.rb
680
+ ```
681
+
803
682
  ### Console report
804
683
 
805
- Surviving mutants are grouped by file and line (one header per location, all
806
- mutation variants listed under it) and each group shows a unified diff with a
807
- few lines of surrounding context:
684
+ Surviving mutants are grouped by file and line (one header per location, all mutation variants listed under it) and
685
+ each group shows a unified diff with a few lines of surrounding context:
808
686
 
809
687
  ```
810
688
  🧬 MUTATION TESTING REPORT
@@ -832,39 +710,28 @@ few lines of surrounding context:
832
710
  💡 Suggestion: Add tests to verify behavior for each of the 2 variants above
833
711
  ```
834
712
 
835
- When at least one mutant timed out, the summary also names the deadline those
836
- mutants were measured against and where it came from, so a genuine hang and a
837
- deadline calibrated from a slow test file are distinguishable at a glance:
713
+ When at least one mutant timed out, the summary also names the deadline those mutants were measured against and where
714
+ it came from, so a genuine hang and a deadline calibrated from a slow test file are distinguishable at a glance:
838
715
 
839
716
  ```
840
717
  Timeout: 3 ⏱️
841
718
  deadline: 6.50s (5x baseline 1.30s)
842
719
  ```
843
720
 
844
- With an explicit `config.timeout` / `--timeout` the same line reads
845
- `deadline: 30.00s (explicitly configured)`.
721
+ With an explicit `config.timeout` / `--timeout` the same line reads `deadline: 30.00s (explicitly configured)`.
846
722
 
847
723
  ### HTML report
848
724
 
849
- Beautiful interactive HTML report with:
850
-
851
- - Summary statistics
852
- - Mutation score visualization
853
- - Filterable mutation list
854
- - Survivors grouped by file and line, with all mutation variants under one card
855
- - Unified diffs with surrounding context for survived and timeout mutants
856
- - Detailed suggestions for improvements
725
+ Interactive HTML report with summary statistics, mutation score visualization, a filterable mutation list, survivors
726
+ grouped by file and line (all mutation variants under one card), unified diffs with surrounding context for survived
727
+ and timeout mutants, and detailed suggestions for improvements.
857
728
 
858
729
  ### JSON report and machine-readable output
859
730
 
860
- Machine-readable report for CI/CD integration and AI agents. Run with `--json`
861
- to get a single, clean JSON document on **stdout** and nothing else: the banner,
862
- progress spinner, colours and the "report saved" notice all go to **stderr**, so
863
- the stream is safe to pipe straight into `jq` or a parser. The process still
864
- exits `0` when the mutation score meets the configured threshold and `1` when it
865
- does not (a degraded single-file run exits `3`, see
866
- [Exit codes](#exit-codes-single-file-mode)), so the exit code remains a
867
- pass/fail signal.
731
+ Machine-readable report for CI/CD integration and AI agents. Run with `--json` to get a single, clean JSON document on
732
+ **stdout** and nothing else: the banner, progress spinner, colours and the "report saved" notice all go to **stderr**,
733
+ so the stream is safe to pipe straight into `jq` or a parser. The exit code remains the pass/fail signal
734
+ (see [Exit codes](#exit-codes)).
868
735
 
869
736
  ```bash
870
737
  bundle exec mutation_test --json examples/calculator.rb examples/calculator_spec.rb | jq .
@@ -873,83 +740,70 @@ bundle exec mutation_test --json examples/calculator.rb examples/calculator_spec
873
740
  bundle exec mutation_test --json lib/calculator.rb | jq .
874
741
  ```
875
742
 
876
- The same report is also written to `tmp/mutation_reports/mutation_report.json`
877
- (see `--output-dir`). A run that resolves exactly one source file, or an explicit
878
- `SOURCE_FILE TEST_FILE` pair, prints the plain per-file report; a multi-file run
879
- (a `FILE` list with more than one file, `--staged`, or `--glob`) prints one
880
- aggregate envelope with a condensed `survivors` array.
743
+ The same report is also written to `tmp/mutation_reports/mutation_report.json` (see `--output-dir`). A run that
744
+ resolves exactly one source file, or an explicit `SOURCE_FILE TEST_FILE` pair, prints the plain per-file report; a
745
+ multi-file run (a `FILE` list with more than one file, `--staged`, or `--glob`) prints one aggregate envelope with a
746
+ condensed `survivors` array.
881
747
 
882
- Every **surviving** mutant carries `file_path`, `line`, `original` and `mutated`,
883
- which is a concrete, located test gap: the worklist you can hand to an AI agent
884
- or a CI gate (see [CI/CD integration](#cicd-integration)).
748
+ Every **surviving** mutant carries `file_path`, `line`, `original` and `mutated`, which is a concrete, located test
749
+ gap: the worklist you can hand to an AI agent or a CI gate (see [CI/CD integration](#cicd-integration)).
885
750
 
886
- Full field-by-field documentation of both shapes (the single-file report and the
887
- multi-file envelope), the `schema_version` policy, and ready-to-use `jq` recipes
888
- live in [docs/json-schema.md](docs/json-schema.md).
751
+ Full field-by-field documentation of both shapes (the single-file report and the multi-file envelope), the
752
+ `schema_version` policy, and ready-to-use `jq` recipes live in [docs/json-schema.md](docs/json-schema.md).
889
753
 
890
754
  ## Pre-push hook
891
755
 
892
- Gate your pushes locally: run mutation testing on the file(s) you touched and
893
- block the push when the score is under your bar, the fast-feedback sibling of
894
- the CI gate below (see [CI/CD integration](#cicd-integration)).
756
+ Gate your pushes locally: run mutation testing on the file(s) you touched and block the push when the score is under
757
+ your bar, the fast-feedback sibling of the CI gate below.
895
758
 
896
- The gem ships a ready-to-copy hook at
897
- [`examples/hooks/pre-push`](examples/hooks/pre-push) (installed with the gem, so
898
- you have it offline). It runs `mutation_test --json`, reads the score with
899
- [`jq`](https://jqlang.github.io/jq/), and on a below-threshold run lists the
900
- surviving mutants (file, line, what changed) so you see which gaps to close.
901
- Install it with:
759
+ The gem ships a ready-to-copy hook at [`examples/hooks/pre-push`](examples/hooks/pre-push) (installed with the gem, so
760
+ you have it offline). It runs `mutation_test --json`, reads the score with [`jq`](https://jqlang.github.io/jq/), and on
761
+ a below-threshold run lists the surviving mutants (file, line, what changed) so you see which gaps to close. Install it
762
+ with:
902
763
 
903
764
  ```bash
904
765
  cp examples/hooks/pre-push .git/hooks/pre-push
905
766
  chmod +x .git/hooks/pre-push
906
767
  ```
907
768
 
908
- Then edit the `THRESHOLD` and the `SOURCE TEST` pair(s) at the top of the copied
909
- hook.
769
+ Then edit the `THRESHOLD` and the `SOURCE TEST` pair(s) at the top of the copied hook.
910
770
 
911
- Prefer to gate on your project's configured threshold rather than one written
912
- into the hook? `mutation_test` already exits non-zero when the score is below
913
- `config.minimum_score` (default 80, see [Configuration](#configuration)), so you
914
- can drop the `jq` comparison and let the exit code be the gate:
771
+ Prefer to gate on your project's configured threshold rather than one written into the hook? `mutation_test` already
772
+ exits non-zero when the score is below `config.minimum_score` (default 80), so you can drop the `jq` comparison and let
773
+ the exit code be the gate:
915
774
 
916
775
  ```sh
917
776
  bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb || exit 1
918
777
  ```
919
778
 
920
- `--minimum-score N` sets that threshold for a single run, which is how you start
921
- below 80 in an existing codebase and ratchet the number up over time:
779
+ `--minimum-score N` sets that threshold for a single run, which is how you start below 80 in an existing codebase and
780
+ ratchet the number up over time:
922
781
 
923
782
  ```sh
924
783
  bundle exec mutation_test --glob 'app/**/*.rb' \
925
784
  --spec-map '\Aapp/(.+)\.rb\z=>spec/\1_spec.rb' --minimum-score 60 || exit 1
926
785
  ```
927
786
 
928
- lefthook or overcommit users: call the shipped hook from your `pre-push` step
929
- instead of writing to `.git/hooks/`.
787
+ lefthook or overcommit users: call the shipped hook from your `pre-push` step instead of writing to `.git/hooks/`.
930
788
 
931
789
  ## CI/CD integration
932
790
 
933
- Run MutationTester as a CI quality gate: the CLI exits non-zero when the mutation
934
- score is below the threshold, so it fails the job with no extra configuration.
791
+ Run MutationTester as a CI quality gate: the CLI exits non-zero when the mutation score is below the threshold, so it
792
+ fails the job with no extra configuration.
935
793
 
936
- The gem ships ready-to-copy GitHub Actions workflows (installed alongside the
937
- gem, so you have them offline too):
794
+ The gem ships ready-to-copy GitHub Actions workflows (installed alongside the gem, so you have them offline too):
938
795
 
939
- - [`examples/github_actions/mutation_test.yml`](examples/github_actions/mutation_test.yml)
940
- is the maintained template. Add the gem to your bundle, copy it to
941
- `.github/workflows/`, edit the `EDIT:` lines, and it runs the gate, uploads the
942
- HTML/JSON reports from `tmp/mutation_reports/` as an artifact (even on
943
- failure), and fails the job below threshold. It also carries commented variants
944
- for parallel execution, several file pairs, and an incremental pull-request gate.
945
- - [`examples/github_actions/ai_mutation_gate.yml`](examples/github_actions/ai_mutation_gate.yml)
946
- is the AI gate: the same pass/fail gate, plus it writes the surviving-mutant
947
- worklist to the GitHub job summary and uploads `survivors.json` for an agent to
948
- turn into missing tests.
796
+ - [`examples/github_actions/mutation_test.yml`](examples/github_actions/mutation_test.yml) is the maintained template.
797
+ Add the gem to your bundle, copy it to `.github/workflows/`, edit the `EDIT:` lines, and it runs the gate, uploads
798
+ the HTML/JSON reports from `tmp/mutation_reports/` as an artifact (even on failure), and fails the job below
799
+ threshold. It also carries commented variants for parallel execution, several file pairs, and an incremental
800
+ pull-request gate.
801
+ - [`examples/github_actions/ai_mutation_gate.yml`](examples/github_actions/ai_mutation_gate.yml) is the AI gate: the
802
+ same pass/fail gate, plus it writes the surviving-mutant worklist to the GitHub job summary and uploads
803
+ `survivors.json` for an agent to turn into missing tests.
949
804
 
950
- See [docs/ci.md](docs/ci.md) for the full recipes: a 5-minute setup, minimal
951
- inline and pull-request workflows, machine mode as a gate and artifact, and the
952
- AI workflow.
805
+ See [docs/ci.md](docs/ci.md) for the full recipes: a 5-minute setup, minimal inline and pull-request workflows, machine
806
+ mode as a gate and artifact, and the AI workflow.
953
807
 
954
808
  ## Troubleshooting
955
809
 
@@ -965,18 +819,14 @@ This usually means:
965
819
 
966
820
  ### Database Conflicts in Parallel Mode
967
821
 
968
- If you see errors like "database is locked" or "record not found":
969
-
970
- **Solution**: Use serial execution (the default):
971
-
972
- ```bash
973
- bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
974
- ```
822
+ If you see errors like "database is locked" or "record not found", your parallel workers are sharing one test database.
975
823
 
976
- Or if using environment variable, make sure it's not set or set to 1:
824
+ **Solution**: Force serial execution with `-p 1` (or `MUTATION_TESTER_PARALLEL_PROCESSES=1`), or keep parallelism by
825
+ giving each worker its own database with `--worker-env`
826
+ (see [Making parallelism work with Rails](#making-parallelism-work-with-rails)):
977
827
 
978
828
  ```bash
979
- MUTATION_TESTER_PARALLEL_PROCESSES=1 bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
829
+ bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb -p 1
980
830
  ```
981
831
 
982
832
  ### Slow Execution
@@ -986,10 +836,11 @@ run.
986
836
 
987
837
  **Tips for faster execution**:
988
838
 
989
- - Test only critical files (don't test everything)
990
- - Use parallel execution if your tests support it (`--parallel N`)
991
- - Consider using faster test databases (SQLite in-memory for unit tests)
839
+ - Test only critical files (don't test everything), or only changed files (`--staged`, `--since`)
992
840
  - Focus on high-value code (models, services, core logic)
841
+ - Keep the parallel and in-memory defaults working (fix the issues that force a fallback; every fallback is announced
842
+ on stderr with its reason)
843
+ - Consider using faster test databases (SQLite in-memory for unit tests)
993
844
 
994
845
  ### "No Mutations Generated"
995
846
 
@@ -1012,32 +863,27 @@ If mutation testing stops at a `debugger` or `binding.pry` statement:
1012
863
 
1013
864
  ### Parser Version Warning on Newer Ruby (Supported Syntax Level)
1014
865
 
1015
- MutationTester parses your source with the `parser` gem. The newest published
1016
- `parser` line recognizes **Ruby 3.3 syntax**; there is not yet a release that
1017
- understands Ruby 3.4+/4.x syntax. So when you run on Ruby 3.4 or newer you may
1018
- see one line on stderr per run:
866
+ MutationTester parses your source with the `parser` gem. The newest published `parser` line recognizes **Ruby 3.3
867
+ syntax**; there is not yet a release that understands Ruby 3.4+/4.x syntax. So when you run on Ruby 3.4 or newer you
868
+ may see one line on stderr per run:
1019
869
 
1020
870
  ```
1021
871
  warning: parser/current is loading parser/ruby33, which recognizes 3.3.x-compliant syntax, but you are running 4.0.2.
1022
872
  ```
1023
873
 
1024
- This warning is **benign**. It only means the parser recognizes syntax up to
1025
- Ruby 3.3. The gem itself runs fine on Ruby 3.4+/4.x, and files written in Ruby
1026
- 3.3-and-earlier syntax are mutated normally.
874
+ This warning is **benign**. It only means the parser recognizes syntax up to Ruby 3.3. The gem itself runs fine on
875
+ Ruby 3.4+/4.x, and files written in Ruby 3.3-and-earlier syntax are mutated normally.
1027
876
 
1028
- The only real limitation is a source file that relies on syntax introduced
1029
- after Ruby 3.3. Such a file cannot be parsed, so it reports an explicit
1030
- `Failed to parse source file` and the run fails - it never fakes a passing
1031
- score. The warning is left in place on purpose as an honest signal; it is not
1032
- globally silenced.
877
+ The only real limitation is a source file that relies on syntax introduced after Ruby 3.3. Such a file cannot be
878
+ parsed, so it reports an explicit `Failed to parse source file` and the run fails - it never fakes a passing score. The
879
+ warning is left in place on purpose as an honest signal; it is not globally silenced.
1033
880
 
1034
881
  ## Development
1035
882
 
1036
883
  Run `bundle install`, then run `rake spec` to run the tests.
1037
884
 
1038
- The gem includes working examples for both RSpec and Minitest under the
1039
- `examples/` directory (a sample `Calculator` class with an RSpec spec and a
1040
- Minitest test). Run them directly with the CLI, or through the example rake tasks:
885
+ The gem includes working examples for both RSpec and Minitest under the `examples/` directory (a sample `Calculator`
886
+ class with an RSpec spec and a Minitest test). Run them directly with the CLI, or through the example rake tasks:
1041
887
 
1042
888
  ```bash
1043
889
  # Directly with the CLI
@@ -1056,12 +902,8 @@ rake example:minitest_parallel # Parallel execution
1056
902
  rake example
1057
903
  ```
1058
904
 
1059
- Each example will:
1060
-
1061
- - Run mutation tests on a sample Calculator class
1062
- - Generate console, HTML, and JSON reports
1063
- - Show mutation score and quality metrics
1064
- - Demonstrate the difference between serial and parallel execution
905
+ Each example runs mutation tests on the sample Calculator class, generates console, HTML, and JSON reports, and shows
906
+ the difference between serial and parallel execution.
1065
907
 
1066
908
  ## Contributing
1067
909