henitai 0.1.2 → 0.1.4

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -1
  3. data/README.md +3 -1
  4. data/assets/schema/henitai.schema.json +0 -4
  5. data/lib/henitai/arid_node_filter.rb +3 -0
  6. data/lib/henitai/available_cpu_count.rb +79 -0
  7. data/lib/henitai/cli.rb +7 -4
  8. data/lib/henitai/configuration.rb +3 -5
  9. data/lib/henitai/configuration_validator.rb +1 -11
  10. data/lib/henitai/coverage_bootstrapper.rb +128 -10
  11. data/lib/henitai/coverage_report_reader.rb +67 -0
  12. data/lib/henitai/eager_load.rb +11 -0
  13. data/lib/henitai/equivalence_detector.rb +60 -1
  14. data/lib/henitai/execution_engine.rb +34 -22
  15. data/lib/henitai/integration/rspec_process_runner.rb +58 -0
  16. data/lib/henitai/integration.rb +195 -110
  17. data/lib/henitai/mutant.rb +3 -1
  18. data/lib/henitai/mutant_generator.rb +25 -48
  19. data/lib/henitai/operator.rb +6 -1
  20. data/lib/henitai/operators/assignment_expression.rb +7 -23
  21. data/lib/henitai/operators/conditional_expression.rb +1 -7
  22. data/lib/henitai/operators/method_chain_unwrap.rb +41 -0
  23. data/lib/henitai/operators/regex_mutator.rb +89 -0
  24. data/lib/henitai/operators/string_literal.rb +2 -1
  25. data/lib/henitai/operators/unary_operator.rb +36 -0
  26. data/lib/henitai/operators/update_operator.rb +70 -0
  27. data/lib/henitai/operators.rb +4 -0
  28. data/lib/henitai/parallel_execution_runner.rb +135 -0
  29. data/lib/henitai/per_test_coverage_selector.rb +60 -0
  30. data/lib/henitai/reporter.rb +14 -2
  31. data/lib/henitai/result.rb +16 -4
  32. data/lib/henitai/runner.rb +75 -11
  33. data/lib/henitai/scenario_execution_result.rb +31 -2
  34. data/lib/henitai/source_parser.rb +12 -1
  35. data/lib/henitai/static_filter.rb +20 -41
  36. data/lib/henitai/version.rb +1 -1
  37. data/lib/henitai.rb +3 -0
  38. data/sig/henitai.rbs +66 -10
  39. metadata +17 -4
@@ -10,7 +10,7 @@ module Henitai
10
10
  # obvious enough to be useful.
11
11
  class EquivalenceDetector
12
12
  def analyze(mutant)
13
- return mutant unless equivalent_arithmetic_mutation?(mutant)
13
+ return mutant unless equivalent_mutation?(mutant)
14
14
 
15
15
  mutant.status = :equivalent
16
16
  mutant
@@ -18,6 +18,10 @@ module Henitai
18
18
 
19
19
  private
20
20
 
21
+ def equivalent_mutation?(mutant)
22
+ equivalent_arithmetic_mutation?(mutant) || equivalent_logical_mutation?(mutant)
23
+ end
24
+
21
25
  def equivalent_arithmetic_mutation?(mutant)
22
26
  original = mutant.original_node
23
27
  mutated = mutant.mutated_node
@@ -50,6 +54,61 @@ module Henitai
50
54
  one_operand?(mutated)
51
55
  end
52
56
 
57
+ def equivalent_logical_mutation?(mutant)
58
+ original = mutant.original_node
59
+ mutated = mutant.mutated_node
60
+ return false unless logical_node?(original)
61
+
62
+ logical_identity_equivalent?(original, mutated)
63
+ end
64
+
65
+ def logical_node?(node)
66
+ node.is_a?(Parser::AST::Node) && %i[and or].include?(node.type)
67
+ end
68
+
69
+ def logical_identity_equivalent?(original, mutated)
70
+ case original.type
71
+ when :or
72
+ false_identity_equivalent?(original, mutated)
73
+ when :and
74
+ true_identity_equivalent?(original, mutated)
75
+ else
76
+ false
77
+ end
78
+ end
79
+
80
+ def false_identity_equivalent?(original, mutated)
81
+ return true if false_operand?(original.children[0]) && same_node?(mutated, original.children[1])
82
+ return true if false_operand?(original.children[1]) && same_node?(mutated, original.children[0])
83
+
84
+ false
85
+ end
86
+
87
+ def true_identity_equivalent?(original, mutated)
88
+ return true if true_operand?(original.children[0]) && same_node?(mutated, original.children[1])
89
+ return true if true_operand?(original.children[1]) && same_node?(mutated, original.children[0])
90
+
91
+ false
92
+ end
93
+
94
+ def false_operand?(node)
95
+ # Parser uses :true / :false node types, so the AST symbols are intentional.
96
+ # rubocop:disable Lint/BooleanSymbol
97
+ boolean_literal?(node, :false)
98
+ # rubocop:enable Lint/BooleanSymbol
99
+ end
100
+
101
+ def true_operand?(node)
102
+ # Parser uses :true / :false node types, so the AST symbols are intentional.
103
+ # rubocop:disable Lint/BooleanSymbol
104
+ boolean_literal?(node, :true)
105
+ # rubocop:enable Lint/BooleanSymbol
106
+ end
107
+
108
+ def boolean_literal?(node, type)
109
+ node.is_a?(Parser::AST::Node) && node.type == type && node.children.empty?
110
+ end
111
+
53
112
  def additive_operator?(operator)
54
113
  %i[+ -].include?(operator)
55
114
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "etc"
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, mutex)
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 || Etc.nprocessors
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, mutex)
44
- queue = Queue.new
45
- mutants.each { |mutant| queue << mutant }
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
- Array.new(worker_count(config)) do
48
- Thread.new do
49
- loop do
50
- mutant = queue.pop(true)
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
- test_prioritizer.sort(
73
- integration.select_tests(mutant.subject),
82
+ tests = integration.select_tests(mutant.subject)
83
+ tests = per_test_coverage_selector.filter(
84
+ tests,
74
85
  mutant,
75
- test_history(config)
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
- @test_prioritizer ||= TestPrioritizer.new
81
- end
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
@@ -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
- log_paths = scenario_log_paths("mutant-#{mutant.id}")
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 run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
198
- log_paths = scenario_log_paths("baseline")
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
- private
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
- ["bundle", "exec", "rspec", *test_files]
223
- end
224
-
225
- def wait_with_timeout(pid, timeout)
226
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
227
-
228
- loop do
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")
@@ -291,35 +314,18 @@ module Henitai
291
314
  end
292
315
 
293
316
  def build_result(wait_result, log_paths)
294
- status = scenario_status(wait_result)
295
317
  stdout = read_log_file(log_paths[:stdout_path])
296
318
  stderr = read_log_file(log_paths[:stderr_path])
297
319
  write_combined_log(log_paths[:log_path], stdout, stderr)
298
320
 
299
- ScenarioExecutionResult.new(
300
- status:,
321
+ ScenarioExecutionResult.build(
322
+ wait_result:,
301
323
  stdout:,
302
324
  stderr:,
303
- log_path: log_paths[:log_path],
304
- exit_status: exit_status_for(wait_result)
325
+ log_path: log_paths[:log_path]
305
326
  )
306
327
  end
307
328
 
308
- def scenario_status(wait_result)
309
- return :timeout if wait_result == :timeout
310
- return :compile_error if exit_status_for(wait_result) == 2
311
- return :survived if wait_result.respond_to?(:success?) && wait_result.success?
312
-
313
- :killed
314
- end
315
-
316
- def exit_status_for(wait_result)
317
- return nil if wait_result == :timeout
318
- return nil unless wait_result.respond_to?(:exitstatus)
319
-
320
- wait_result.exitstatus
321
- end
322
-
323
329
  def spec_files
324
330
  Dir.glob("spec/**/*_spec.rb")
325
331
  end
@@ -398,10 +404,70 @@ module Henitai
398
404
  ].uniq
399
405
  end
400
406
 
401
- def reap_child(pid)
402
- Process.wait(pid)
403
- rescue Errno::ECHILD, Errno::ESRCH
404
- nil
407
+ def spawn_suite_process(test_files, log_paths)
408
+ File.open(log_paths[:stdout_path], "w") do |stdout_file|
409
+ File.open(log_paths[:stderr_path], "w") do |stderr_file|
410
+ Process.spawn(
411
+ subprocess_env,
412
+ *suite_command(test_files),
413
+ out: stdout_file,
414
+ err: stderr_file,
415
+ pgroup: true
416
+ )
417
+ end
418
+ end
419
+ end
420
+
421
+ def run_in_child(mutant:, test_files:, log_paths:)
422
+ Thread.report_on_exception = false
423
+ with_subprocess_env do
424
+ scenario_log_support.with_coverage_dir(mutant.id) do
425
+ scenario_log_support.capture_child_output(log_paths) do
426
+ return 2 if Mutant::Activator.activate!(mutant) == :compile_error
427
+
428
+ run_tests(test_files)
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ def read_log_file(path)
435
+ return "" unless File.exist?(path)
436
+
437
+ File.read(path)
438
+ end
439
+
440
+ def write_combined_log(path, stdout, stderr)
441
+ FileUtils.mkdir_p(File.dirname(path))
442
+ File.write(path, combined_log(stdout, stderr))
443
+ end
444
+
445
+ def combined_log(stdout, stderr)
446
+ [
447
+ (stdout.empty? ? nil : "stdout:\n#{stdout}"),
448
+ (stderr.empty? ? nil : "stderr:\n#{stderr}")
449
+ ].compact.join("\n")
450
+ end
451
+ end
452
+
453
+ # Stores the child-process log helpers shared by the integration specs.
454
+ class ScenarioLogSupport
455
+ def read_log_file(path)
456
+ return "" unless File.exist?(path)
457
+
458
+ File.read(path)
459
+ end
460
+
461
+ def write_combined_log(path, stdout, stderr)
462
+ FileUtils.mkdir_p(File.dirname(path))
463
+ File.write(path, combined_log(stdout, stderr))
464
+ end
465
+
466
+ def combined_log(stdout, stderr)
467
+ [
468
+ (stdout.empty? ? nil : "stdout:\n#{stdout}"),
469
+ (stderr.empty? ? nil : "stderr:\n#{stderr}")
470
+ ].compact.join("\n")
405
471
  end
406
472
  end
407
473
 
@@ -424,13 +490,11 @@ module Henitai
424
490
 
425
491
  def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
426
492
  log_paths = scenario_log_paths("baseline")
427
- FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
428
- pid = File.open(log_paths[:stdout_path], "w") do |stdout_file|
429
- File.open(log_paths[:stderr_path], "w") do |stderr_file|
430
- Process.spawn(subprocess_env, *suite_command(test_files), out: stdout_file, err: stderr_file)
431
- end
432
- end
433
- build_result(wait_with_timeout(pid, timeout), log_paths)
493
+ pid = spawn_suite_process(test_files, log_paths)
494
+ wait_result = wait_with_timeout(pid, timeout)
495
+ build_result(wait_result, log_paths)
496
+ ensure
497
+ cleanup_suite_process(pid, wait_result)
434
498
  end
435
499
 
436
500
  private
@@ -463,12 +527,33 @@ module Henitai
463
527
  end
464
528
 
465
529
  def subprocess_env
466
- env = {} # : Hash[String, String]
530
+ env = super
467
531
  env["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
468
532
  env["PARALLEL_WORKERS"] = "1"
469
533
  env
470
534
  end
471
535
 
536
+ def spawn_suite_process(test_files, log_paths)
537
+ FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
538
+ File.open(log_paths[:stdout_path], "w") do |stdout_file|
539
+ File.open(log_paths[:stderr_path], "w") do |stderr_file|
540
+ Process.spawn(
541
+ subprocess_env,
542
+ *suite_command(test_files),
543
+ out: stdout_file,
544
+ err: stderr_file
545
+ )
546
+ end
547
+ end
548
+ end
549
+
550
+ def cleanup_suite_process(pid, wait_result)
551
+ return unless pid
552
+
553
+ cleanup_child_process(pid)
554
+ reap_child(pid) if wait_result.nil?
555
+ end
556
+
472
557
  def spec_files
473
558
  (Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb"))
474
559
  .reject { |f| f.start_with?("test/system/") }
@@ -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