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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +1 -1
- data/docs/execution-runners.md +12 -0
- data/examples/github_actions/ai_mutation_gate.yml +1 -1
- data/examples/github_actions/mutation_test.yml +1 -1
- data/exe/mutation_test +6 -1
- data/lib/mutation_tester/configuration.rb +8 -1
- data/lib/mutation_tester/fork_runner/worker.rb +12 -0
- data/lib/mutation_tester/fork_runner.rb +15 -8
- data/lib/mutation_tester/mutation_runner.rb +38 -6
- data/lib/mutation_tester/test_command.rb +2 -1
- data/lib/mutation_tester/version.rb +1 -1
- data/readme.md +383 -541
- metadata +2 -2
data/readme.md
CHANGED
|
@@ -2,24 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Oxyconit/mutation_tester/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
Simple mutation testing framework for Ruby applications with RSpec or Minitest.
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
`Gemfile
|
|
122
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
155
|
+
# Many files in one run: select sources with a glob
|
|
186
156
|
mutation_test [OPTIONS] --glob 'lib/**/*.rb'
|
|
187
157
|
```
|
|
188
158
|
|
|
189
|
-
|
|
159
|
+
### Choosing what to test
|
|
190
160
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
#
|
|
267
|
-
bundle exec
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
196
|
+
```bash
|
|
197
|
+
bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
|
|
283
198
|
```
|
|
284
199
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
|
387
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
###
|
|
311
|
+
### Exit codes
|
|
445
312
|
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
450
|
-
bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
|
|
451
|
-
```
|
|
321
|
+
### Running with rake
|
|
452
322
|
|
|
453
|
-
|
|
323
|
+
In a **non-Rails** project, require the tasks from your `Rakefile`:
|
|
454
324
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
330
|
+
In a **Rails** app the tasks load automatically through the gem's railtie, so no Rakefile change is needed.
|
|
468
331
|
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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,
|
|
655
|
-
environment freshly and picks up the variable. The `in_memory` runner clones a single preloaded worker that
|
|
656
|
-
connected to one database, so
|
|
657
|
-
skips `in_memory` and uses `fork`, announcing the
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
- **in_memory** (default where supported): re-evaluates the mutated source in the
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
strict-equality
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[`
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
913
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
-
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
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.
|
|
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
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|