henitai 0.1.1 → 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 +91 -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 -8
- data/lib/henitai/coverage_formatter.rb +4 -4
- 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 +222 -100
- data/lib/henitai/minitest_simplecov.rb +3 -0
- data/lib/henitai/mutant/activator.rb +78 -42
- 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/rspec_coverage_formatter.rb +10 -0
- data/lib/henitai/runner.rb +75 -11
- data/lib/henitai/source_parser.rb +12 -1
- data/lib/henitai/static_filter.rb +53 -38
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +3 -0
- data/sig/henitai.rbs +65 -11
- metadata +18 -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
|
|
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
|
-
|
|
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.
|
|
@@ -56,8 +57,8 @@ module Henitai
|
|
|
56
57
|
|
|
57
58
|
def build_child_output_files(log_paths)
|
|
58
59
|
{
|
|
59
|
-
original_stdout:
|
|
60
|
-
original_stderr:
|
|
60
|
+
original_stdout: stdout_stream.dup,
|
|
61
|
+
original_stderr: stderr_stream.dup,
|
|
61
62
|
stdout_file: File.new(log_paths[:stdout_path], "w"),
|
|
62
63
|
stderr_file: File.new(log_paths[:stderr_path], "w")
|
|
63
64
|
}
|
|
@@ -69,13 +70,17 @@ module Henitai
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def redirect_child_output(output_files)
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
reopen_child_output_stream(stdout_stream, output_files[:stdout_file])
|
|
74
|
+
reopen_child_output_stream(stderr_stream, output_files[:stderr_file])
|
|
75
|
+
$stdout = stdout_stream
|
|
76
|
+
$stderr = stderr_stream
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def restore_child_output(output_files)
|
|
77
|
-
reopen_child_output_stream(
|
|
78
|
-
reopen_child_output_stream(
|
|
80
|
+
reopen_child_output_stream(stdout_stream, output_files[:original_stdout])
|
|
81
|
+
reopen_child_output_stream(stderr_stream, output_files[:original_stderr])
|
|
82
|
+
$stdout = output_files[:original_stdout]
|
|
83
|
+
$stderr = output_files[:original_stderr]
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
def reopen_child_output_stream(stream, original_stream)
|
|
@@ -94,6 +99,14 @@ module Henitai
|
|
|
94
99
|
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
95
100
|
File.join(reports_dir, "mutation-coverage", mutant_id.to_s)
|
|
96
101
|
end
|
|
102
|
+
|
|
103
|
+
def stdout_stream
|
|
104
|
+
@stdout_stream ||= IO.for_fd(1)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def stderr_stream
|
|
108
|
+
@stderr_stream ||= IO.for_fd(2)
|
|
109
|
+
end
|
|
97
110
|
end
|
|
98
111
|
|
|
99
112
|
# Integration adapter for RSpec.
|
|
@@ -105,7 +118,12 @@ module Henitai
|
|
|
105
118
|
def self.for(name)
|
|
106
119
|
const_get(name.capitalize)
|
|
107
120
|
rescue NameError
|
|
108
|
-
|
|
121
|
+
available = constants.filter_map do |constant_name|
|
|
122
|
+
integration = const_get(constant_name)
|
|
123
|
+
constant_name.to_s.downcase if integration.is_a?(Class) && integration < Base
|
|
124
|
+
end.sort.join(", ")
|
|
125
|
+
|
|
126
|
+
raise ArgumentError, "Unknown integration: #{name}. Available: #{available}"
|
|
109
127
|
end
|
|
110
128
|
|
|
111
129
|
# Base class for all integrations.
|
|
@@ -130,6 +148,100 @@ module Henitai
|
|
|
130
148
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
131
149
|
raise NotImplementedError
|
|
132
150
|
end
|
|
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
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def pause(seconds)
|
|
193
|
+
sleep(seconds)
|
|
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
|
|
133
245
|
end
|
|
134
246
|
|
|
135
247
|
# RSpec integration adapter.
|
|
@@ -162,64 +274,25 @@ module Henitai
|
|
|
162
274
|
end
|
|
163
275
|
|
|
164
276
|
def run_mutant(mutant:, test_files:, timeout:)
|
|
165
|
-
|
|
166
|
-
pid = Process.fork do
|
|
167
|
-
ENV["HENITAI_MUTANT_ID"] = mutant.id
|
|
168
|
-
Process.exit(run_in_child(mutant:, test_files:, log_paths:))
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
build_result(wait_with_timeout(pid, timeout), log_paths)
|
|
277
|
+
RspecProcessRunner.new.run_mutant(self, mutant:, test_files:, timeout:)
|
|
172
278
|
end
|
|
173
279
|
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
177
|
-
pid = File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
178
|
-
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
179
|
-
Process.spawn(*suite_command(test_files), out: stdout_file, err: stderr_file)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
build_result(wait_with_timeout(pid, timeout), log_paths)
|
|
280
|
+
def per_test_coverage_supported?
|
|
281
|
+
true
|
|
183
282
|
end
|
|
184
283
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def run_in_child(mutant:, test_files:, log_paths:)
|
|
188
|
-
scenario_log_support.with_coverage_dir(mutant.id) do
|
|
189
|
-
scenario_log_support.capture_child_output(log_paths) do
|
|
190
|
-
return 2 if Mutant::Activator.activate!(mutant) == :compile_error
|
|
191
|
-
|
|
192
|
-
run_tests(test_files)
|
|
193
|
-
end
|
|
194
|
-
end
|
|
284
|
+
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
285
|
+
RspecProcessRunner.new.run_suite(self, test_files, timeout:)
|
|
195
286
|
end
|
|
196
287
|
|
|
197
288
|
def suite_command(test_files)
|
|
198
|
-
[
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return Process.last_status if Process.wait(pid, Process::WNOHANG)
|
|
206
|
-
return handle_timeout(pid) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
207
|
-
|
|
208
|
-
pause(0.01)
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def handle_timeout(pid)
|
|
213
|
-
begin
|
|
214
|
-
Process.kill(:SIGTERM, pid)
|
|
215
|
-
pause(2.0)
|
|
216
|
-
Process.kill(:SIGKILL, pid)
|
|
217
|
-
rescue Errno::ESRCH
|
|
218
|
-
# The child may exit after SIGTERM but before SIGKILL.
|
|
219
|
-
ensure
|
|
220
|
-
reap_child(pid)
|
|
221
|
-
end
|
|
222
|
-
: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
|
+
]
|
|
223
296
|
end
|
|
224
297
|
|
|
225
298
|
def run_tests(test_files)
|
|
@@ -230,36 +303,6 @@ module Henitai
|
|
|
230
303
|
status == true ? 0 : 1
|
|
231
304
|
end
|
|
232
305
|
|
|
233
|
-
def rspec_options
|
|
234
|
-
["--require", "henitai/coverage_formatter"]
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def pause(seconds)
|
|
238
|
-
sleep(seconds)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def scenario_log_support
|
|
242
|
-
@scenario_log_support ||= ScenarioLogSupport.new
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def read_log_file(path)
|
|
246
|
-
return "" unless File.exist?(path)
|
|
247
|
-
|
|
248
|
-
File.read(path)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def write_combined_log(path, stdout, stderr)
|
|
252
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
253
|
-
File.write(path, combined_log(stdout, stderr))
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def combined_log(stdout, stderr)
|
|
257
|
-
[
|
|
258
|
-
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
259
|
-
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
260
|
-
].compact.join("\n")
|
|
261
|
-
end
|
|
262
|
-
|
|
263
306
|
def scenario_log_paths(name)
|
|
264
307
|
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
265
308
|
log_dir = File.join(reports_dir, "mutation-logs")
|
|
@@ -378,10 +421,70 @@ module Henitai
|
|
|
378
421
|
].uniq
|
|
379
422
|
end
|
|
380
423
|
|
|
381
|
-
def
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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")
|
|
385
488
|
end
|
|
386
489
|
end
|
|
387
490
|
|
|
@@ -404,13 +507,11 @@ module Henitai
|
|
|
404
507
|
|
|
405
508
|
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
406
509
|
log_paths = scenario_log_paths("baseline")
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
end
|
|
413
|
-
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)
|
|
414
515
|
end
|
|
415
516
|
|
|
416
517
|
private
|
|
@@ -443,12 +544,33 @@ module Henitai
|
|
|
443
544
|
end
|
|
444
545
|
|
|
445
546
|
def subprocess_env
|
|
446
|
-
env =
|
|
547
|
+
env = super
|
|
447
548
|
env["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
448
549
|
env["PARALLEL_WORKERS"] = "1"
|
|
449
550
|
env
|
|
450
551
|
end
|
|
451
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
|
+
|
|
452
574
|
def spec_files
|
|
453
575
|
(Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb"))
|
|
454
576
|
.reject { |f| f.start_with?("test/system/") }
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
# Must be required before any application code is loaded so that Coverage
|
|
7
7
|
# tracking is active from the first line.
|
|
8
8
|
|
|
9
|
+
require "coverage"
|
|
10
|
+
Coverage.start(lines: true, branches: true, methods: true)
|
|
11
|
+
|
|
9
12
|
require "simplecov"
|
|
10
13
|
|
|
11
14
|
SimpleCov.coverage_dir(ENV.fetch("HENITAI_COVERAGE_DIR", "coverage"))
|