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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +83 -0
  5. data/LICENSE.txt +19 -0
  6. data/Rakefile +57 -0
  7. data/docs/ci.md +158 -0
  8. data/docs/execution-runners.md +163 -0
  9. data/docs/json-schema.md +171 -0
  10. data/docs/mutation-types.md +207 -0
  11. data/examples/calculator.rb +35 -0
  12. data/examples/calculator_100_perc_spec.rb +121 -0
  13. data/examples/calculator_minitest.rb +53 -0
  14. data/examples/calculator_spec.rb +105 -0
  15. data/examples/github_actions/ai_mutation_gate.yml +75 -0
  16. data/examples/github_actions/mutation_test.yml +87 -0
  17. data/examples/hooks/pre-push +41 -0
  18. data/examples/run_example.rb +31 -0
  19. data/examples/run_example_minitest.rb +38 -0
  20. data/examples/run_example_parallel_minitest.rb +48 -0
  21. data/examples/run_example_parallel_rspec.rb +48 -0
  22. data/examples/run_example_rspec.rb +38 -0
  23. data/examples/spec_helper.rb +21 -0
  24. data/exe/mutation_test +345 -0
  25. data/lib/mutation_tester/batch_runner.rb +286 -0
  26. data/lib/mutation_tester/configuration.rb +105 -0
  27. data/lib/mutation_tester/core.rb +193 -0
  28. data/lib/mutation_tester/fork_runner/worker.rb +202 -0
  29. data/lib/mutation_tester/fork_runner.rb +382 -0
  30. data/lib/mutation_tester/framework_detector.rb +25 -0
  31. data/lib/mutation_tester/in_memory_loader.rb +25 -0
  32. data/lib/mutation_tester/mutation_runner.rb +644 -0
  33. data/lib/mutation_tester/mutator.rb +874 -0
  34. data/lib/mutation_tester/progress_display.rb +130 -0
  35. data/lib/mutation_tester/railtie.rb +7 -0
  36. data/lib/mutation_tester/rake_task.rb +18 -0
  37. data/lib/mutation_tester/reporters/base_reporter.rb +154 -0
  38. data/lib/mutation_tester/reporters/batch_json_reporter.rb +72 -0
  39. data/lib/mutation_tester/reporters/console_reporter.rb +130 -0
  40. data/lib/mutation_tester/reporters/html_reporter.rb +693 -0
  41. data/lib/mutation_tester/reporters/json_reporter.rb +67 -0
  42. data/lib/mutation_tester/test_command.rb +165 -0
  43. data/lib/mutation_tester/version.rb +3 -0
  44. data/lib/mutation_tester.rb +52 -0
  45. data/lib/tasks/mutation_tester.rake +80 -0
  46. data/readme.md +925 -0
  47. 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
@@ -0,0 +1,9 @@
1
+ # Gemfile
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'pry'
8
+ gem 'rubocop', require: false
9
+ end
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
+ ```