mutation_tester 1.5.0 → 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,336 +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`). A parallel `in_memory` run falls back to `fork` unless `--after-fork` is also given; a serial run (`-p 1`) decides mutants in a shadow workspace instead of mutating the checkout in place. See [Making parallelism work with Rails](#making-parallelism-work-with-rails). |
214
- | `--after-fork FILE` | Ruby file loaded inside each preloaded in-memory clone right after it forks and receives its per-worker environment (see `--worker-env`), so the app can re-establish per-worker state such as its database connection. With `--worker-env` set, this keeps the `in_memory` runner available in parallel runs. A clone whose after-fork file raises is dropped with a stderr warning and its worker falls back to file-based execution. See [Making parallelism work with Rails](#making-parallelism-work-with-rails). |
215
- | `--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). |
216
- | `-h, --help` | Show help message. |
217
- | `-v, --version` | Show version. |
218
- | `--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). |
219
- | `--no-progress` | Disable progress display. |
220
- | `--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). |
221
- | `--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). |
222
- | `--reporters LIST` | Comma-separated reporters to run: `console`, `html`, `json` (default: `console,html,json`). An unknown name errors and exits 1. |
223
- | `--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:
224
168
 
225
169
  ```bash
226
- # Choose which reporters run and where their files land
227
- bundle exec mutation_test --reporters json,html --output-dir build/mutation \
228
- 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)
229
173
  ```
230
174
 
231
- ### Exit codes (single-file mode)
232
-
233
- - `0` - the run passed (score met the threshold, or `fail_on_threshold` is disabled).
234
- - `1` - the mutation score is below the threshold, or the input is unusable (missing
235
- file, unknown reporter, source with a syntax error).
236
- - `2` - a usage error (conflicting flags; see the batch sections below).
237
- - `3` - the run aborted or degraded before reaching a verdict: the shadow workspace
238
- was unreliable (the unmutated source failed there, or the workspace copy of the
239
- source turned out not to be the code the tests execute), or every mutant ended as
240
- `error`/`stillborn` so nothing was scored.
241
- This signals an infrastructure or runner problem, not a test-quality gap, so CI
242
- hooks can distinguish it from a genuine threshold failure.
243
- - `130` - interrupted with Ctrl+C.
244
-
245
- Batch modes (`FILE...` lists, `--staged`, `--glob`) keep the exit codes documented in
246
- their sections below (`0`/`1`/`2`); a degraded file is named explicitly in the batch
247
- summary instead of being blamed on the threshold.
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`.
248
177
 
249
- ### Running with rake
250
-
251
- You can also run mutation tests through rake tasks.
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.
252
183
 
253
- In a **non-Rails** project, require the tasks from your `Rakefile`:
254
-
255
- ```ruby
256
- # Rakefile
257
- require 'mutation_tester/rake_task'
258
- ```
259
-
260
- In a **Rails** app the tasks load automatically through the gem's railtie, so no
261
- Rakefile change is needed.
262
-
263
- Then run either task. Rake takes the file arguments inside brackets (not
264
- 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`:
265
186
 
266
187
  ```bash
267
- # Top-level task
268
- bundle exec rake "mutation_test[app/models/user.rb,spec/models/user_spec.rb]"
269
-
270
- # Namespaced task
271
- 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
272
191
  ```
273
192
 
274
- ### Programmatic usage
275
-
276
- You can also run the gem directly from Ruby with `MutationTester.run(source, test)`:
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:
277
195
 
278
- ```ruby
279
- # RSpec
280
- MutationTester.run('examples/calculator.rb', 'examples/calculator_spec.rb')
281
-
282
- # Minitest
283
- MutationTester.run('examples/calculator.rb', 'examples/calculator_minitest.rb')
196
+ ```bash
197
+ bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
284
198
  ```
285
199
 
286
- ### File lists and --staged: test what you changed
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.
287
204
 
288
- Passing one or more source files is the main interface. Each file is mapped to
289
- its spec by convention (`lib/X.rb` -> `spec/X_spec.rb`, override with
290
- `--spec-glob`), and the whole list runs as one aggregated batch: every file is
291
- processed, the summary shows one `PASS`/`FAIL` line per file, reports land in
292
- per-file subdirectories, and the exit code reflects the whole run.
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.
293
208
 
294
- ```bash
295
- bundle exec mutation_test lib/calculator.rb lib/parser.rb
209
+ #### Batch behaviour and skipped files
296
210
 
297
- # The natural "test what I changed" flow:
298
- bundle exec mutation_test --staged
299
- bundle exec mutation_test $(git diff --cached --name-only)
300
- ```
211
+ All multi-file modes (`FILE...` lists, `--staged`, `--glob`) behave the same way:
301
212
 
302
- `--staged` reads the list from the git staging area (`git diff --cached
303
- --name-only`), so the two commands above are equivalent when run from the
304
- repository root. Files staged as deleted are ignored. Outside a git repository,
305
- or when nothing is staged, the CLI prints a readable error instead of running.
306
- `--staged` cannot be combined with positional arguments or `--glob`.
307
-
308
- When any mutant survives, the aggregate summary ends with a survivors section:
309
- one `file:line original -> mutated` line per surviving mutant. A surviving
310
- mutant is a change to your code that your tests do not detect, so each line is
311
- a concrete test gap to close. With zero survivors the section is absent.
312
-
313
- The list may contain anything a real `git diff` produces; unmutable entries are
314
- reported as `SKIPPED` with an explicit reason and never count as a success:
315
-
316
- - **file not found** - the path does not exist.
317
- - **not a Ruby source file** - e.g. a staged `.md` or config file.
318
- - **a test file, not a mutable source** - a test file passed directly
319
- (`*_spec.rb`, `*.spec.rb`, `*_test.rb`, `test_*.rb`, or minitest content).
320
- - **no matching spec file** - the convention (or `--spec-glob` / `--spec-map`)
321
- points at a spec that does not exist; the expected path is printed.
322
-
323
- Exit codes: `0` when at least one file was processed and every processed file
324
- met the threshold; `1` when any processed file was below threshold or when
325
- every listed file was skipped (nothing was actually mutation-tested); `2` for
326
- usage errors (flag conflicts, `--staged` outside a git repository).
327
-
328
- **Legacy pair heuristic:** exactly two arguments where the second is recognized
329
- as a test file (by the name patterns above or by minitest content) keep the
330
- original `SOURCE_FILE TEST_FILE` behavior. Two source files enter list mode. An
331
- RSpec test file with an unconventional name (no `_spec.rb` suffix) is not
332
- recognized, so that pair is treated as a file list; rename the test or use the
333
- conventional layout to get the explicit pair.
334
-
335
- ### Batch mode: run many files in one command
336
-
337
- `--glob PATTERN` mutation-tests every source file the pattern matches in a single
338
- run, so you no longer need to script a loop around `mutation_test` or depend on
339
- the Rails-only `rake mutation:test_models` task.
340
-
341
- Each matched source file is mapped to its spec by convention: `lib/X.rb` becomes
342
- `spec/X_spec.rb`. See [Mapping sources to specs](#mapping-sources-to-specs) for
343
- the two ways to override that convention.
344
-
345
- Behaviour:
346
-
347
- - **Every file is processed.** A file whose score is below the threshold does not
348
- abort the batch; the run continues to the next file (the `mutation:test_models`
349
- pattern).
350
- - **Reports never overwrite each other.** Each processed file writes its reporter
351
- output to its own subdirectory under `--output-dir` (a slug derived from the
352
- source path), so per-file HTML/JSON reports coexist.
353
- - **A console aggregate summary** is printed at the end: one line per processed
354
- file with its score and `PASS`/`FAIL` against the threshold, followed by a
355
- clearly separated `SKIPPED` list.
356
- - **A source file with no matching spec is `SKIPPED`**, reported explicitly and
357
- never counted as a success. A single skipped file next to processed ones does
358
- not by itself fail the run, but a run that skipped *every* matched file
359
- measured nothing and fails (see the exit codes below).
360
-
361
- Exit codes:
362
-
363
- - `0` - at least one file was mutation-tested and every processed file met the
364
- mutation score threshold. A `--since` run where nothing changed also exits `0`
365
- (see below).
366
- - `1` - at least one processed file was below threshold, the glob matched no
367
- source files at all, every matched file was skipped so nothing was actually
368
- mutation-tested, or `--fail-fast` stopped the run at a surviving mutant.
369
- - `2` - a usage error: `--spec-glob` or `--spec-map` with an explicit
370
- `SOURCE_FILE TEST_FILE` pair, a malformed `--spec-map` rule, `--since` given
371
- without `--glob`, `--staged` combined with positional arguments or `--glob`,
372
- or `--since`/`--staged` used outside a git repository (for `--since` also an
373
- unknown revision).
374
-
375
- The "every matched file was skipped" case is deliberate: a typo in
376
- `--spec-glob`/`--spec-map`, or a refactor that moves the test directory, would
377
- otherwise leave a green CI step that measured nothing.
378
-
379
- ```bash
380
- # Minitest project, JSON report per file, custom output directory
381
- bundle exec mutation_test --glob 'lib/**/*.rb' --spec-glob 'test/{name}_test.rb' \
382
- --reporters json --output-dir build/mutation
383
- ```
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.
384
228
 
385
229
  ### Mapping sources to specs
386
230
 
387
- Every mode that takes more than an explicit `SOURCE_FILE TEST_FILE` pair (a
388
- positional `FILE` list, `--staged`, `--glob`) derives the test path from the
389
- 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:
390
233
 
391
234
  1. `--spec-map 'PATTERN=>REPLACEMENT'` - regular-expression rules.
392
235
  2. `--spec-glob TEMPLATE` - a `{name}` template (default `spec/{name}_spec.rb`).
393
236
 
394
- **`--spec-glob TEMPLATE`** substitutes `{name}`, which is the source path with a
395
- leading `lib/` segment removed and the `.rb` extension stripped, subdirectories
396
- preserved (`lib/foo/bar.rb` -> `spec/foo/bar_spec.rb`). Because `{name}` is one
397
- value, a template can only add a prefix and a suffix around the source path. For
398
- 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:
399
241
 
400
242
  ```bash
401
243
  bundle exec mutation_test --glob 'lib/**/*.rb' --spec-glob 'test/{name}_test.rb'
402
244
  ```
403
245
 
404
- **`--spec-map 'PATTERN=>REPLACEMENT'`** covers the layouts a template cannot
405
- express: those that substitute *inside* the path, after a variable-length
406
- prefix. `PATTERN` is a Ruby regular expression matched against the whole source
407
- path (a leading `./` removed); the first `=>` separates it from `REPLACEMENT`,
408
- which may use `\1`, `\2`, ... backreferences and produces the whole spec path.
409
- 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.
410
250
 
411
251
  - The flag is repeatable and the first matching rule wins.
412
- - A source that matches no rule falls back to `--spec-glob` (or the default
413
- 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.
414
254
  - Quote the rule in single quotes so the shell leaves the backslashes alone.
415
255
 
416
- Rails and Rails-shaped layouts, where the rule is "replace the `app/` segment
417
- 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":
418
258
 
419
259
  ```bash
420
260
  # Plain Rails, Minitest: app/models/current.rb -> test/models/current_test.rb
@@ -438,43 +278,78 @@ bundle exec mutation_test --glob '{app,lib}/**/*.rb' \
438
278
  --spec-glob 'test/{name}_test.rb'
439
279
  ```
440
280
 
441
- When a rule produces a path that does not exist, the file is reported as
442
- `SKIPPED (no matching spec file)` with the expected path printed, and a run in
443
- 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. |
444
310
 
445
- ### Incremental mode: mutate only what changed
311
+ ### Exit codes
446
312
 
447
- `--since REV` narrows a `--glob` batch to the files that changed since a git
448
- 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. |
449
320
 
450
- ```bash
451
- bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
452
- ```
321
+ ### Running with rake
453
322
 
454
- Behaviour:
323
+ In a **non-Rails** project, require the tasks from your `Rakefile`:
455
324
 
456
- - A matched file counts as **changed** when `git diff --name-only REV` lists it;
457
- new files the revision does not know about (committed or still untracked) also
458
- count as changed.
459
- - Unchanged matched files are reported as skipped in the batch summary
460
- (`SKIPPED (unchanged since REV)`), never mutated.
461
- - When nothing matched by the glob changed since `REV`, the run succeeds with a
462
- "Nothing to mutate" message and exit code `0`, so a PR that does not touch
463
- your sources does not fail the gate.
464
- - Outside a git repository (or without a `git` executable), or with a revision
465
- the repository does not know, the CLI prints a readable error and exits `2`
466
- before any mutation runs.
325
+ ```ruby
326
+ # Rakefile
327
+ require 'mutation_tester/rake_task'
328
+ ```
467
329
 
468
- ### 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.
469
331
 
470
- `--fail-fast` turns the run into a cheap gate: the run stops as soon as one
471
- mutant survives, the reports contain the results obtained up to that point with
472
- a clear interruption notice, and the process finishes with exit code `1`. It
473
- works in single-file mode, with `--glob` (the batch stops and remaining files
474
- 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:
475
334
 
476
335
  ```bash
477
- 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')
478
353
  ```
479
354
 
480
355
  ## Configuration
@@ -572,14 +447,12 @@ end
572
447
 
573
448
  ## Execution model
574
449
 
575
- MutationTester runs in parallel by default and picks the fastest safe execution
576
- runner automatically. This section covers when to override those defaults, how
577
- 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.
578
452
 
579
453
  ### Parallel execution (on by default)
580
454
 
581
- By default, mutation_tester runs in **parallel**: the process count is derived
582
- 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.
583
456
  Force a specific count, or serial execution, when your tests need it:
584
457
 
585
458
  ```bash
@@ -593,48 +466,33 @@ bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb -p 1
593
466
  MUTATION_TESTER_PARALLEL_PROCESSES=4 bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
594
467
  ```
595
468
 
596
- 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
597
470
  auto-derived core count; an explicit `config.parallel_processes` assignment in Ruby also replaces the auto default. An
598
- invalid value (less than 1, or non-numeric) falls back to 1 with a warning on stderr. `-p 1` forces serial execution.
599
-
600
- **⚠️ Force serial execution with `-p 1` if your tests share database state!**
601
-
602
- In parallel mode each mutant runs in an isolated shadow workspace. Every `.rb`
603
- file is a physical copy (non-Ruby files stay symlinks for speed), so mutations
604
- apply correctly even when a spec loads the source indirectly (e.g. via
605
- `spec_helper`), and `$LOAD_PATH` entries pointing into the project resolve
606
- inside the workspace, so a test file that reaches its source through
607
- `require "test_helper"` gets the mutated copy too. The parallel mutation score
608
- therefore matches serial.
609
-
610
- Before the first mutant, the run proves this in the workspace itself: the
611
- unmutated source must pass there, and the same suite must fail once that copy of
612
- the source is replaced by a `raise`. A run whose tests pass even then is aborted
613
- as an infrastructure failure (exit code `3`) rather than reported as a 0.0%
614
- score, because the mutated file is demonstrably not the code being executed.
471
+ invalid value (less than 1, or non-numeric) falls back to 1 with a warning on stderr.
615
472
 
616
- ### When to use serial vs parallel execution
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).
617
478
 
618
- Use **parallel execution** (the default) 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.
619
484
 
620
- - **Pure Ruby classes** - No database, no shared state
621
- - **Unit tests with mocks** - Fast and independent tests
622
- - **Large codebases** - Significant time savings
623
- - **CI/CD with powerful machines** - Make use of available resources
624
-
625
- Force **serial execution with `-p 1`** for:
626
-
627
- - ✅ **Rails applications with database** - Avoids conflicts
628
- - ✅ **Tests that share state** - No interference between test runs
629
- - ✅ **First time using mutation testing** - Easier to debug
630
- - ✅ **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.
631
489
 
632
490
  ### Making parallelism work with Rails
633
491
 
634
- Parallel workers get an isolated filesystem (each mutant runs in its own shadow workspace), but they share one
635
- **database** unless you give each worker its own. If your app is already set up for `parallel_tests` (a `database.yml`
636
- keyed on `TEST_ENV_NUMBER` and per-worker databases created with `rake parallel:prepare`), `--worker-env` bridges the
637
- 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.
638
496
 
639
497
  **`--worker-env NAME`** sets the environment variable `NAME` to a distinct value in each worker before it boots its
640
498
  test environment, following the `parallel_tests` `TEST_ENV_NUMBER` convention:
@@ -712,39 +570,15 @@ model runs on serial `-p 1`. `--worker-env` is only useful once the databases ex
712
570
 
713
571
  ### Execution runners (fork, spawn, in-memory)
714
572
 
715
- Every mutant is executed by one of three runners, and `auto` (the default) picks
716
- the fastest safe one, announcing every fallback on stderr:
717
-
718
- - **in_memory** (default where supported): re-evaluates the mutated source in the
719
- memory of a fresh fork of a preloaded process, with zero file writes per mutant
720
- and no shadow workspaces. RSpec and Minitest; the fastest path. Mutations that only
721
- take effect at class-load time (constants consumed by macros, `validates`/`has_many`/
722
- `before_save`/`scope`/`attribute`, anything inside an `included do` block) cannot
723
- be observed by re-evaluating source in a preloaded process, so those mutants are
724
- routed automatically to the file-based path and the rest still run in memory (see
725
- below); the combined score matches a full `fork` run.
726
- - **fork**: preloads the environment once (RubyGems, Bundler, the test framework)
727
- and forks a fresh child per mutant. RSpec and Minitest on platforms with
728
- `Process.fork`; removes most of the fixed per-mutant boot cost.
729
- - **spawn**: starts one full process per mutant (`bundle exec rspec ...` or
730
- `bundle exec ruby test_file.rb`). Slower per mutant, but works everywhere
731
- (the only runner on platforms without `Process.fork`).
732
-
733
- `auto` tries `in_memory`, then `fork`, then `spawn`; every step down prints one
734
- stderr warning with its reason, so a fallback is never silent. All runners
735
- produce identical scores and per-mutant statuses and enforce the same hard
736
- per-mutant timeout (monotonic deadline plus a process-group kill).
737
-
738
- **Load-time mutants under in-memory (Rails).** The in-memory runner classifies each
739
- mutation by its AST context. A mutation inside a method body defined directly in a
740
- class/module is re-appliable in memory and runs there (fast). A mutation on a
741
- class/module-body statement (a constant, a `validates`/`has_many`/`before_save`/
742
- `scope`/`attribute` macro, or anything inside `included do ... end`) is decided
743
- file-based within the same run, because re-evaluating the source does not re-run
744
- those class-load registrations. This keeps in-memory speed for the common case
745
- while matching a full `fork` score on Rails concerns and models. When at least one
746
- mutant is routed this way, the run prints one stderr notice. It is automatic; you
747
- 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`).
748
582
 
749
583
  | Mode | Picked by `auto` when | Falls back to |
750
584
  |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
@@ -752,84 +586,78 @@ do not need to pick `--runner fork` for correctness on load-time code.
752
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 |
753
587
  | `spawn` | the platform has no `Process.fork` | nothing; it works everywhere |
754
588
 
755
- Force a specific runner (skipping the auto attempts) with the `--runner
756
- fork|spawn|in_memory` flag, the `MUTATION_TESTER_RUNNER` environment variable, or
757
- `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`:
758
602
 
759
603
  ```bash
760
604
  mutation_test --runner spawn lib/calculator.rb spec/calculator_spec.rb
761
605
  ```
762
606
 
763
- Reach for `--runner spawn` when you want maximum isolation or are debugging a
764
- suspicious result from a preloaded runner, and `--runner fork` when your source
765
- is not cleanly re-evaluable in memory but you still want the preloaded-environment
766
- speed. For the full per-runner mechanics, when to force each one, and the complete
767
- fork and in-memory limitation lists (frozen classes, load-time `defined?` guards,
768
- 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
769
611
  [docs/execution-runners.md](docs/execution-runners.md#execution-runners-fork-spawn-in-memory).
770
612
 
771
613
  ### Stopping a mutant at its first failing test
772
614
 
773
- A mutant only needs one failing test to be killed, so every mutant run stops at
774
- its first failure: RSpec mutant runs get `--fail-fast`, and Minitest mutant runs
775
- get a preloaded reporter that aborts the run the same way (both on the file-based
776
- runners and inside the preloaded fork worker). This never changes a verdict, only
777
- the work done to reach it: a run that stops early had already failed, and a run
778
- with no failure is unaffected and still executes every test.
779
-
780
- It matters most for a mutant that breaks something every test touches (a broken
781
- class body, a constant every example reads). Such a mutant used to pay the full
782
- test file once per mutant, which on a large test file can exceed the per-mutant
783
- deadline and turn a decided kill into a reported timeout. Adding tests to the file
784
- 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
785
623
  unaffected: they are expected to pass, and a passing run runs every test.
786
624
 
787
625
  ### Test selection (fast kill with full-file confirmation)
788
626
 
789
- For RSpec suites on the file-based runners, MutationTester runs each mutant in
790
- two phases: it first runs only the examples whose group matches the mutated
791
- method (`rspec spec_file -e '#foo' -e '.foo'`) to kill it fast, then confirms a
792
- passing subset against the full spec file before a mutant can be reported as
793
- survived, so selection never introduces false survivors. It degrades to the full
794
- file when the mutant is not inside a method, the spec has no matching group, or
795
- the suite is Minitest; the in-memory runner skips selection entirely (its
796
- examples are already loaded). Disable it with `--no-test-selection` or
797
- `config.test_selection = false`. See
798
- [docs/execution-runners.md](docs/execution-runners.md#test-selection-fast-kill-with-full-file-confirmation)
799
- 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.
800
635
 
801
636
  ## Mutation types
802
637
 
803
- MutationTester generates several families of mutations, enabled by default:
804
- arithmetic, bitwise compound-assignment, comparison, logical, boolean, number,
805
- string, conditional, call-removal, nil-injection and argument mutations. A
806
- strict-equality family is opt-in. Enable or disable individual families through
807
- `config.mutation_types` (see [Configuration](#configuration)) or turn
808
- 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`.
809
642
 
810
- See [docs/mutation-types.md](docs/mutation-types.md) for the full catalog: every
811
- operator swap and structural mutation each family generates, its reported `type`,
812
- 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.
813
645
 
814
646
  ## Equivalent mutants
815
647
 
816
- A mutation score of 100% is not always achievable, and a surviving mutation is
817
- not always a gap in your tests. Some mutations produce code that behaves
818
- **identically** to the original for every possible input, an *equivalent
819
- mutant*, and no test can ever kill it. For example, in a `max` implementation the
820
- original `a > b ? a : b` and the mutant `a >= b ? a : b` differ only when
821
- `a == b`, and both return the same value there, so the mutant survives no matter
822
- how thorough your tests are. Because equivalence is undecidable in the general
823
- case, treat survivors as *candidates* to review rather than guaranteed test gaps;
824
- 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
825
654
  below 100%.
826
655
 
827
656
  ### Excluding a line with `# mutation_tester:disable`
828
657
 
829
- Once you have confirmed that a survivor is equivalent, annotate its line with a
830
- trailing `# mutation_tester:disable` comment (the same style as
831
- `# rubocop:disable`) so the mutator skips every mutation on that line and the
832
- 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:
833
661
 
834
662
  ```ruby
835
663
  def max(a, b)
@@ -837,19 +665,24 @@ def max(a, b)
837
665
  end
838
666
  ```
839
667
 
840
- The marker is honoured only inside a real comment (never inside a string
841
- literal), and only on the line it sits on. See
842
- [docs/mutation-types.md](docs/mutation-types.md#excluding-a-line-with--mutation_testerdisable)
843
- for the console output and the current limits (no block ranges or per-type
844
- 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).
845
671
 
846
672
  ## Reports and output
847
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
+
848
682
  ### Console report
849
683
 
850
- Surviving mutants are grouped by file and line (one header per location, all
851
- mutation variants listed under it) and each group shows a unified diff with a
852
- 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:
853
686
 
854
687
  ```
855
688
  🧬 MUTATION TESTING REPORT
@@ -877,39 +710,28 @@ few lines of surrounding context:
877
710
  💡 Suggestion: Add tests to verify behavior for each of the 2 variants above
878
711
  ```
879
712
 
880
- When at least one mutant timed out, the summary also names the deadline those
881
- mutants were measured against and where it came from, so a genuine hang and a
882
- 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:
883
715
 
884
716
  ```
885
717
  Timeout: 3 ⏱️
886
718
  deadline: 6.50s (5x baseline 1.30s)
887
719
  ```
888
720
 
889
- With an explicit `config.timeout` / `--timeout` the same line reads
890
- `deadline: 30.00s (explicitly configured)`.
721
+ With an explicit `config.timeout` / `--timeout` the same line reads `deadline: 30.00s (explicitly configured)`.
891
722
 
892
723
  ### HTML report
893
724
 
894
- Beautiful interactive HTML report with:
895
-
896
- - Summary statistics
897
- - Mutation score visualization
898
- - Filterable mutation list
899
- - Survivors grouped by file and line, with all mutation variants under one card
900
- - Unified diffs with surrounding context for survived and timeout mutants
901
- - 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.
902
728
 
903
729
  ### JSON report and machine-readable output
904
730
 
905
- Machine-readable report for CI/CD integration and AI agents. Run with `--json`
906
- to get a single, clean JSON document on **stdout** and nothing else: the banner,
907
- progress spinner, colours and the "report saved" notice all go to **stderr**, so
908
- the stream is safe to pipe straight into `jq` or a parser. The process still
909
- exits `0` when the mutation score meets the configured threshold and `1` when it
910
- does not (a degraded single-file run exits `3`, see
911
- [Exit codes](#exit-codes-single-file-mode)), so the exit code remains a
912
- 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)).
913
735
 
914
736
  ```bash
915
737
  bundle exec mutation_test --json examples/calculator.rb examples/calculator_spec.rb | jq .
@@ -918,83 +740,70 @@ bundle exec mutation_test --json examples/calculator.rb examples/calculator_spec
918
740
  bundle exec mutation_test --json lib/calculator.rb | jq .
919
741
  ```
920
742
 
921
- The same report is also written to `tmp/mutation_reports/mutation_report.json`
922
- (see `--output-dir`). A run that resolves exactly one source file, or an explicit
923
- `SOURCE_FILE TEST_FILE` pair, prints the plain per-file report; a multi-file run
924
- (a `FILE` list with more than one file, `--staged`, or `--glob`) prints one
925
- 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.
926
747
 
927
- Every **surviving** mutant carries `file_path`, `line`, `original` and `mutated`,
928
- which is a concrete, located test gap: the worklist you can hand to an AI agent
929
- 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)).
930
750
 
931
- Full field-by-field documentation of both shapes (the single-file report and the
932
- multi-file envelope), the `schema_version` policy, and ready-to-use `jq` recipes
933
- 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).
934
753
 
935
754
  ## Pre-push hook
936
755
 
937
- Gate your pushes locally: run mutation testing on the file(s) you touched and
938
- block the push when the score is under your bar, the fast-feedback sibling of
939
- 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.
940
758
 
941
- The gem ships a ready-to-copy hook at
942
- [`examples/hooks/pre-push`](examples/hooks/pre-push) (installed with the gem, so
943
- you have it offline). It runs `mutation_test --json`, reads the score with
944
- [`jq`](https://jqlang.github.io/jq/), and on a below-threshold run lists the
945
- surviving mutants (file, line, what changed) so you see which gaps to close.
946
- 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:
947
763
 
948
764
  ```bash
949
765
  cp examples/hooks/pre-push .git/hooks/pre-push
950
766
  chmod +x .git/hooks/pre-push
951
767
  ```
952
768
 
953
- Then edit the `THRESHOLD` and the `SOURCE TEST` pair(s) at the top of the copied
954
- hook.
769
+ Then edit the `THRESHOLD` and the `SOURCE TEST` pair(s) at the top of the copied hook.
955
770
 
956
- Prefer to gate on your project's configured threshold rather than one written
957
- into the hook? `mutation_test` already exits non-zero when the score is below
958
- `config.minimum_score` (default 80, see [Configuration](#configuration)), so you
959
- 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:
960
774
 
961
775
  ```sh
962
776
  bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb || exit 1
963
777
  ```
964
778
 
965
- `--minimum-score N` sets that threshold for a single run, which is how you start
966
- 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:
967
781
 
968
782
  ```sh
969
783
  bundle exec mutation_test --glob 'app/**/*.rb' \
970
784
  --spec-map '\Aapp/(.+)\.rb\z=>spec/\1_spec.rb' --minimum-score 60 || exit 1
971
785
  ```
972
786
 
973
- lefthook or overcommit users: call the shipped hook from your `pre-push` step
974
- 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/`.
975
788
 
976
789
  ## CI/CD integration
977
790
 
978
- Run MutationTester as a CI quality gate: the CLI exits non-zero when the mutation
979
- 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.
980
793
 
981
- The gem ships ready-to-copy GitHub Actions workflows (installed alongside the
982
- 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):
983
795
 
984
- - [`examples/github_actions/mutation_test.yml`](examples/github_actions/mutation_test.yml)
985
- is the maintained template. Add the gem to your bundle, copy it to
986
- `.github/workflows/`, edit the `EDIT:` lines, and it runs the gate, uploads the
987
- HTML/JSON reports from `tmp/mutation_reports/` as an artifact (even on
988
- failure), and fails the job below threshold. It also carries commented variants
989
- for parallel execution, several file pairs, and an incremental pull-request gate.
990
- - [`examples/github_actions/ai_mutation_gate.yml`](examples/github_actions/ai_mutation_gate.yml)
991
- is the AI gate: the same pass/fail gate, plus it writes the surviving-mutant
992
- worklist to the GitHub job summary and uploads `survivors.json` for an agent to
993
- 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.
994
804
 
995
- See [docs/ci.md](docs/ci.md) for the full recipes: a 5-minute setup, minimal
996
- inline and pull-request workflows, machine mode as a gate and artifact, and the
997
- 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.
998
807
 
999
808
  ## Troubleshooting
1000
809
 
@@ -1010,18 +819,14 @@ This usually means:
1010
819
 
1011
820
  ### Database Conflicts in Parallel Mode
1012
821
 
1013
- If you see errors like "database is locked" or "record not found":
822
+ If you see errors like "database is locked" or "record not found", your parallel workers are sharing one test database.
1014
823
 
1015
- **Solution**: Use serial execution (the default):
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)):
1016
827
 
1017
828
  ```bash
1018
- bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
1019
- ```
1020
-
1021
- Or if using environment variable, make sure it's not set or set to 1:
1022
-
1023
- ```bash
1024
- 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
1025
830
  ```
1026
831
 
1027
832
  ### Slow Execution
@@ -1031,10 +836,11 @@ run.
1031
836
 
1032
837
  **Tips for faster execution**:
1033
838
 
1034
- - Test only critical files (don't test everything)
1035
- - Use parallel execution if your tests support it (`--parallel N`)
1036
- - 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`)
1037
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)
1038
844
 
1039
845
  ### "No Mutations Generated"
1040
846
 
@@ -1057,32 +863,27 @@ If mutation testing stops at a `debugger` or `binding.pry` statement:
1057
863
 
1058
864
  ### Parser Version Warning on Newer Ruby (Supported Syntax Level)
1059
865
 
1060
- MutationTester parses your source with the `parser` gem. The newest published
1061
- `parser` line recognizes **Ruby 3.3 syntax**; there is not yet a release that
1062
- understands Ruby 3.4+/4.x syntax. So when you run on Ruby 3.4 or newer you may
1063
- 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:
1064
869
 
1065
870
  ```
1066
871
  warning: parser/current is loading parser/ruby33, which recognizes 3.3.x-compliant syntax, but you are running 4.0.2.
1067
872
  ```
1068
873
 
1069
- This warning is **benign**. It only means the parser recognizes syntax up to
1070
- Ruby 3.3. The gem itself runs fine on Ruby 3.4+/4.x, and files written in Ruby
1071
- 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.
1072
876
 
1073
- The only real limitation is a source file that relies on syntax introduced
1074
- after Ruby 3.3. Such a file cannot be parsed, so it reports an explicit
1075
- `Failed to parse source file` and the run fails - it never fakes a passing
1076
- score. The warning is left in place on purpose as an honest signal; it is not
1077
- 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.
1078
880
 
1079
881
  ## Development
1080
882
 
1081
883
  Run `bundle install`, then run `rake spec` to run the tests.
1082
884
 
1083
- The gem includes working examples for both RSpec and Minitest under the
1084
- `examples/` directory (a sample `Calculator` class with an RSpec spec and a
1085
- 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:
1086
887
 
1087
888
  ```bash
1088
889
  # Directly with the CLI
@@ -1101,12 +902,8 @@ rake example:minitest_parallel # Parallel execution
1101
902
  rake example
1102
903
  ```
1103
904
 
1104
- Each example will:
1105
-
1106
- - Run mutation tests on a sample Calculator class
1107
- - Generate console, HTML, and JSON reports
1108
- - Show mutation score and quality metrics
1109
- - 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.
1110
907
 
1111
908
  ## Contributing
1112
909