mutation_tester 1.4.2 → 1.5.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/docs/execution-runners.md +12 -0
- data/exe/mutation_test +6 -1
- data/lib/mutation_tester/configuration.rb +8 -1
- data/lib/mutation_tester/fork_runner/worker.rb +11 -0
- data/lib/mutation_tester/fork_runner.rb +12 -6
- data/lib/mutation_tester/mutation_runner.rb +38 -6
- data/lib/mutation_tester/version.rb +1 -1
- data/readme.md +50 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95884456f3cb22f463001c227181cbb9250fe09c3e94eec4039cc0fac54c6e18
|
|
4
|
+
data.tar.gz: 984f636d787af8f79d4d117684f57a69bdd48347651ecbb6421468d2730522e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 890a83603e65df88fcad21e62a27faa57c8540c403d8ca92ae218b9471d04471272f01f409f7c32cff8bf567feda042167c75b7be83962315c06976626ec142b
|
|
7
|
+
data.tar.gz: 943bb88ba38f80d37c78ecde8f91567000e952a4b68209cfac9d94aa336129b8ce2bdec2eaac4759fa328fb334daa79fd34a8a7a587d069804fcf1d4c907c943
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-08-08
|
|
4
|
+
|
|
5
|
+
- Added `--after-fork FILE` (and `config.after_fork_file` / the `MUTATION_TESTER_AFTER_FORK` environment variable): a Ruby file loaded inside each preloaded in-memory clone right after it forks and receives its per-worker environment, so the app can re-establish per-worker state such as its ActiveRecord connection. With `--worker-env` set, this keeps the `in_memory` runner available in parallel runs instead of falling back to `fork`: each clone now receives its own per-worker value of the variable through the existing per-clone environment plumbing and runs the after-fork file once, giving in-memory execution and per-worker database isolation at the same time. Failure modes stay loud: a clone whose after-fork file raises reports the error on stderr and is dropped, its worker deciding its share of mutants file-based, and a missing after-fork file makes the whole run fall back to file-based execution with a warning. A serial in-memory run (`-p 1`) has only one worker and now stays in memory with `--worker-env` even without the hook.
|
|
6
|
+
- A serial file-based run (`-p 1`) with `--worker-env` now decides each mutant in a shadow workspace instead of writing mutants into the real source file with a `.mutation_backup` alongside, so a process killed mid-run can no longer leave a mutated source file in the checkout. This combination occurs naturally when a caller uses one code path for both a parallel run and a serial re-run. Without `--worker-env` serial file-based runs keep the in-place strategy (the next run still restores a leftover backup automatically).
|
|
7
|
+
|
|
3
8
|
## [1.4.2] - 2026-08-05
|
|
4
9
|
|
|
5
10
|
- Added `--spec-map 'PATTERN=>REPLACEMENT'`, a repeatable rule that builds the whole spec path from the whole source path with a regular expression, for the layouts the `{name}` template cannot express. `{name}` is the source path minus a leading `lib/`, so a template can only wrap it with a prefix and a suffix; every mapping that substitutes *inside* the path, after a variable-length prefix, was out of reach: `packs/identity/app/models/party.rb -> packs/identity/test/models/party_test.rb` (Packwerk / packs-rails), the same shape with Rails engines, and the plain Rails `app/ -> test/` rule. Because `--since` requires `--glob` and `--glob` maps through `--spec-glob`, such a layout previously lost the entire batch tier at once and had to be reimplemented in a wrapper script. The first matching rule wins and a source matching no rule falls back to `--spec-glob`, so one run can cover `app/` through a rule and `lib/` through the template. The flag works with `--glob`, a positional `FILE` list and `--staged`; a malformed rule is a usage error (exit code `2`) reported before any mutation runs.
|
data/docs/execution-runners.md
CHANGED
|
@@ -134,6 +134,18 @@ no fallback is possible.
|
|
|
134
134
|
in a fresh fork of its own clone. If a pooled worker dies mid-run, that
|
|
135
135
|
worker finishes its share of mutants through the file-based path with a
|
|
136
136
|
warning; the other workers stay in memory.
|
|
137
|
+
- With `--worker-env` the preloaded clones would all share the primary
|
|
138
|
+
worker's already-established database connection, so a parallel run
|
|
139
|
+
(`-p N`, N > 1) skips the in-memory runner with a warning unless
|
|
140
|
+
`--after-fork FILE` is also given. With `--after-fork`, each clone
|
|
141
|
+
receives its per-worker value of the `--worker-env` variable and loads
|
|
142
|
+
`FILE` right after forking, and that file re-establishes the per-worker
|
|
143
|
+
state (typically the ActiveRecord connection), which keeps the whole run
|
|
144
|
+
in memory with per-worker database isolation. A serial run (`-p 1`) has
|
|
145
|
+
only one worker and stays in memory without any hook. A clone whose
|
|
146
|
+
after-fork file raises reports the error on stderr and is dropped, and
|
|
147
|
+
its worker falls back to the file-based path; a missing after-fork file
|
|
148
|
+
makes the whole run fall back with a warning.
|
|
137
149
|
- Before any mutant runs, the runner re-applies the **unmutated** source in a
|
|
138
150
|
probe child and runs the suite. If that probe fails (for example the file has
|
|
139
151
|
top-level side effects that break on a second execution, or the class is
|
data/exe/mutation_test
CHANGED
|
@@ -88,10 +88,14 @@ OptionParser.new do |opts|
|
|
|
88
88
|
options[:strict_equality] = true
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
opts.on('--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. Provision the databases yourself (e.g. rake parallel:prepare).
|
|
91
|
+
opts.on('--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. Provision the databases yourself (e.g. rake parallel:prepare). Without --after-fork a parallel in_memory run falls back to fork (preloaded clones share one database connection); pair it with --after-fork to keep the in_memory runner. A serial run (-p 1) decides mutants in a shadow workspace instead of mutating the checkout in place.') do |name|
|
|
92
92
|
options[:worker_env] = name
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
opts.on('--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 (e.g. a file calling ActiveRecord::Base.establish_connection). With --worker-env set, this keeps the in_memory runner available in parallel runs.') do |file|
|
|
96
|
+
options[:after_fork] = file
|
|
97
|
+
end
|
|
98
|
+
|
|
95
99
|
opts.on('-h', '--help', 'Show this help message') do
|
|
96
100
|
puts opts
|
|
97
101
|
exit
|
|
@@ -178,6 +182,7 @@ apply_configuration = lambda do
|
|
|
178
182
|
config.timeout_factor = options[:timeout_factor] if options[:timeout_factor]
|
|
179
183
|
config.timeout_policy = options[:timeout_policy] if options[:timeout_policy]
|
|
180
184
|
config.worker_env_var = options[:worker_env] if options.key?(:worker_env)
|
|
185
|
+
config.after_fork_file = options[:after_fork] if options.key?(:after_fork)
|
|
181
186
|
config.fail_fast = options.fetch(:fail_fast, false)
|
|
182
187
|
config.mutation_types[:strict_equality] = true if options[:strict_equality]
|
|
183
188
|
|
|
@@ -19,7 +19,8 @@ module MutationTester
|
|
|
19
19
|
number <= 0 ? '' : (number + 1).to_s
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
attr_reader :parallel_processes, :runner, :worker_env_var, :timeout, :timeout_factor,
|
|
22
|
+
attr_reader :parallel_processes, :runner, :worker_env_var, :after_fork_file, :timeout, :timeout_factor,
|
|
23
|
+
:timeout_policy
|
|
23
24
|
|
|
24
25
|
attr_accessor :baseline_duration,
|
|
25
26
|
:baseline_timeout,
|
|
@@ -38,6 +39,7 @@ module MutationTester
|
|
|
38
39
|
self.parallel_processes = ENV['MUTATION_TESTER_PARALLEL_PROCESSES'] || self.class.auto_parallel_processes
|
|
39
40
|
self.runner = ENV['MUTATION_TESTER_RUNNER'] || :auto
|
|
40
41
|
self.worker_env_var = ENV['MUTATION_TESTER_WORKER_ENV']
|
|
42
|
+
self.after_fork_file = ENV['MUTATION_TESTER_AFTER_FORK']
|
|
41
43
|
@timeout = DEFAULT_TIMEOUT
|
|
42
44
|
@timeout_factor = DEFAULT_TIMEOUT_FACTOR
|
|
43
45
|
@timeout_policy = :killed
|
|
@@ -136,6 +138,11 @@ module MutationTester
|
|
|
136
138
|
@worker_env_var = normalized.empty? ? nil : normalized
|
|
137
139
|
end
|
|
138
140
|
|
|
141
|
+
def after_fork_file=(value)
|
|
142
|
+
normalized = value.to_s.strip
|
|
143
|
+
@after_fork_file = normalized.empty? ? nil : File.expand_path(normalized)
|
|
144
|
+
end
|
|
145
|
+
|
|
139
146
|
def worker_env_assignment(index)
|
|
140
147
|
return nil unless @worker_env_var
|
|
141
148
|
|
|
@@ -238,6 +238,17 @@ spawn_clone = lambda do |request, out|
|
|
|
238
238
|
out.close
|
|
239
239
|
STDIN.reopen(File::NULL)
|
|
240
240
|
request['env']&.each { |name, value| ENV[name] = value }
|
|
241
|
+
if request['after_fork']
|
|
242
|
+
begin
|
|
243
|
+
load(request['after_fork'])
|
|
244
|
+
rescue ScriptError, StandardError => e
|
|
245
|
+
clone_out.puts(JSON.generate(
|
|
246
|
+
'event' => 'clone_error',
|
|
247
|
+
'message' => "the after-fork file #{request['after_fork']} raised #{e.class}: #{e.message}; this clone is unavailable"
|
|
248
|
+
))
|
|
249
|
+
raise
|
|
250
|
+
end
|
|
251
|
+
end
|
|
241
252
|
serve.call(input, clone_out)
|
|
242
253
|
end
|
|
243
254
|
Process.detach(child)
|
|
@@ -39,10 +39,10 @@ module MutationTester
|
|
|
39
39
|
refill_pool(pool_key(use_bundle_exec, framework), count, primary, env_for: env_for)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def prepare_in_memory_pool(count, primary)
|
|
42
|
+
def prepare_in_memory_pool(count, primary, env_for: nil, after_fork: nil)
|
|
43
43
|
return [] unless available? && primary&.ready?
|
|
44
44
|
|
|
45
|
-
refill_pool(IN_MEMORY_POOL_KEY, count, primary)
|
|
45
|
+
refill_pool(IN_MEMORY_POOL_KEY, count, primary, env_for: env_for, after_fork: after_fork)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def in_memory_pool_prepared?
|
|
@@ -116,13 +116,13 @@ module MutationTester
|
|
|
116
116
|
Parallel.worker_number if defined?(Parallel) && Parallel.respond_to?(:worker_number)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
def refill_pool(key, count, primary, env_for: nil)
|
|
119
|
+
def refill_pool(key, count, primary, env_for: nil, after_fork: nil)
|
|
120
120
|
entry = (pool[key] ||= { owner: Process.pid, runners: [] })
|
|
121
121
|
entry[:runners] = entry[:runners].each_with_index.map do |runner, index|
|
|
122
|
-
runner&.ready? ? runner : primary.fork_clone(env: env_for&.call(index))
|
|
122
|
+
runner&.ready? ? runner : primary.fork_clone(env: env_for&.call(index), after_fork: after_fork)
|
|
123
123
|
end
|
|
124
124
|
entry[:runners].size.upto(count - 1) do |index|
|
|
125
|
-
entry[:runners] << primary.fork_clone(env: env_for&.call(index))
|
|
125
|
+
entry[:runners] << primary.fork_clone(env: env_for&.call(index), after_fork: after_fork)
|
|
126
126
|
end
|
|
127
127
|
entry[:runners]
|
|
128
128
|
end
|
|
@@ -198,7 +198,7 @@ module MutationTester
|
|
|
198
198
|
fail_worker
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
def fork_clone(env: nil)
|
|
201
|
+
def fork_clone(env: nil, after_fork: nil)
|
|
202
202
|
return nil unless ready?
|
|
203
203
|
return nil unless File.respond_to?(:mkfifo)
|
|
204
204
|
|
|
@@ -210,6 +210,7 @@ module MutationTester
|
|
|
210
210
|
|
|
211
211
|
clone_request = { 'job' => job_path, 'events' => events_path }
|
|
212
212
|
clone_request['env'] = env if env
|
|
213
|
+
clone_request['after_fork'] = after_fork if after_fork
|
|
213
214
|
@job_writer.puts(JSON.generate('clone' => clone_request))
|
|
214
215
|
event = read_event(monotonic_time + CLONE_TIMEOUT)
|
|
215
216
|
return nil unless event.is_a?(Hash) && event['event'] == 'cloned'
|
|
@@ -264,6 +265,11 @@ module MutationTester
|
|
|
264
265
|
next
|
|
265
266
|
end
|
|
266
267
|
|
|
268
|
+
if event['event'] == 'clone_error'
|
|
269
|
+
warn("[MutationTester] #{event['message']}")
|
|
270
|
+
return false
|
|
271
|
+
end
|
|
272
|
+
|
|
267
273
|
return event['event'] == 'ready'
|
|
268
274
|
end
|
|
269
275
|
end
|
|
@@ -56,16 +56,17 @@ module MutationTester
|
|
|
56
56
|
elsif in_memory_first?
|
|
57
57
|
run_in_memory_series(mutations, &progress_callback)
|
|
58
58
|
elsif @config.parallel_processes == 1
|
|
59
|
-
|
|
59
|
+
run_file_based_series(mutations, &progress_callback)
|
|
60
60
|
else
|
|
61
61
|
run_in_shadow_parallel(mutations, &progress_callback)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def in_memory_first?
|
|
66
|
-
return false
|
|
66
|
+
return false unless %i[in_memory auto].include?(@config.runner)
|
|
67
|
+
return true unless @config.worker_env_var
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
@config.parallel_processes == 1 || !@config.after_fork_file.nil?
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def run_in_memory_series(mutations, &progress_callback)
|
|
@@ -100,7 +101,12 @@ module MutationTester
|
|
|
100
101
|
blocker = prepare_in_memory_execution
|
|
101
102
|
return fall_back_to_parallel_file_based(blocker, mutations, &progress_callback) if blocker
|
|
102
103
|
|
|
103
|
-
pool = ForkRunner.prepare_in_memory_pool(
|
|
104
|
+
pool = ForkRunner.prepare_in_memory_pool(
|
|
105
|
+
[@config.parallel_processes, mutations.size].min,
|
|
106
|
+
@in_memory_runner,
|
|
107
|
+
env_for: worker_env_for,
|
|
108
|
+
after_fork: @config.after_fork_file
|
|
109
|
+
)
|
|
104
110
|
if pool.compact.empty?
|
|
105
111
|
return fall_back_to_parallel_file_based('the preloaded worker pool could not be cloned', mutations, &progress_callback)
|
|
106
112
|
end
|
|
@@ -161,6 +167,28 @@ module MutationTester
|
|
|
161
167
|
cleanup_shadow_workspaces
|
|
162
168
|
end
|
|
163
169
|
|
|
170
|
+
def run_file_based_series(mutations, &progress_callback)
|
|
171
|
+
project_root = @config.worker_env_var ? discoverable_project_root : nil
|
|
172
|
+
return run_in_place_series(mutations, &progress_callback) unless project_root
|
|
173
|
+
|
|
174
|
+
run_in_shadow_series(mutations, project_root, &progress_callback)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def run_in_shadow_series(mutations, project_root, &progress_callback)
|
|
178
|
+
warn "[MutationTester] --worker-env #{@config.worker_env_var} is set, so the serial run decides each mutant in a shadow workspace and never mutates the checkout in place."
|
|
179
|
+
shadow_run_root
|
|
180
|
+
results = []
|
|
181
|
+
mutations.each_with_index do |mutation, index|
|
|
182
|
+
result = run_single_mutation(mutation, :shadow, project_root)
|
|
183
|
+
progress_callback.call(mutation, index + 1) if progress_callback
|
|
184
|
+
results << result
|
|
185
|
+
break if stop_early?(result)
|
|
186
|
+
end
|
|
187
|
+
results
|
|
188
|
+
ensure
|
|
189
|
+
cleanup_shadow_workspaces
|
|
190
|
+
end
|
|
191
|
+
|
|
164
192
|
def run_in_place_series(mutations, &progress_callback)
|
|
165
193
|
write_in_place_backup
|
|
166
194
|
results = []
|
|
@@ -401,6 +429,9 @@ module MutationTester
|
|
|
401
429
|
|
|
402
430
|
def prepare_in_memory_execution
|
|
403
431
|
return 'Process.fork is not supported on this platform' unless ForkRunner.available?
|
|
432
|
+
if @config.after_fork_file && !File.exist?(@config.after_fork_file)
|
|
433
|
+
return "the after-fork file #{@config.after_fork_file} does not exist"
|
|
434
|
+
end
|
|
404
435
|
if InMemoryLoader.load_time_defined_guard?(@original_content)
|
|
405
436
|
return 'the source file uses defined? at load time, so redefinition would silently skip the guarded code'
|
|
406
437
|
end
|
|
@@ -451,7 +482,7 @@ module MutationTester
|
|
|
451
482
|
offset_callback = progress_callback && lambda do |mutation, index|
|
|
452
483
|
progress_callback.call(mutation, completed + index)
|
|
453
484
|
end
|
|
454
|
-
|
|
485
|
+
run_file_based_series(mutations, &offset_callback)
|
|
455
486
|
end
|
|
456
487
|
|
|
457
488
|
def fall_back_to_parallel_file_based(reason, mutations, &progress_callback)
|
|
@@ -470,8 +501,9 @@ module MutationTester
|
|
|
470
501
|
def announce_worker_env_in_memory_opt_out
|
|
471
502
|
return unless @config.worker_env_var
|
|
472
503
|
return unless %i[in_memory auto].include?(@config.runner)
|
|
504
|
+
return if @config.parallel_processes == 1 || @config.after_fork_file
|
|
473
505
|
|
|
474
|
-
warn "[MutationTester] --worker-env #{@config.worker_env_var} is set, so the in-memory runner is skipped (its clones share one preloaded database connection); using the fork runner for per-worker database isolation."
|
|
506
|
+
warn "[MutationTester] --worker-env #{@config.worker_env_var} is set without --after-fork, so the in-memory runner is skipped (its clones share one preloaded database connection); using the fork runner for per-worker database isolation. Pass --after-fork FILE to keep the in-memory runner and re-establish per-worker connections inside each clone."
|
|
475
507
|
end
|
|
476
508
|
|
|
477
509
|
def run_mutation_load_time(mutation, result, project_root)
|
data/readme.md
CHANGED
|
@@ -210,7 +210,8 @@ mutation_test [OPTIONS] --glob 'lib/**/*.rb'
|
|
|
210
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
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
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`).
|
|
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). |
|
|
214
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). |
|
|
215
216
|
| `-h, --help` | Show help message. |
|
|
216
217
|
| `-v, --version` | Show version. |
|
|
@@ -510,6 +511,19 @@ MutationTester.configure do |config|
|
|
|
510
511
|
# setting a specific mode forces it. See the Execution model section below.
|
|
511
512
|
config.runner = :auto
|
|
512
513
|
|
|
514
|
+
# Per-worker database isolation (parallel_tests TEST_ENV_NUMBER convention).
|
|
515
|
+
# Set to an environment variable name to give each parallel worker a distinct
|
|
516
|
+
# value before it boots its test environment. Equivalent to the --worker-env
|
|
517
|
+
# CLI flag. See Making parallelism work with Rails below.
|
|
518
|
+
# config.worker_env_var = "TEST_ENV_NUMBER"
|
|
519
|
+
|
|
520
|
+
# Ruby file loaded inside each preloaded in-memory clone right after it forks
|
|
521
|
+
# and receives its per-worker environment, so the app can re-establish
|
|
522
|
+
# per-worker state such as its database connection. Keeps the in_memory
|
|
523
|
+
# runner available in parallel worker-env runs. Equivalent to the
|
|
524
|
+
# --after-fork CLI flag.
|
|
525
|
+
# config.after_fork_file = "db/mutation_after_fork.rb"
|
|
526
|
+
|
|
513
527
|
# Two-phase test selection (RSpec only): fast-kill on a matching example
|
|
514
528
|
# subset, always confirmed by the full file before a mutant is reported as
|
|
515
529
|
# survived. Set to false to always run the full file. See Execution model.
|
|
@@ -651,10 +665,41 @@ bundle exec mutation_test --glob 'app/models/**/*.rb' \
|
|
|
651
665
|
|
|
652
666
|
`MUTATION_TESTER_WORKER_ENV=TEST_ENV_NUMBER` is equivalent to passing the flag.
|
|
653
667
|
|
|
654
|
-
**Runner support.** `--worker-env` works with the `fork` and `spawn` runners,
|
|
655
|
-
environment freshly and picks up the variable. The `in_memory` runner clones a single preloaded worker that
|
|
656
|
-
connected to one database, so
|
|
657
|
-
skips `in_memory` and uses `fork`, announcing the
|
|
668
|
+
**Runner support.** `--worker-env` works with the `fork` and `spawn` runners out of the box, because each mutant boots
|
|
669
|
+
its test environment freshly and picks up the variable. The `in_memory` runner clones a single preloaded worker that
|
|
670
|
+
has already connected to one database, so setting the variable alone cannot re-point an existing connection; in a
|
|
671
|
+
parallel run without `--after-fork` the runner selection therefore skips `in_memory` and uses `fork`, announcing the
|
|
672
|
+
reason on stderr. A serial run (`-p 1`) has only one worker and stays in memory.
|
|
673
|
+
|
|
674
|
+
**Keeping the in-memory runner with `--after-fork`.** Pass `--after-fork FILE` (or
|
|
675
|
+
`MUTATION_TESTER_AFTER_FORK=FILE`) to keep the `in_memory` runner in parallel worker-env runs. Each preloaded clone
|
|
676
|
+
then receives its per-worker value of the `--worker-env` variable and loads `FILE` right after forking, and that file
|
|
677
|
+
is where your app re-establishes its per-worker state. For a Rails app with a `TEST_ENV_NUMBER`-keyed `database.yml`
|
|
678
|
+
that usually means reconnecting ActiveRecord:
|
|
679
|
+
|
|
680
|
+
```ruby
|
|
681
|
+
# db/mutation_after_fork.rb
|
|
682
|
+
ActiveRecord::Base.establish_connection(
|
|
683
|
+
ActiveRecord::Base.configurations
|
|
684
|
+
.configs_for(env_name: 'test', name: 'primary')
|
|
685
|
+
.configuration_hash
|
|
686
|
+
.merge(database: "myapp_test#{ENV['TEST_ENV_NUMBER']}")
|
|
687
|
+
)
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
```bash
|
|
691
|
+
bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb \
|
|
692
|
+
-p 4 --worker-env TEST_ENV_NUMBER --after-fork db/mutation_after_fork.rb
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
The file runs once per clone, inside the clone only (never in the primary preloaded worker or in your shell process).
|
|
696
|
+
If it raises, the clone reports the error on stderr and is dropped, and its worker decides its share of mutants through
|
|
697
|
+
the file-based path; if the file does not exist, the whole run falls back to file-based execution with a warning.
|
|
698
|
+
|
|
699
|
+
**Serial runs never mutate your checkout.** With `--worker-env` set, a `-p 1` run on the file-based runners decides
|
|
700
|
+
each mutant in a shadow workspace instead of writing mutants into the real source file, so a killed process cannot
|
|
701
|
+
leave a mutated file behind. (Without `--worker-env`, a serial file-based run still uses in-place mutation with a
|
|
702
|
+
`.mutation_backup` file that the next run restores automatically.)
|
|
658
703
|
|
|
659
704
|
**Still simplest without a parallel database setup:** if you have not provisioned per-worker databases, keep Rails
|
|
660
705
|
model runs on serial `-p 1`. `--worker-env` is only useful once the databases exist.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mutation_tester
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kamil Dzierbicki
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: parallel
|