henitai 0.1.2 → 0.1.3
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 +82 -1
- data/README.md +3 -1
- data/assets/schema/henitai.schema.json +0 -4
- data/lib/henitai/arid_node_filter.rb +3 -0
- data/lib/henitai/available_cpu_count.rb +79 -0
- data/lib/henitai/cli.rb +7 -4
- data/lib/henitai/configuration.rb +3 -5
- data/lib/henitai/configuration_validator.rb +1 -11
- data/lib/henitai/coverage_bootstrapper.rb +112 -7
- data/lib/henitai/coverage_report_reader.rb +67 -0
- data/lib/henitai/eager_load.rb +11 -0
- data/lib/henitai/equivalence_detector.rb +60 -1
- data/lib/henitai/execution_engine.rb +34 -22
- data/lib/henitai/integration/rspec_process_runner.rb +58 -0
- data/lib/henitai/integration.rb +192 -90
- data/lib/henitai/mutant.rb +3 -1
- data/lib/henitai/mutant_generator.rb +25 -48
- data/lib/henitai/operator.rb +6 -1
- data/lib/henitai/operators/assignment_expression.rb +7 -23
- data/lib/henitai/operators/conditional_expression.rb +1 -7
- data/lib/henitai/operators/method_chain_unwrap.rb +41 -0
- data/lib/henitai/operators/regex_mutator.rb +89 -0
- data/lib/henitai/operators/unary_operator.rb +36 -0
- data/lib/henitai/operators/update_operator.rb +70 -0
- data/lib/henitai/operators.rb +4 -0
- data/lib/henitai/parallel_execution_runner.rb +135 -0
- data/lib/henitai/per_test_coverage_selector.rb +60 -0
- data/lib/henitai/result.rb +16 -4
- data/lib/henitai/runner.rb +75 -11
- data/lib/henitai/source_parser.rb +12 -1
- data/lib/henitai/static_filter.rb +20 -41
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +3 -0
- data/sig/henitai.rbs +59 -10
- metadata +16 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "parallel_execution_runner"
|
|
4
4
|
|
|
5
5
|
module Henitai
|
|
6
6
|
# Runs pending mutants through the selected integration.
|
|
@@ -12,7 +12,7 @@ module Henitai
|
|
|
12
12
|
pending_mutants = Array(mutants).select(&:pending?)
|
|
13
13
|
mutex = Mutex.new
|
|
14
14
|
if parallel_execution?(config, pending_mutants)
|
|
15
|
-
run_parallel(pending_mutants, integration, config, progress_reporter
|
|
15
|
+
run_parallel(pending_mutants, integration, config, progress_reporter)
|
|
16
16
|
else
|
|
17
17
|
run_linear(pending_mutants, integration, config, progress_reporter, mutex)
|
|
18
18
|
end
|
|
@@ -31,7 +31,12 @@ module Henitai
|
|
|
31
31
|
|
|
32
32
|
def worker_count(config)
|
|
33
33
|
configured_jobs = config.respond_to?(:jobs) ? config.jobs : nil
|
|
34
|
-
configured_jobs
|
|
34
|
+
return configured_jobs if configured_jobs
|
|
35
|
+
|
|
36
|
+
# The fallback stays conservative for now; the execution policy still
|
|
37
|
+
# defaults to a single worker even though AvailableCpuCount exists as a
|
|
38
|
+
# future policy hook.
|
|
39
|
+
1
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def run_linear(mutants, integration, config, progress_reporter, mutex)
|
|
@@ -40,24 +45,29 @@ module Henitai
|
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
def run_parallel(mutants, integration, config, progress_reporter
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
def run_parallel(mutants, integration, config, progress_reporter)
|
|
49
|
+
ParallelExecutionRunner.new(
|
|
50
|
+
worker_count: worker_count(config)
|
|
51
|
+
).run(
|
|
52
|
+
mutants,
|
|
53
|
+
integration,
|
|
54
|
+
config,
|
|
55
|
+
progress_reporter,
|
|
56
|
+
stdin_pipe: pipe_stdin?,
|
|
57
|
+
process_mutant: method(:process_mutant)
|
|
58
|
+
)
|
|
59
|
+
end
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
process_mutant(mutant, integration, config, progress_reporter, mutex)
|
|
52
|
-
rescue ThreadError
|
|
53
|
-
break
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end.each(&:join)
|
|
61
|
+
def pipe_stdin?
|
|
62
|
+
$stdin.stat.pipe?
|
|
63
|
+
rescue Errno::EBADF
|
|
64
|
+
false
|
|
57
65
|
end
|
|
58
66
|
|
|
59
67
|
def process_mutant(mutant, integration, config, progress_reporter, mutex)
|
|
60
68
|
test_files = prioritized_tests_for(mutant, integration, config)
|
|
69
|
+
mutant.covered_by = test_files if mutant.respond_to?(:covered_by=)
|
|
70
|
+
mutant.tests_completed = test_files.size if mutant.respond_to?(:tests_completed=)
|
|
61
71
|
scenario_result = run_with_flaky_retry(mutant, integration, config, test_files, mutex)
|
|
62
72
|
mutant.status = scenario_status(scenario_result)
|
|
63
73
|
|
|
@@ -69,16 +79,18 @@ module Henitai
|
|
|
69
79
|
end
|
|
70
80
|
|
|
71
81
|
def prioritized_tests_for(mutant, integration, config)
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
tests = integration.select_tests(mutant.subject)
|
|
83
|
+
tests = per_test_coverage_selector.filter(
|
|
84
|
+
tests,
|
|
74
85
|
mutant,
|
|
75
|
-
|
|
86
|
+
reports_dir: config.reports_dir
|
|
76
87
|
)
|
|
88
|
+
test_prioritizer.sort(tests, mutant, test_history(config))
|
|
77
89
|
end
|
|
78
90
|
|
|
79
|
-
def test_prioritizer
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
def test_prioritizer = @test_prioritizer ||= TestPrioritizer.new
|
|
92
|
+
|
|
93
|
+
def per_test_coverage_selector = @per_test_coverage_selector ||= PerTestCoverageSelector.new
|
|
82
94
|
|
|
83
95
|
def test_history(config)
|
|
84
96
|
return {} unless config.respond_to?(:history)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
module Integration
|
|
5
|
+
# Runs RSpec child and suite processes on behalf of the integration.
|
|
6
|
+
class RspecProcessRunner
|
|
7
|
+
def run_mutant(integration, mutant:, test_files:, timeout:)
|
|
8
|
+
log_paths = integration.scenario_log_paths(mutant_log_name(mutant))
|
|
9
|
+
pid = fork_mutant_process(integration, mutant, test_files, log_paths)
|
|
10
|
+
wait_result = integration.wait_with_timeout(pid, timeout)
|
|
11
|
+
integration.build_result(wait_result, log_paths)
|
|
12
|
+
ensure
|
|
13
|
+
finalize_mutant_run(integration, pid, wait_result)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run_suite(integration, test_files, timeout:)
|
|
17
|
+
log_paths = integration.scenario_log_paths("baseline")
|
|
18
|
+
wait_result = nil
|
|
19
|
+
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
20
|
+
pid = integration.spawn_suite_process(test_files, log_paths)
|
|
21
|
+
wait_result = integration.wait_with_timeout(pid, timeout)
|
|
22
|
+
integration.build_result(wait_result, log_paths)
|
|
23
|
+
ensure
|
|
24
|
+
if pid
|
|
25
|
+
integration.cleanup_process_group(pid) unless wait_result == :timeout
|
|
26
|
+
integration.reap_child(pid) if wait_result.nil?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def fork_mutant_process(integration, mutant, test_files, log_paths)
|
|
33
|
+
Process.fork do
|
|
34
|
+
Process.setpgid(0, 0)
|
|
35
|
+
ENV["HENITAI_MUTANT_ID"] = mutant.id
|
|
36
|
+
Process.exit(
|
|
37
|
+
integration.run_in_child(
|
|
38
|
+
mutant: mutant,
|
|
39
|
+
test_files: test_files,
|
|
40
|
+
log_paths: log_paths
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def finalize_mutant_run(integration, pid, wait_result)
|
|
47
|
+
return unless pid
|
|
48
|
+
|
|
49
|
+
integration.cleanup_process_group(pid) unless wait_result == :timeout
|
|
50
|
+
integration.reap_child(pid) if wait_result.nil?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def mutant_log_name(mutant)
|
|
54
|
+
"mutant-#{mutant.id}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/henitai/integration.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "minitest"
|
|
5
|
+
require_relative "integration/rspec_process_runner"
|
|
5
6
|
|
|
6
7
|
module Henitai
|
|
7
8
|
# Namespace for test-framework integrations.
|
|
@@ -148,11 +149,99 @@ module Henitai
|
|
|
148
149
|
raise NotImplementedError
|
|
149
150
|
end
|
|
150
151
|
|
|
152
|
+
def per_test_coverage_supported?
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def wait_with_timeout(pid, timeout)
|
|
157
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
158
|
+
|
|
159
|
+
loop do
|
|
160
|
+
wait_result = Process.wait(pid, Process::WNOHANG)
|
|
161
|
+
return Process.last_status if wait_result
|
|
162
|
+
|
|
163
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
164
|
+
final_wait_result = Process.wait(pid, Process::WNOHANG)
|
|
165
|
+
return Process.last_status if final_wait_result
|
|
166
|
+
|
|
167
|
+
return handle_timeout(pid)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
pause(0.01)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def reap_child(pid)
|
|
175
|
+
Process.wait(pid)
|
|
176
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def cleanup_process_group(pid)
|
|
181
|
+
Process.kill(:SIGTERM, -pid)
|
|
182
|
+
pause(2.0)
|
|
183
|
+
Process.kill(:SIGKILL, -pid)
|
|
184
|
+
rescue Errno::EPERM
|
|
185
|
+
cleanup_child_process(pid)
|
|
186
|
+
rescue Errno::ESRCH
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
151
190
|
private
|
|
152
191
|
|
|
153
192
|
def pause(seconds)
|
|
154
193
|
sleep(seconds)
|
|
155
194
|
end
|
|
195
|
+
|
|
196
|
+
def handle_timeout(pid)
|
|
197
|
+
begin
|
|
198
|
+
cleanup_process_group(pid)
|
|
199
|
+
ensure
|
|
200
|
+
reap_child(pid)
|
|
201
|
+
end
|
|
202
|
+
:timeout
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def cleanup_child_process(pid)
|
|
206
|
+
Process.kill(:SIGTERM, pid)
|
|
207
|
+
pause(2.0)
|
|
208
|
+
Process.kill(:SIGKILL, pid)
|
|
209
|
+
rescue Errno::EPERM, Errno::ESRCH
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def rspec_options
|
|
214
|
+
[]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def subprocess_env
|
|
218
|
+
{ "PARALLEL_WORKERS" => "1" }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def scenario_log_support
|
|
222
|
+
@scenario_log_support ||= ScenarioLogSupport.new
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def with_subprocess_env
|
|
226
|
+
original_env = {} # : Hash[String, String?]
|
|
227
|
+
subprocess_env.each do |key, value|
|
|
228
|
+
original_env[key] = ENV.fetch(key, nil)
|
|
229
|
+
ENV[key] = value
|
|
230
|
+
end
|
|
231
|
+
yield
|
|
232
|
+
ensure
|
|
233
|
+
restore_subprocess_env(original_env)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def restore_subprocess_env(original_env)
|
|
237
|
+
original_env.each do |key, value|
|
|
238
|
+
if value.nil?
|
|
239
|
+
ENV.delete(key)
|
|
240
|
+
else
|
|
241
|
+
ENV[key] = value
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
156
245
|
end
|
|
157
246
|
|
|
158
247
|
# RSpec integration adapter.
|
|
@@ -185,65 +274,25 @@ module Henitai
|
|
|
185
274
|
end
|
|
186
275
|
|
|
187
276
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
188
|
-
|
|
189
|
-
pid = Process.fork do
|
|
190
|
-
ENV["HENITAI_MUTANT_ID"] = mutant.id
|
|
191
|
-
Process.exit(run_in_child(mutant:, test_files:, log_paths:))
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
build_result(wait_with_timeout(pid, timeout), log_paths)
|
|
277
|
+
RspecProcessRunner.new.run_mutant(self, mutant:, test_files:, timeout:)
|
|
195
278
|
end
|
|
196
279
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
200
|
-
pid = File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
201
|
-
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
202
|
-
Process.spawn(*suite_command(test_files), out: stdout_file, err: stderr_file)
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
build_result(wait_with_timeout(pid, timeout), log_paths)
|
|
280
|
+
def per_test_coverage_supported?
|
|
281
|
+
true
|
|
206
282
|
end
|
|
207
283
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def run_in_child(mutant:, test_files:, log_paths:)
|
|
211
|
-
Thread.report_on_exception = false
|
|
212
|
-
scenario_log_support.with_coverage_dir(mutant.id) do
|
|
213
|
-
scenario_log_support.capture_child_output(log_paths) do
|
|
214
|
-
return 2 if Mutant::Activator.activate!(mutant) == :compile_error
|
|
215
|
-
|
|
216
|
-
run_tests(test_files)
|
|
217
|
-
end
|
|
218
|
-
end
|
|
284
|
+
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
285
|
+
RspecProcessRunner.new.run_suite(self, test_files, timeout:)
|
|
219
286
|
end
|
|
220
287
|
|
|
221
288
|
def suite_command(test_files)
|
|
222
|
-
[
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return Process.last_status if Process.wait(pid, Process::WNOHANG)
|
|
230
|
-
return handle_timeout(pid) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
231
|
-
|
|
232
|
-
pause(0.01)
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def handle_timeout(pid)
|
|
237
|
-
begin
|
|
238
|
-
Process.kill(:SIGTERM, pid)
|
|
239
|
-
pause(2.0)
|
|
240
|
-
Process.kill(:SIGKILL, pid)
|
|
241
|
-
rescue Errno::ESRCH
|
|
242
|
-
# The child may exit after SIGTERM but before SIGKILL.
|
|
243
|
-
ensure
|
|
244
|
-
reap_child(pid)
|
|
245
|
-
end
|
|
246
|
-
:timeout
|
|
289
|
+
[
|
|
290
|
+
"bundle", "exec", "ruby",
|
|
291
|
+
"-r", "henitai/rspec_coverage_formatter",
|
|
292
|
+
"-S", "rspec", *test_files,
|
|
293
|
+
"--format", "progress",
|
|
294
|
+
"--format", "Henitai::CoverageFormatter"
|
|
295
|
+
]
|
|
247
296
|
end
|
|
248
297
|
|
|
249
298
|
def run_tests(test_files)
|
|
@@ -254,32 +303,6 @@ module Henitai
|
|
|
254
303
|
status == true ? 0 : 1
|
|
255
304
|
end
|
|
256
305
|
|
|
257
|
-
def rspec_options
|
|
258
|
-
["--require", "henitai/rspec_coverage_formatter"]
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def scenario_log_support
|
|
262
|
-
@scenario_log_support ||= ScenarioLogSupport.new
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def read_log_file(path)
|
|
266
|
-
return "" unless File.exist?(path)
|
|
267
|
-
|
|
268
|
-
File.read(path)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def write_combined_log(path, stdout, stderr)
|
|
272
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
273
|
-
File.write(path, combined_log(stdout, stderr))
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def combined_log(stdout, stderr)
|
|
277
|
-
[
|
|
278
|
-
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
279
|
-
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
280
|
-
].compact.join("\n")
|
|
281
|
-
end
|
|
282
|
-
|
|
283
306
|
def scenario_log_paths(name)
|
|
284
307
|
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
285
308
|
log_dir = File.join(reports_dir, "mutation-logs")
|
|
@@ -398,10 +421,70 @@ module Henitai
|
|
|
398
421
|
].uniq
|
|
399
422
|
end
|
|
400
423
|
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
424
|
+
def spawn_suite_process(test_files, log_paths)
|
|
425
|
+
File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
426
|
+
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
427
|
+
Process.spawn(
|
|
428
|
+
subprocess_env,
|
|
429
|
+
*suite_command(test_files),
|
|
430
|
+
out: stdout_file,
|
|
431
|
+
err: stderr_file,
|
|
432
|
+
pgroup: true
|
|
433
|
+
)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def run_in_child(mutant:, test_files:, log_paths:)
|
|
439
|
+
Thread.report_on_exception = false
|
|
440
|
+
with_subprocess_env do
|
|
441
|
+
scenario_log_support.with_coverage_dir(mutant.id) do
|
|
442
|
+
scenario_log_support.capture_child_output(log_paths) do
|
|
443
|
+
return 2 if Mutant::Activator.activate!(mutant) == :compile_error
|
|
444
|
+
|
|
445
|
+
run_tests(test_files)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def read_log_file(path)
|
|
452
|
+
return "" unless File.exist?(path)
|
|
453
|
+
|
|
454
|
+
File.read(path)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def write_combined_log(path, stdout, stderr)
|
|
458
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
459
|
+
File.write(path, combined_log(stdout, stderr))
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def combined_log(stdout, stderr)
|
|
463
|
+
[
|
|
464
|
+
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
465
|
+
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
466
|
+
].compact.join("\n")
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Stores the child-process log helpers shared by the integration specs.
|
|
471
|
+
class ScenarioLogSupport
|
|
472
|
+
def read_log_file(path)
|
|
473
|
+
return "" unless File.exist?(path)
|
|
474
|
+
|
|
475
|
+
File.read(path)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def write_combined_log(path, stdout, stderr)
|
|
479
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
480
|
+
File.write(path, combined_log(stdout, stderr))
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def combined_log(stdout, stderr)
|
|
484
|
+
[
|
|
485
|
+
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
486
|
+
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
487
|
+
].compact.join("\n")
|
|
405
488
|
end
|
|
406
489
|
end
|
|
407
490
|
|
|
@@ -424,13 +507,11 @@ module Henitai
|
|
|
424
507
|
|
|
425
508
|
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
426
509
|
log_paths = scenario_log_paths("baseline")
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
end
|
|
433
|
-
build_result(wait_with_timeout(pid, timeout), log_paths)
|
|
510
|
+
pid = spawn_suite_process(test_files, log_paths)
|
|
511
|
+
wait_result = wait_with_timeout(pid, timeout)
|
|
512
|
+
build_result(wait_result, log_paths)
|
|
513
|
+
ensure
|
|
514
|
+
cleanup_suite_process(pid, wait_result)
|
|
434
515
|
end
|
|
435
516
|
|
|
436
517
|
private
|
|
@@ -463,12 +544,33 @@ module Henitai
|
|
|
463
544
|
end
|
|
464
545
|
|
|
465
546
|
def subprocess_env
|
|
466
|
-
env =
|
|
547
|
+
env = super
|
|
467
548
|
env["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
468
549
|
env["PARALLEL_WORKERS"] = "1"
|
|
469
550
|
env
|
|
470
551
|
end
|
|
471
552
|
|
|
553
|
+
def spawn_suite_process(test_files, log_paths)
|
|
554
|
+
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
555
|
+
File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
556
|
+
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
557
|
+
Process.spawn(
|
|
558
|
+
subprocess_env,
|
|
559
|
+
*suite_command(test_files),
|
|
560
|
+
out: stdout_file,
|
|
561
|
+
err: stderr_file
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def cleanup_suite_process(pid, wait_result)
|
|
568
|
+
return unless pid
|
|
569
|
+
|
|
570
|
+
cleanup_child_process(pid)
|
|
571
|
+
reap_child(pid) if wait_result.nil?
|
|
572
|
+
end
|
|
573
|
+
|
|
472
574
|
def spec_files
|
|
473
575
|
(Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb"))
|
|
474
576
|
.reject { |f| f.start_with?("test/system/") }
|
data/lib/henitai/mutant.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Henitai
|
|
|
35
35
|
|
|
36
36
|
attr_reader :id, :subject, :operator, :original_node, :mutated_node,
|
|
37
37
|
:mutation_type, :description, :location
|
|
38
|
-
attr_accessor :status, :killing_test, :duration
|
|
38
|
+
attr_accessor :status, :killing_test, :duration, :covered_by, :tests_completed
|
|
39
39
|
|
|
40
40
|
# @param subject [Subject] the subject being mutated
|
|
41
41
|
# @param operator [Symbol] operator name, e.g. :ArithmeticOperator
|
|
@@ -53,6 +53,8 @@ module Henitai
|
|
|
53
53
|
@status = :pending
|
|
54
54
|
@killing_test = nil
|
|
55
55
|
@duration = nil
|
|
56
|
+
@covered_by = nil
|
|
57
|
+
@tests_completed = nil
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def killed? = @status == :killed
|
|
@@ -34,7 +34,8 @@ module Henitai
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def generate_for_subject(subject, operators, config:, arid_node_filter:, syntax_validator:)
|
|
37
|
-
|
|
37
|
+
source_node = source_node_for(subject)
|
|
38
|
+
return [] unless source_node
|
|
38
39
|
|
|
39
40
|
visitor = SubjectVisitor.new(
|
|
40
41
|
subject,
|
|
@@ -43,11 +44,8 @@ module Henitai
|
|
|
43
44
|
arid_node_filter:,
|
|
44
45
|
syntax_validator:
|
|
45
46
|
)
|
|
46
|
-
visitor.process(
|
|
47
|
-
|
|
48
|
-
visitor.mutants,
|
|
49
|
-
max_mutants_per_line: config&.max_mutants_per_line || 1
|
|
50
|
-
)
|
|
47
|
+
visitor.process(source_node)
|
|
48
|
+
visitor.mutants
|
|
51
49
|
end
|
|
52
50
|
|
|
53
51
|
# Depth-first pre-order AST visitor for a single subject.
|
|
@@ -60,13 +58,8 @@ module Henitai
|
|
|
60
58
|
@mutants = []
|
|
61
59
|
@arid_node_filter = arid_node_filter
|
|
62
60
|
@syntax_validator = syntax_validator
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
) do |operator, map|
|
|
66
|
-
operator.class.node_types.each do |node_type|
|
|
67
|
-
map[node_type] << operator
|
|
68
|
-
end
|
|
69
|
-
end
|
|
61
|
+
initialize_subject_range(subject)
|
|
62
|
+
@operators_by_node_type = index_operators(operators)
|
|
70
63
|
end
|
|
71
64
|
|
|
72
65
|
def process(node)
|
|
@@ -95,27 +88,28 @@ module Henitai
|
|
|
95
88
|
end
|
|
96
89
|
|
|
97
90
|
def node_within_subject_range?(node)
|
|
91
|
+
return true unless @subject_range_begin
|
|
92
|
+
|
|
98
93
|
location = node.location&.expression
|
|
99
|
-
return true unless location
|
|
94
|
+
return true unless location
|
|
100
95
|
|
|
101
|
-
|
|
102
|
-
ranges_overlap?(node_range, @subject.source_range)
|
|
96
|
+
location.line <= @subject_range_end && @subject_range_begin <= location.last_line
|
|
103
97
|
end
|
|
104
98
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
end
|
|
99
|
+
def initialize_subject_range(subject)
|
|
100
|
+
subject_range = subject.source_range
|
|
101
|
+
return unless subject_range
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
key = line_key(mutant)
|
|
113
|
-
selected[key] ||= []
|
|
114
|
-
selected[key] << mutant
|
|
103
|
+
@subject_range_begin = subject_range.begin
|
|
104
|
+
@subject_range_end = subject_range.end
|
|
115
105
|
end
|
|
116
106
|
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
def index_operators(operators)
|
|
108
|
+
operators.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |operator, map|
|
|
109
|
+
operator.class.node_types.each do |node_type|
|
|
110
|
+
map[node_type] << operator
|
|
111
|
+
end
|
|
112
|
+
end
|
|
119
113
|
end
|
|
120
114
|
end
|
|
121
115
|
|
|
@@ -131,28 +125,11 @@ module Henitai
|
|
|
131
125
|
)
|
|
132
126
|
end
|
|
133
127
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
mutant.location[:start_line]
|
|
138
|
-
]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def mutant_priority_key(mutant)
|
|
142
|
-
[
|
|
143
|
-
operator_priority(mutant.operator),
|
|
144
|
-
mutant.location[:start_col] || 0,
|
|
145
|
-
mutant.description
|
|
146
|
-
]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def operator_priority(operator_name)
|
|
150
|
-
operator_priority_map.fetch(operator_name, operator_priority_map.length)
|
|
151
|
-
end
|
|
128
|
+
def source_node_for(subject)
|
|
129
|
+
return subject.ast_node if subject.ast_node
|
|
130
|
+
return nil unless subject.source_file && subject.source_range
|
|
152
131
|
|
|
153
|
-
|
|
154
|
-
# The constant order defines signal priority for per-line pruning.
|
|
155
|
-
@operator_priority_map ||= Operator::FULL_SET.each_with_index.to_h
|
|
132
|
+
SourceParser.new.parse_file(subject.source_file)
|
|
156
133
|
end
|
|
157
134
|
end
|
|
158
135
|
end
|
data/lib/henitai/operator.rb
CHANGED
|
@@ -13,7 +13,8 @@ module Henitai
|
|
|
13
13
|
#
|
|
14
14
|
# Additional operators (full set):
|
|
15
15
|
# ArrayDeclaration, HashLiteral, RangeLiteral, SafeNavigation,
|
|
16
|
-
# PatternMatch, BlockStatement, MethodExpression, AssignmentExpression
|
|
16
|
+
# PatternMatch, BlockStatement, MethodExpression, AssignmentExpression,
|
|
17
|
+
# UnaryOperator, UpdateOperator, RegexMutator, MethodChainUnwrap
|
|
17
18
|
#
|
|
18
19
|
# Each operator subclass must implement:
|
|
19
20
|
# - .node_types → Array<Symbol> AST node types this operator handles
|
|
@@ -32,12 +33,16 @@ module Henitai
|
|
|
32
33
|
FULL_SET = (LIGHT_SET + %w[
|
|
33
34
|
ArrayDeclaration
|
|
34
35
|
HashLiteral
|
|
36
|
+
MethodChainUnwrap
|
|
35
37
|
RangeLiteral
|
|
38
|
+
RegexMutator
|
|
36
39
|
SafeNavigation
|
|
37
40
|
PatternMatch
|
|
38
41
|
BlockStatement
|
|
39
42
|
MethodExpression
|
|
40
43
|
AssignmentExpression
|
|
44
|
+
UnaryOperator
|
|
45
|
+
UpdateOperator
|
|
41
46
|
]).freeze
|
|
42
47
|
|
|
43
48
|
# @param set [Symbol] :light or :full
|