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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +105 -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 +128 -10
- 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 +195 -110
- 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/string_literal.rb +2 -1
- 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/reporter.rb +14 -2
- data/lib/henitai/result.rb +16 -4
- data/lib/henitai/runner.rb +75 -11
- data/lib/henitai/scenario_execution_result.rb +31 -2
- 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 +66 -10
- 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
|
|
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.
|
|
@@ -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")
|
|
@@ -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.
|
|
300
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 =
|
|
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/") }
|
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
|