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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +1 -1
- data/examples/github_actions/ai_mutation_gate.yml +1 -1
- data/examples/github_actions/mutation_test.yml +1 -1
- data/lib/mutation_tester/fork_runner/worker.rb +1 -0
- data/lib/mutation_tester/fork_runner.rb +3 -2
- data/lib/mutation_tester/test_command.rb +2 -1
- data/lib/mutation_tester/version.rb +1 -1
- data/readme.md +335 -538
- 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,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
|
|
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`). 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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
268
|
-
bundle exec
|
|
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
|
-
|
|
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
|
-
```
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
`--
|
|
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
|
-
|
|
295
|
-
bundle exec mutation_test lib/calculator.rb lib/parser.rb
|
|
209
|
+
#### Batch behaviour and skipped files
|
|
296
210
|
|
|
297
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
a
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
388
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
###
|
|
311
|
+
### Exit codes
|
|
446
312
|
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
451
|
-
bundle exec mutation_test --glob 'lib/**/*.rb' --since origin/main
|
|
452
|
-
```
|
|
321
|
+
### Running with rake
|
|
453
322
|
|
|
454
|
-
|
|
323
|
+
In a **non-Rails** project, require the tasks from your `Rakefile`:
|
|
455
324
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
330
|
+
In a **Rails** app the tasks load automatically through the gem's railtie, so no Rakefile change is needed.
|
|
469
331
|
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
- **in_memory** (default where supported): re-evaluates the mutated source in the
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
strict-equality
|
|
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
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[`
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
-
|
|
991
|
-
|
|
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
|
-
|
|
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**:
|
|
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
|
-
|
|
1062
|
-
|
|
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.
|
|
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
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|