mutation_tester 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +19 -0
- data/Rakefile +57 -0
- data/docs/ci.md +158 -0
- data/docs/execution-runners.md +163 -0
- data/docs/json-schema.md +171 -0
- data/docs/mutation-types.md +207 -0
- data/examples/calculator.rb +35 -0
- data/examples/calculator_100_perc_spec.rb +121 -0
- data/examples/calculator_minitest.rb +53 -0
- data/examples/calculator_spec.rb +105 -0
- data/examples/github_actions/ai_mutation_gate.yml +75 -0
- data/examples/github_actions/mutation_test.yml +87 -0
- data/examples/hooks/pre-push +41 -0
- data/examples/run_example.rb +31 -0
- data/examples/run_example_minitest.rb +38 -0
- data/examples/run_example_parallel_minitest.rb +48 -0
- data/examples/run_example_parallel_rspec.rb +48 -0
- data/examples/run_example_rspec.rb +38 -0
- data/examples/spec_helper.rb +21 -0
- data/exe/mutation_test +345 -0
- data/lib/mutation_tester/batch_runner.rb +286 -0
- data/lib/mutation_tester/configuration.rb +105 -0
- data/lib/mutation_tester/core.rb +193 -0
- data/lib/mutation_tester/fork_runner/worker.rb +202 -0
- data/lib/mutation_tester/fork_runner.rb +382 -0
- data/lib/mutation_tester/framework_detector.rb +25 -0
- data/lib/mutation_tester/in_memory_loader.rb +25 -0
- data/lib/mutation_tester/mutation_runner.rb +644 -0
- data/lib/mutation_tester/mutator.rb +874 -0
- data/lib/mutation_tester/progress_display.rb +130 -0
- data/lib/mutation_tester/railtie.rb +7 -0
- data/lib/mutation_tester/rake_task.rb +18 -0
- data/lib/mutation_tester/reporters/base_reporter.rb +154 -0
- data/lib/mutation_tester/reporters/batch_json_reporter.rb +72 -0
- data/lib/mutation_tester/reporters/console_reporter.rb +130 -0
- data/lib/mutation_tester/reporters/html_reporter.rb +693 -0
- data/lib/mutation_tester/reporters/json_reporter.rb +67 -0
- data/lib/mutation_tester/test_command.rb +165 -0
- data/lib/mutation_tester/version.rb +3 -0
- data/lib/mutation_tester.rb +52 -0
- data/lib/tasks/mutation_tester.rake +80 -0
- data/readme.md +925 -0
- metadata +213 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 20fff769b6b8b7efeb874208f5e3c25206ca92a2fc9f220bdd4fcb520eab944c
|
|
4
|
+
data.tar.gz: 3b630985b285958780d49d356cbe04f629ce50e1aca947d2f40d8bdddd274d42
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f40cf255d91681f4ae191b444ba8f99ba897ea746b2f157190d69bce091e90c4aaec9bde9c73fe69008de17916478ff40227a7e2439de687d4a576d72d664d0a
|
|
7
|
+
data.tar.gz: 9c8d46ca0d8a98dff41de40b61e8fdd60c68c55ed37303e02d08009968a0263bf932e0892af3e13af12810c27cb61f695f1d01a77e46f2dbc375735fe49b87a4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.2.0] - 2026-07-14
|
|
4
|
+
|
|
5
|
+
- Fixed the in-memory runner reporting false survivors for mutations that only take effect at class-load time (constants consumed by macros, `validates`/`has_many`/`before_save`/`scope`/`attribute`, and anything inside an `included do` block). The mutator now classifies each mutation by AST context, and the in-memory run routes load-time mutations to the file-based path while keeping method-body mutations in memory, so the default (`auto`) score matches a full `fork` run on Rails concerns and models. Measured on a real Rails concern the default score went from a misleading 1.63% to the correct 35.77%.
|
|
6
|
+
|
|
7
|
+
## [1.1.0] - 2026-07-13
|
|
8
|
+
|
|
9
|
+
- Added `--worker-env NAME` (and the `MUTATION_TESTER_WORKER_ENV` environment variable) to set a distinct per-worker value of `NAME` before each parallel worker boots, following the `parallel_tests` `TEST_ENV_NUMBER` convention (worker 0 -> "", worker N -> N+1). This lets a Rails app with a `parallel_tests`-style `database.yml` run parallel mutation testing with a per-worker database instead of being forced to serial execution. Supported by the `fork` and `spawn` runners; the `in_memory` runner cannot isolate a per-worker database and falls back to `fork` with an announced notice.
|
|
10
|
+
|
|
11
|
+
## [1.0.0] - 2026-07-12
|
|
12
|
+
|
|
13
|
+
- Released first version.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
mutation_tester (1.2.0)
|
|
5
|
+
parallel (~> 1.20)
|
|
6
|
+
parser (~> 3.3)
|
|
7
|
+
rainbow (~> 3.0)
|
|
8
|
+
unparser (>= 0.6, < 0.9)
|
|
9
|
+
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
ast (2.4.3)
|
|
14
|
+
coderay (1.1.3)
|
|
15
|
+
diff-lcs (1.6.2)
|
|
16
|
+
json (2.16.0)
|
|
17
|
+
language_server-protocol (3.17.0.5)
|
|
18
|
+
lint_roller (1.1.0)
|
|
19
|
+
method_source (1.1.0)
|
|
20
|
+
minitest (5.26.2)
|
|
21
|
+
parallel (1.27.0)
|
|
22
|
+
parser (3.3.11.1)
|
|
23
|
+
ast (~> 2.4.1)
|
|
24
|
+
racc
|
|
25
|
+
prism (1.6.0)
|
|
26
|
+
pry (0.15.2)
|
|
27
|
+
coderay (~> 1.1)
|
|
28
|
+
method_source (~> 1.0)
|
|
29
|
+
racc (1.8.1)
|
|
30
|
+
rainbow (3.1.1)
|
|
31
|
+
rake (13.3.1)
|
|
32
|
+
regexp_parser (2.11.3)
|
|
33
|
+
rspec (3.13.2)
|
|
34
|
+
rspec-core (~> 3.13.0)
|
|
35
|
+
rspec-expectations (~> 3.13.0)
|
|
36
|
+
rspec-mocks (~> 3.13.0)
|
|
37
|
+
rspec-core (3.13.6)
|
|
38
|
+
rspec-support (~> 3.13.0)
|
|
39
|
+
rspec-expectations (3.13.5)
|
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
41
|
+
rspec-support (~> 3.13.0)
|
|
42
|
+
rspec-mocks (3.13.7)
|
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
44
|
+
rspec-support (~> 3.13.0)
|
|
45
|
+
rspec-support (3.13.6)
|
|
46
|
+
rubocop (1.81.7)
|
|
47
|
+
json (~> 2.3)
|
|
48
|
+
language_server-protocol (~> 3.17.0.2)
|
|
49
|
+
lint_roller (~> 1.1.0)
|
|
50
|
+
parallel (~> 1.10)
|
|
51
|
+
parser (>= 3.3.0.2)
|
|
52
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
53
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
54
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
55
|
+
ruby-progressbar (~> 1.7)
|
|
56
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
57
|
+
rubocop-ast (1.48.0)
|
|
58
|
+
parser (>= 3.3.7.2)
|
|
59
|
+
prism (~> 1.4)
|
|
60
|
+
ruby-progressbar (1.13.0)
|
|
61
|
+
unicode-display_width (3.2.0)
|
|
62
|
+
unicode-emoji (~> 4.1)
|
|
63
|
+
unicode-emoji (4.2.0)
|
|
64
|
+
unparser (0.8.1)
|
|
65
|
+
diff-lcs (~> 1.6)
|
|
66
|
+
parser (>= 3.3.0)
|
|
67
|
+
prism (>= 1.5.1)
|
|
68
|
+
|
|
69
|
+
PLATFORMS
|
|
70
|
+
arm64-darwin-24
|
|
71
|
+
ruby
|
|
72
|
+
|
|
73
|
+
DEPENDENCIES
|
|
74
|
+
bundler (~> 2.0)
|
|
75
|
+
minitest (~> 5.0)
|
|
76
|
+
mutation_tester!
|
|
77
|
+
pry
|
|
78
|
+
rake (~> 13.0)
|
|
79
|
+
rspec (~> 3.0)
|
|
80
|
+
rubocop
|
|
81
|
+
|
|
82
|
+
BUNDLED WITH
|
|
83
|
+
2.6.9
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 - MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
require_relative 'lib/mutation_tester/rake_task'
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
task default: :spec
|
|
8
|
+
|
|
9
|
+
desc 'Run the full test suite (unit + integration RSpec + Minitest)'
|
|
10
|
+
task :test do
|
|
11
|
+
puts '--- Running RSpec Suite (unit + integration) ---'
|
|
12
|
+
sh 'bundle exec rspec'
|
|
13
|
+
|
|
14
|
+
puts "\n--- Running Minitest Suite ---"
|
|
15
|
+
sh 'ruby test/mutator_test.rb'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
namespace :test do
|
|
19
|
+
task :unit do
|
|
20
|
+
sh 'bundle exec rspec --exclude-pattern "**/integration_spec.rb"'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
task :integration do
|
|
24
|
+
sh 'bundle exec rspec spec/integration_spec.rb'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
task :minitest do
|
|
28
|
+
sh 'ruby test/mutator_test.rb'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc 'Run example mutation test (default: RSpec)'
|
|
33
|
+
task :example do
|
|
34
|
+
require_relative 'examples/run_example'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
namespace :example do
|
|
38
|
+
desc 'Run RSpec example (serial execution)'
|
|
39
|
+
task :rspec do
|
|
40
|
+
require_relative 'examples/run_example_rspec'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc 'Run Minitest example (serial execution)'
|
|
44
|
+
task :minitest do
|
|
45
|
+
require_relative 'examples/run_example_minitest'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc 'Run RSpec example (parallel execution)'
|
|
49
|
+
task :rspec_parallel do
|
|
50
|
+
require_relative 'examples/run_example_parallel_rspec'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc 'Run Minitest example (parallel execution)'
|
|
54
|
+
task :minitest_parallel do
|
|
55
|
+
require_relative 'examples/run_example_parallel_minitest'
|
|
56
|
+
end
|
|
57
|
+
end
|
data/docs/ci.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# CI/CD Integration
|
|
2
|
+
|
|
3
|
+
Ready-to-copy recipes for running MutationTester as a CI quality gate. See the
|
|
4
|
+
[CI/CD integration](../readme.md#cicd-integration) section of the README for the
|
|
5
|
+
overview and the shipped templates under
|
|
6
|
+
[`examples/github_actions/`](../examples/github_actions).
|
|
7
|
+
|
|
8
|
+
## Mutation testing in CI in 5 minutes
|
|
9
|
+
|
|
10
|
+
The gem ships a ready-to-copy GitHub Actions workflow at
|
|
11
|
+
[`examples/github_actions/mutation_test.yml`](../examples/github_actions/mutation_test.yml)
|
|
12
|
+
(installed alongside the gem, so you also have it offline). Three steps:
|
|
13
|
+
|
|
14
|
+
1. Add the gem to your bundle: `bundle add mutation_tester --group development,test`.
|
|
15
|
+
2. Copy the template to `.github/workflows/mutation_test.yml`.
|
|
16
|
+
3. Edit the lines marked `EDIT:` to point at your Ruby version and your source/test files.
|
|
17
|
+
|
|
18
|
+
The workflow checks out your code, sets up Ruby with `bundler-cache`, runs
|
|
19
|
+
`bundle exec mutation_test SOURCE TEST`, uploads the HTML/JSON reports from
|
|
20
|
+
`tmp/mutation_reports/` as a build artifact (even on failure), and fails the job
|
|
21
|
+
when the mutation score is below your threshold (the CLI exit code is the gate).
|
|
22
|
+
It also carries commented variants for parallel execution
|
|
23
|
+
(`MUTATION_TESTER_PARALLEL_PROCESSES`), for testing several file pairs, and for
|
|
24
|
+
an incremental pull-request gate (`--since`/`--fail-fast`).
|
|
25
|
+
|
|
26
|
+
## GitHub Actions (minimal inline workflow)
|
|
27
|
+
|
|
28
|
+
The same thing, condensed to a copy-pasteable minimal workflow. It matches the
|
|
29
|
+
maintained template above; reach for the template when you want the parallel and
|
|
30
|
+
multi-file variants. The quality gate needs no extra configuration: the CLI exits
|
|
31
|
+
non-zero when the mutation score is below the threshold, which fails the step (and
|
|
32
|
+
the job).
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
name: Mutation Testing
|
|
36
|
+
|
|
37
|
+
on:
|
|
38
|
+
push:
|
|
39
|
+
pull_request:
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
mutation_test:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- name: Set up Ruby
|
|
48
|
+
uses: ruby/setup-ruby@v1
|
|
49
|
+
with:
|
|
50
|
+
ruby-version: '3.3' # EDIT: match your project's Ruby (floor is 3.0)
|
|
51
|
+
bundler-cache: true
|
|
52
|
+
|
|
53
|
+
- name: Run mutation tests
|
|
54
|
+
# EDIT: point at your own source file and its test file.
|
|
55
|
+
run: bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
|
|
56
|
+
|
|
57
|
+
- name: Upload mutation reports
|
|
58
|
+
if: always() # keep the reports even when the gate failed the job
|
|
59
|
+
uses: actions/upload-artifact@v4
|
|
60
|
+
with:
|
|
61
|
+
name: mutation-reports
|
|
62
|
+
# Default output_dir; change it if you pass a custom --output-dir.
|
|
63
|
+
path: tmp/mutation_reports/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## GitHub Actions on pull requests (incremental gate)
|
|
67
|
+
|
|
68
|
+
On a pull request you rarely want to mutate the whole project. Combine
|
|
69
|
+
`--since` and `--fail-fast` to mutate only the files the PR changed and stop at
|
|
70
|
+
the first surviving mutant. The maintained template
|
|
71
|
+
[`examples/github_actions/mutation_test.yml`](../examples/github_actions/mutation_test.yml)
|
|
72
|
+
carries this variant too; the condensed version:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
name: Mutation Testing (PR)
|
|
76
|
+
|
|
77
|
+
on:
|
|
78
|
+
pull_request:
|
|
79
|
+
|
|
80
|
+
jobs:
|
|
81
|
+
mutation_test:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
with:
|
|
86
|
+
fetch-depth: 0 # full history so git can diff against the base branch
|
|
87
|
+
|
|
88
|
+
- name: Set up Ruby
|
|
89
|
+
uses: ruby/setup-ruby@v1
|
|
90
|
+
with:
|
|
91
|
+
ruby-version: '3.3' # EDIT: match your project's Ruby
|
|
92
|
+
bundler-cache: true
|
|
93
|
+
|
|
94
|
+
- name: Run mutation tests on changed files only
|
|
95
|
+
# EDIT: point the glob at your own sources.
|
|
96
|
+
run: |
|
|
97
|
+
bundle exec mutation_test --glob 'lib/**/*.rb' \
|
|
98
|
+
--since "origin/${{ github.base_ref }}" --fail-fast
|
|
99
|
+
|
|
100
|
+
- name: Upload mutation reports
|
|
101
|
+
if: always()
|
|
102
|
+
uses: actions/upload-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: mutation-reports
|
|
105
|
+
path: tmp/mutation_reports/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A PR that touches nothing under `lib/` exits `0` with a "Nothing to mutate"
|
|
109
|
+
message, so the gate never blocks unrelated changes.
|
|
110
|
+
|
|
111
|
+
## Machine mode as a CI gate and artifact
|
|
112
|
+
|
|
113
|
+
`--json` makes the CLI double as the gate (its exit code follows the threshold)
|
|
114
|
+
and the artifact producer. Capture stdout to a file, upload it, and the run
|
|
115
|
+
fails the job automatically when the score drops below the threshold:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
- name: Run mutation tests (JSON report on stdout)
|
|
119
|
+
run: |
|
|
120
|
+
bundle exec mutation_test --json \
|
|
121
|
+
app/models/user.rb spec/models/user_spec.rb > mutation_report.json
|
|
122
|
+
|
|
123
|
+
- name: Upload JSON report
|
|
124
|
+
if: always() # keep the report even when the gate failed the job
|
|
125
|
+
uses: actions/upload-artifact@v4
|
|
126
|
+
with:
|
|
127
|
+
name: mutation-report-json
|
|
128
|
+
path: mutation_report.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## AI workflow (mutation gate)
|
|
132
|
+
|
|
133
|
+
`--json` turns the report into a worklist for an AI agent (or a script): every
|
|
134
|
+
**surviving** mutant carries `file_path`, `line`, `original` and `mutated`, which
|
|
135
|
+
is a concrete, located test gap. A CI job can run the gate, publish those
|
|
136
|
+
survivors, and hand them to an agent that proposes the missing tests.
|
|
137
|
+
|
|
138
|
+
The gem ships a ready-to-copy workflow at
|
|
139
|
+
[`examples/github_actions/ai_mutation_gate.yml`](../examples/github_actions/ai_mutation_gate.yml)
|
|
140
|
+
(installed with the gem, so you have it offline). Copy it to
|
|
141
|
+
`.github/workflows/ai_mutation_gate.yml`, add the gem to your bundle, and edit
|
|
142
|
+
the `EDIT:` lines. It:
|
|
143
|
+
|
|
144
|
+
1. runs `mutation_test --json` and captures the report,
|
|
145
|
+
2. writes the score and a table of surviving mutants to the GitHub job summary
|
|
146
|
+
(`$GITHUB_STEP_SUMMARY`),
|
|
147
|
+
3. extracts the survivors into a machine-readable `survivors.json` and uploads it
|
|
148
|
+
(with the full report) as an artifact even on a failing run,
|
|
149
|
+
4. re-fails the job when the score is below the threshold, so it stays a gate.
|
|
150
|
+
|
|
151
|
+
The survivor extraction is the same `jq` filter documented under
|
|
152
|
+
[Extracting the score and the survived mutants](json-schema.md#extracting-the-score-and-the-survived-mutants),
|
|
153
|
+
written to `survivors.json`. That file is what you feed the agent ("here are
|
|
154
|
+
located test gaps; for each, write a focused test that would kill the mutant");
|
|
155
|
+
the template carries a commented step showing where to wire your agent CLI. Use
|
|
156
|
+
it alongside the plain [5-minute CI template](#mutation-testing-in-ci-in-5-minutes):
|
|
157
|
+
the AI gate adds the surviving-mutant worklist, the plain template is just the
|
|
158
|
+
pass/fail gate.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Execution Runners and Test Selection
|
|
2
|
+
|
|
3
|
+
Deep reference for how MutationTester executes each mutant and how two-phase test
|
|
4
|
+
selection speeds up kills. See the [Execution model](../readme.md#execution-model)
|
|
5
|
+
section of the README for the overview, the parallel/serial guidance, and how to
|
|
6
|
+
force a runner.
|
|
7
|
+
|
|
8
|
+
## Execution runners (fork, spawn, in-memory)
|
|
9
|
+
|
|
10
|
+
Every mutant is executed by one of three runners:
|
|
11
|
+
|
|
12
|
+
- **fork**: a helper process preloads the environment
|
|
13
|
+
once (RubyGems, Bundler and `rspec-core`, without loading the mutated file or
|
|
14
|
+
the specs), and each mutant runs in a fresh fork of that process. The fork
|
|
15
|
+
loads the spec only after the mutated source has been written, so every
|
|
16
|
+
mutant is visible and no state leaks between mutants. This removes most of
|
|
17
|
+
the fixed per-mutant boot cost, which matters on large suites and in CI.
|
|
18
|
+
- **spawn**: each mutant starts a full new process (`bundle exec rspec ...`).
|
|
19
|
+
Slower per mutant, but works everywhere.
|
|
20
|
+
- **in_memory** (default where supported): the helper process additionally preloads the spec
|
|
21
|
+
file and, through it, the original source, once per run. Each mutant then
|
|
22
|
+
runs in a fresh fork that re-evaluates the mutated source in memory
|
|
23
|
+
(redefining the loaded methods and class constants, with the
|
|
24
|
+
"already initialized constant" warning silenced only inside that child) and
|
|
25
|
+
runs the already-loaded examples. Nothing is written to disk on the mutation
|
|
26
|
+
hot path: the source file and the project directory stay untouched for the
|
|
27
|
+
whole run and no shadow workspaces are created. With `-p N` the preloaded
|
|
28
|
+
process is forked into a pool of clones and each parallel worker runs its
|
|
29
|
+
mutants in memory against its own clone. This brings the cost per mutation
|
|
30
|
+
below the fork runner and close to in-process mutation tools, at the price
|
|
31
|
+
of requiring loadable, re-evaluable code (see the limitations below).
|
|
32
|
+
|
|
33
|
+
Selection is automatic (`auto`): the fastest safe path is tried first and every
|
|
34
|
+
step down to a slower one prints a single stderr warning with its reason, so a
|
|
35
|
+
fallback is never silent. The order is `in_memory` (RSpec with `Process.fork`
|
|
36
|
+
available and a passing unmutated-source probe), then `fork`, then `spawn`.
|
|
37
|
+
All runners produce identical scores and per-mutant statuses, and all enforce
|
|
38
|
+
the same hard per-mutant timeout (monotonic deadline plus a process-group
|
|
39
|
+
kill).
|
|
40
|
+
|
|
41
|
+
| Mode | Picked by `auto` when | Falls back to |
|
|
42
|
+
|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
43
|
+
| `in_memory` | the suite is RSpec, the platform has `Process.fork`, the file has no load-time `defined?` guard, and re-applying the unmutated source in a probe child passes the suite | `fork`/`spawn` (whole run) with a stderr warning naming the reason; a single worker dying mid-run falls back only for its share of mutants; a mutant that raises while being applied falls back alone |
|
|
44
|
+
| `fork` | the suite is RSpec, `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 |
|
|
45
|
+
| `spawn` | the suite is Minitest, or the platform has no `Process.fork` | nothing; it works everywhere |
|
|
46
|
+
|
|
47
|
+
Forcing a mode with `--runner fork|spawn|in_memory` skips the auto attempts and
|
|
48
|
+
uses that mode directly (`in_memory` keeps its own documented safety fallbacks;
|
|
49
|
+
`fork` still degrades to `spawn` where `Process.fork` does not exist).
|
|
50
|
+
|
|
51
|
+
### When to force --runner fork or spawn
|
|
52
|
+
|
|
53
|
+
- **`--runner fork`**: your source file is not cleanly re-evaluable in memory
|
|
54
|
+
(heavy load-time side effects, code that must never be redefined) and you do
|
|
55
|
+
not want to rely on the automatic probe, but you still want the preloaded
|
|
56
|
+
environment speed.
|
|
57
|
+
- **`--runner spawn`**: you want maximum isolation (one full pristine process
|
|
58
|
+
per mutant), you are debugging a suspicious result from a preloaded runner,
|
|
59
|
+
or your environment misbehaves with forked workers (for example C extensions
|
|
60
|
+
that do not survive `fork`).
|
|
61
|
+
- Stay on the default `auto` everywhere else: it always announces on stderr
|
|
62
|
+
which path it took, so CI logs show exactly how the mutants were executed.
|
|
63
|
+
|
|
64
|
+
Override the automatic choice with any of:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# CLI flag
|
|
68
|
+
mutation_test --runner spawn lib/calculator.rb spec/calculator_spec.rb
|
|
69
|
+
|
|
70
|
+
# Environment variable
|
|
71
|
+
MUTATION_TESTER_RUNNER=spawn mutation_test lib/calculator.rb spec/calculator_spec.rb
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
MutationTester.configure do |config|
|
|
76
|
+
config.runner = :spawn
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Limitations of the fork runner
|
|
81
|
+
|
|
82
|
+
- Minitest suites always use `spawn` (fork support for Minitest is a separate
|
|
83
|
+
decision after RSpec experience is collected).
|
|
84
|
+
- Platforms without `Process.fork` (for example Windows or JRuby) always use
|
|
85
|
+
`spawn`, even when `--runner fork` is requested.
|
|
86
|
+
- If the helper process fails to preload the environment, the run warns once
|
|
87
|
+
and falls back to `spawn`.
|
|
88
|
+
|
|
89
|
+
### Limitations of the in-memory runner
|
|
90
|
+
|
|
91
|
+
The in-memory runner never fails silently: each case below falls back to
|
|
92
|
+
file-based execution with a warning, and a mutant is marked `error` only when
|
|
93
|
+
no fallback is possible.
|
|
94
|
+
|
|
95
|
+
- RSpec only, and the file must be classic loadable code (classes/modules).
|
|
96
|
+
Minitest suites fall back to the file-based path with a warning.
|
|
97
|
+
- With `-p N` (N > 1) the run stays fully in memory: the environment, the
|
|
98
|
+
original source and the specs are preloaded once, the preloaded process is
|
|
99
|
+
forked into N pooled clones, and every parallel worker applies each mutant
|
|
100
|
+
in a fresh fork of its own clone. If a pooled worker dies mid-run, that
|
|
101
|
+
worker finishes its share of mutants through the file-based path with a
|
|
102
|
+
warning; the other workers stay in memory.
|
|
103
|
+
- Before any mutant runs, the runner re-applies the **unmutated** source in a
|
|
104
|
+
probe child and runs the suite. If that probe fails (for example the file has
|
|
105
|
+
top-level side effects that break on a second execution, or the class is
|
|
106
|
+
frozen so it cannot be reopened), the whole run falls back to file-based
|
|
107
|
+
execution with a warning instead of reporting false kills. In a parallel run
|
|
108
|
+
this fallback lands on the regular parallel file-based path, after the same
|
|
109
|
+
shadow-workspace sanity check that path always performs.
|
|
110
|
+
- Files that use `defined?` at load time (for example
|
|
111
|
+
`X = 1 unless defined?(X)`) are rejected up front with a fallback warning:
|
|
112
|
+
the guard would silently skip the redefinition and mutants could falsely
|
|
113
|
+
survive. `defined?` inside method bodies is fine.
|
|
114
|
+
- A mutant that raises while being applied in memory (for example a top-level
|
|
115
|
+
`raise`, a mutation that makes the file unloadable, or a `FrozenError` on
|
|
116
|
+
redefinition) falls back to file-based execution for that single mutant with
|
|
117
|
+
a warning: the mutant is decided from disk in a shadow workspace and that
|
|
118
|
+
result is final. An objectively unloadable mutant therefore counts as
|
|
119
|
+
`killed`, exactly as under the fork and spawn runners, while a failure
|
|
120
|
+
specific to the in-memory mechanics still gets an honest file-based verdict
|
|
121
|
+
instead of a false kill. Such a mutant is reported as `error` only when the
|
|
122
|
+
fallback is impossible because no project root (a `Gemfile` or a `.git`
|
|
123
|
+
directory) is discoverable above the source file.
|
|
124
|
+
- `require_relative` in the mutated file is safe: it is idempotent on the
|
|
125
|
+
second evaluation because the file is already in `$LOADED_FEATURES`.
|
|
126
|
+
- Two-phase test selection does not apply in this mode (serial or parallel);
|
|
127
|
+
every mutant runs the full preloaded example set (the examples are already
|
|
128
|
+
in memory, so the per-mutant cost stays low). Because no selection happens,
|
|
129
|
+
the `Selection:` summary line is omitted for in-memory runs; it reappears
|
|
130
|
+
only when a fallback actually executed mutants through the file-based path.
|
|
131
|
+
|
|
132
|
+
## Test selection (fast kill with full-file confirmation)
|
|
133
|
+
|
|
134
|
+
For RSpec suites, MutationTester runs each mutant in two phases instead of
|
|
135
|
+
always paying for the whole spec file:
|
|
136
|
+
|
|
137
|
+
1. **Fast kill (subset)**: for a mutant inside method `foo`, it first runs only
|
|
138
|
+
the examples whose group description matches the method
|
|
139
|
+
(`rspec spec_file -e '#foo' -e '.foo'`, following the common
|
|
140
|
+
`describe '#foo'` / `describe '.foo'` convention). If any of these examples
|
|
141
|
+
fail or time out, the mutant is finished right there: killed (or timeout)
|
|
142
|
+
without ever running the rest of the file.
|
|
143
|
+
2. **Full-file confirmation**: if the subset passes, the full spec file is run
|
|
144
|
+
and only that result decides the status. A mutant can never be reported as
|
|
145
|
+
survived from the subset alone, so selection cannot introduce false
|
|
146
|
+
survivors.
|
|
147
|
+
|
|
148
|
+
The heuristic degrades safely: when the mutant is not inside a method, when the
|
|
149
|
+
spec file contains no `'#foo'` / `".foo"` group description, or when the suite
|
|
150
|
+
is Minitest, the full file runs directly. Scores and per-mutant statuses are
|
|
151
|
+
identical with and without selection on both file-based execution runners (fork
|
|
152
|
+
and spawn); selection only changes how fast killed mutants die. The in-memory
|
|
153
|
+
runner (the default path) performs no selection at all: every mutant runs the
|
|
154
|
+
full preloaded example set, which is why its report omits the `Selection:`
|
|
155
|
+
summary line (see the in-memory limitations above).
|
|
156
|
+
|
|
157
|
+
Disable it with the `--no-test-selection` CLI flag or in Ruby:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
MutationTester.configure do |config|
|
|
161
|
+
config.test_selection = false
|
|
162
|
+
end
|
|
163
|
+
```
|