henitai 0.1.10 → 0.2.1
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 +94 -1
- data/README.md +33 -7
- data/assets/schema/henitai.schema.json +6 -0
- data/lib/henitai/cli/clean_command.rb +48 -0
- data/lib/henitai/cli/command_support.rb +51 -0
- data/lib/henitai/cli/init_command.rb +64 -0
- data/lib/henitai/cli/operator_command.rb +95 -0
- data/lib/henitai/cli/options.rb +120 -0
- data/lib/henitai/cli/run_command.rb +103 -0
- data/lib/henitai/cli.rb +17 -327
- data/lib/henitai/configuration.rb +26 -12
- data/lib/henitai/configuration_validator/rules.rb +143 -0
- data/lib/henitai/configuration_validator/scalars.rb +123 -0
- data/lib/henitai/configuration_validator.rb +12 -239
- data/lib/henitai/coverage_bootstrapper.rb +24 -24
- data/lib/henitai/eager_load.rb +36 -5
- data/lib/henitai/execution_engine.rb +6 -11
- data/lib/henitai/git_diff_analyzer.rb +34 -0
- data/lib/henitai/integration/base.rb +171 -0
- data/lib/henitai/integration/child_debug_support.rb +115 -0
- data/lib/henitai/integration/child_runtime_control.rb +50 -0
- data/lib/henitai/integration/coverage_suppression.rb +43 -0
- data/lib/henitai/integration/minitest.rb +133 -0
- data/lib/henitai/integration/mutant_run_support.rb +77 -0
- data/lib/henitai/integration/rspec_child_runner.rb +61 -0
- data/lib/henitai/integration/rspec_process_runner.rb +66 -13
- data/lib/henitai/integration/rspec_test_selection.rb +135 -0
- data/lib/henitai/integration/scenario_log_support.rb +116 -0
- data/lib/henitai/integration.rb +43 -519
- data/lib/henitai/mutant/activator.rb +13 -79
- data/lib/henitai/mutant/parameter_source.rb +98 -0
- data/lib/henitai/mutant.rb +14 -2
- data/lib/henitai/mutant_generator.rb +21 -2
- data/lib/henitai/mutant_history_store/sql.rb +72 -0
- data/lib/henitai/mutant_history_store.rb +12 -91
- data/lib/henitai/mutant_identity.rb +34 -0
- data/lib/henitai/parallel_execution_runner.rb +29 -11
- data/lib/henitai/per_test_coverage_collector.rb +3 -1
- data/lib/henitai/process_wakeup.rb +49 -0
- data/lib/henitai/process_worker_runner.rb +148 -0
- data/lib/henitai/reporter.rb +96 -11
- data/lib/henitai/result.rb +49 -16
- data/lib/henitai/runner.rb +96 -30
- data/lib/henitai/scenario_execution_result.rb +16 -3
- data/lib/henitai/slot_scheduler/draining.rb +140 -0
- data/lib/henitai/slot_scheduler/process_control.rb +43 -0
- data/lib/henitai/slot_scheduler.rb +214 -0
- data/lib/henitai/static_filter.rb +10 -3
- data/lib/henitai/survivor_activation_cache.rb +81 -0
- data/lib/henitai/survivor_loader.rb +140 -0
- data/lib/henitai/survivor_rerun_strategy.rb +195 -0
- data/lib/henitai/survivor_selector.rb +36 -0
- data/lib/henitai/survivor_test_filter.rb +72 -0
- data/lib/henitai/unparse_helper.rb +5 -2
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +10 -0
- data/sig/configuration_validator.rbs +46 -22
- data/sig/henitai.rbs +329 -53
- metadata +46 -2
data/lib/henitai/integration.rb
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "stringio"
|
|
5
|
+
require_relative "process_wakeup"
|
|
4
6
|
require_relative "integration/rspec_process_runner"
|
|
7
|
+
require_relative "integration/scenario_log_support"
|
|
8
|
+
require_relative "integration/coverage_suppression"
|
|
9
|
+
require_relative "integration/child_debug_support"
|
|
10
|
+
require_relative "integration/base"
|
|
11
|
+
require_relative "integration/mutant_run_support"
|
|
12
|
+
require_relative "integration/rspec_child_runner"
|
|
13
|
+
require_relative "integration/rspec_test_selection"
|
|
5
14
|
|
|
6
15
|
module Henitai
|
|
7
16
|
# Namespace for test-framework integrations.
|
|
@@ -18,96 +27,6 @@ module Henitai
|
|
|
18
27
|
# Built-in integrations:
|
|
19
28
|
# rspec — RSpec 3.x
|
|
20
29
|
module Integration
|
|
21
|
-
# Shared helpers for capturing stdout/stderr from child test processes.
|
|
22
|
-
class ScenarioLogSupport
|
|
23
|
-
def capture_child_output(log_paths)
|
|
24
|
-
output_files = open_child_output(log_paths)
|
|
25
|
-
yield
|
|
26
|
-
ensure
|
|
27
|
-
close_child_output(output_files)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def with_coverage_dir(mutant_id)
|
|
31
|
-
original_coverage_dir = ENV.fetch("HENITAI_COVERAGE_DIR", nil)
|
|
32
|
-
ENV["HENITAI_COVERAGE_DIR"] = mutation_coverage_dir(mutant_id)
|
|
33
|
-
yield
|
|
34
|
-
ensure
|
|
35
|
-
if original_coverage_dir.nil?
|
|
36
|
-
ENV.delete("HENITAI_COVERAGE_DIR")
|
|
37
|
-
else
|
|
38
|
-
ENV["HENITAI_COVERAGE_DIR"] = original_coverage_dir
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def open_child_output(log_paths)
|
|
43
|
-
FileUtils.mkdir_p(File.dirname(log_paths[:log_path]))
|
|
44
|
-
output_files = build_child_output_files(log_paths)
|
|
45
|
-
sync_child_output_files(output_files)
|
|
46
|
-
redirect_child_output(output_files)
|
|
47
|
-
output_files
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def close_child_output(output_files)
|
|
51
|
-
return unless output_files
|
|
52
|
-
|
|
53
|
-
restore_child_output(output_files)
|
|
54
|
-
close_child_output_files(output_files)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def build_child_output_files(log_paths)
|
|
58
|
-
{
|
|
59
|
-
original_stdout: stdout_stream.dup,
|
|
60
|
-
original_stderr: stderr_stream.dup,
|
|
61
|
-
stdout_file: File.new(log_paths[:stdout_path], "w"),
|
|
62
|
-
stderr_file: File.new(log_paths[:stderr_path], "w")
|
|
63
|
-
}
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def sync_child_output_files(output_files)
|
|
67
|
-
output_files[:stdout_file].sync = true
|
|
68
|
-
output_files[:stderr_file].sync = true
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def redirect_child_output(output_files)
|
|
72
|
-
reopen_child_output_stream(stdout_stream, output_files[:stdout_file])
|
|
73
|
-
reopen_child_output_stream(stderr_stream, output_files[:stderr_file])
|
|
74
|
-
$stdout = stdout_stream
|
|
75
|
-
$stderr = stderr_stream
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def restore_child_output(output_files)
|
|
79
|
-
reopen_child_output_stream(stdout_stream, output_files[:original_stdout])
|
|
80
|
-
reopen_child_output_stream(stderr_stream, output_files[:original_stderr])
|
|
81
|
-
$stdout = stdout_stream
|
|
82
|
-
$stderr = stderr_stream
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def reopen_child_output_stream(stream, original_stream)
|
|
86
|
-
stream.reopen(original_stream) if original_stream
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def close_child_output_files(output_files)
|
|
90
|
-
%i[stdout_file stderr_file original_stdout original_stderr].each do |key|
|
|
91
|
-
output_files[key]&.close
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
def mutation_coverage_dir(mutant_id)
|
|
98
|
-
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
99
|
-
File.join(reports_dir, "mutation-coverage", mutant_id.to_s)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def stdout_stream
|
|
103
|
-
@stdout_stream ||= IO.for_fd(1)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def stderr_stream
|
|
107
|
-
@stderr_stream ||= IO.for_fd(2)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
30
|
# Integration adapter for RSpec.
|
|
112
31
|
#
|
|
113
32
|
# This class exists as the stable public entry point for the RSpec
|
|
@@ -125,156 +44,15 @@ module Henitai
|
|
|
125
44
|
raise ArgumentError, "Unknown integration: #{name}. Available: #{available}"
|
|
126
45
|
end
|
|
127
46
|
|
|
128
|
-
# Base class for all integrations.
|
|
129
|
-
class Base
|
|
130
|
-
# @param subject [Subject]
|
|
131
|
-
# @return [Array<String>] paths to test files that cover this subject
|
|
132
|
-
def select_tests(subject)
|
|
133
|
-
raise NotImplementedError
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# @return [Array<String>] all test files for the configured framework
|
|
137
|
-
def test_files
|
|
138
|
-
raise NotImplementedError
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Run test files in a child process with the mutant active.
|
|
142
|
-
#
|
|
143
|
-
# @param mutant [Mutant]
|
|
144
|
-
# @param test_files [Array<String>]
|
|
145
|
-
# @param timeout [Float] seconds
|
|
146
|
-
# @return [ScenarioExecutionResult]
|
|
147
|
-
def run_mutant(mutant:, test_files:, timeout:)
|
|
148
|
-
raise NotImplementedError
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def per_test_coverage_supported?
|
|
152
|
-
false
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def wait_with_timeout(pid, timeout)
|
|
156
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
157
|
-
|
|
158
|
-
loop do
|
|
159
|
-
wait_result = Process.wait(pid, Process::WNOHANG)
|
|
160
|
-
return Process.last_status if wait_result
|
|
161
|
-
|
|
162
|
-
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
163
|
-
final_wait_result = Process.wait(pid, Process::WNOHANG)
|
|
164
|
-
return Process.last_status if final_wait_result
|
|
165
|
-
|
|
166
|
-
return handle_timeout(pid)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
pause(0.01)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def reap_child(pid)
|
|
174
|
-
Process.wait(pid)
|
|
175
|
-
rescue Errno::ECHILD, Errno::ESRCH
|
|
176
|
-
nil
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def cleanup_process_group(pid)
|
|
180
|
-
Process.kill(:SIGTERM, -pid)
|
|
181
|
-
pause(2.0)
|
|
182
|
-
Process.kill(:SIGKILL, -pid)
|
|
183
|
-
rescue Errno::EPERM
|
|
184
|
-
cleanup_child_process(pid)
|
|
185
|
-
rescue Errno::ESRCH
|
|
186
|
-
nil
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
private
|
|
190
|
-
|
|
191
|
-
def pause(seconds)
|
|
192
|
-
sleep(seconds)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def handle_timeout(pid)
|
|
196
|
-
begin
|
|
197
|
-
cleanup_process_group(pid)
|
|
198
|
-
ensure
|
|
199
|
-
reap_child(pid)
|
|
200
|
-
end
|
|
201
|
-
:timeout
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def cleanup_child_process(pid)
|
|
205
|
-
Process.kill(:SIGTERM, pid)
|
|
206
|
-
pause(2.0)
|
|
207
|
-
Process.kill(:SIGKILL, pid)
|
|
208
|
-
rescue Errno::EPERM, Errno::ESRCH
|
|
209
|
-
nil
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def rspec_options
|
|
213
|
-
[]
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def subprocess_env
|
|
217
|
-
{ "PARALLEL_WORKERS" => "1" }
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def scenario_log_support
|
|
221
|
-
@scenario_log_support ||= ScenarioLogSupport.new
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def with_subprocess_env
|
|
225
|
-
original_env = {} # : Hash[String, String?]
|
|
226
|
-
subprocess_env.each do |key, value|
|
|
227
|
-
original_env[key] = ENV.fetch(key, nil)
|
|
228
|
-
ENV[key] = value
|
|
229
|
-
end
|
|
230
|
-
yield
|
|
231
|
-
ensure
|
|
232
|
-
restore_subprocess_env(original_env)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def restore_subprocess_env(original_env)
|
|
236
|
-
original_env.each do |key, value|
|
|
237
|
-
if value.nil?
|
|
238
|
-
ENV.delete(key)
|
|
239
|
-
else
|
|
240
|
-
ENV[key] = value
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
47
|
# RSpec integration adapter.
|
|
247
48
|
class Rspec < Base
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
(require|require_relative)
|
|
252
|
-
\s*
|
|
253
|
-
(?:\(\s*)?
|
|
254
|
-
["']([^"']+)["']
|
|
255
|
-
\s*\)?
|
|
256
|
-
/x
|
|
49
|
+
include MutantRunSupport
|
|
50
|
+
include RspecChildRunner
|
|
51
|
+
include RspecTestSelection
|
|
257
52
|
|
|
258
|
-
|
|
259
|
-
matches = spec_files.select do |path|
|
|
260
|
-
content = File.read(path)
|
|
261
|
-
selection_patterns(subject).any? { |pattern| content.include?(pattern) }
|
|
262
|
-
rescue StandardError
|
|
263
|
-
false
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
return matches unless matches.empty?
|
|
267
|
-
|
|
268
|
-
fallback_spec_files(subject)
|
|
269
|
-
end
|
|
53
|
+
DEFAULT_SUITE_TIMEOUT = 300.0
|
|
270
54
|
|
|
271
|
-
def test_files
|
|
272
|
-
spec_files
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def run_mutant(mutant:, test_files:, timeout:)
|
|
276
|
-
RspecProcessRunner.new.run_mutant(self, mutant:, test_files:, timeout:)
|
|
277
|
-
end
|
|
55
|
+
def test_files = spec_files
|
|
278
56
|
|
|
279
57
|
def per_test_coverage_supported?
|
|
280
58
|
true
|
|
@@ -288,140 +66,30 @@ module Henitai
|
|
|
288
66
|
[
|
|
289
67
|
"bundle", "exec", "ruby",
|
|
290
68
|
"-r", "henitai/rspec_coverage_formatter",
|
|
291
|
-
"-
|
|
292
|
-
|
|
293
|
-
"--format", "Henitai::CoverageFormatter"
|
|
69
|
+
"-e", rspec_suite_runner_script,
|
|
70
|
+
*test_files
|
|
294
71
|
]
|
|
295
72
|
end
|
|
296
73
|
|
|
297
|
-
def
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return status if status.is_a?(Integer)
|
|
74
|
+
def rspec_suite_runner_script
|
|
75
|
+
<<~RUBY
|
|
76
|
+
require "rspec/core"
|
|
301
77
|
|
|
302
|
-
|
|
303
|
-
|
|
78
|
+
test_files = ARGV.map { |file| File.expand_path(file) }
|
|
79
|
+
config = RSpec.configuration
|
|
80
|
+
options = RSpec::Core::ConfigurationOptions.new(
|
|
81
|
+
["--format", "progress", "--format", "Henitai::CoverageFormatter"]
|
|
82
|
+
)
|
|
83
|
+
runner = RSpec::Core::Runner.send(:new, options)
|
|
304
84
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
stdout_path: File.join(log_dir, "#{name}.stdout.log"),
|
|
310
|
-
stderr_path: File.join(log_dir, "#{name}.stderr.log"),
|
|
311
|
-
log_path: File.join(log_dir, "#{name}.log")
|
|
312
|
-
}
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def build_result(wait_result, log_paths)
|
|
316
|
-
stdout = read_log_file(log_paths[:stdout_path])
|
|
317
|
-
stderr = read_log_file(log_paths[:stderr_path])
|
|
318
|
-
write_combined_log(log_paths[:log_path], stdout, stderr)
|
|
319
|
-
|
|
320
|
-
ScenarioExecutionResult.build(
|
|
321
|
-
wait_result:,
|
|
322
|
-
stdout:,
|
|
323
|
-
stderr:,
|
|
324
|
-
log_path: log_paths[:log_path]
|
|
325
|
-
)
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def spec_files
|
|
329
|
-
paths = Dir.glob("spec/**/*_spec.rb")
|
|
330
|
-
paths - excluded_spec_files
|
|
331
|
-
end
|
|
85
|
+
RSpec::Core::Runner.send(:trap_interrupt)
|
|
86
|
+
runner.send(:configure, $stderr, $stdout)
|
|
87
|
+
config.files_to_run = test_files
|
|
88
|
+
config.load_spec_files
|
|
332
89
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
matches = spec_files.select do |path|
|
|
337
|
-
requires_source_file_transitively?(path, subject.source_file)
|
|
338
|
-
rescue StandardError
|
|
339
|
-
false
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
matches.empty? ? spec_files : matches
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def excluded_spec_files
|
|
346
|
-
rspec_exclude_patterns.flat_map { |pattern| Dir.glob(pattern) }.uniq
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def rspec_exclude_patterns
|
|
350
|
-
rspec_config_lines.filter_map do |line|
|
|
351
|
-
line[/\A--exclude-pattern\s+(.+)\z/, 1]
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def rspec_config_lines
|
|
356
|
-
return [] unless File.exist?(rspec_config_path)
|
|
357
|
-
|
|
358
|
-
File.readlines(rspec_config_path, chomp: true).map(&:strip)
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def rspec_config_path
|
|
362
|
-
".rspec"
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def selection_patterns(subject)
|
|
366
|
-
[
|
|
367
|
-
subject.expression,
|
|
368
|
-
subject.namespace
|
|
369
|
-
].compact.uniq.sort_by(&:length).reverse
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
def requires_source_file?(spec_file, source_file)
|
|
373
|
-
content = File.read(spec_file)
|
|
374
|
-
basename = File.basename(source_file, ".rb")
|
|
375
|
-
content.include?(basename) || content.include?(source_file)
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def requires_source_file_transitively?(spec_file, source_file, visited = [])
|
|
379
|
-
normalized_spec_file = File.expand_path(spec_file)
|
|
380
|
-
return false if visited.include?(normalized_spec_file)
|
|
381
|
-
|
|
382
|
-
visited << normalized_spec_file
|
|
383
|
-
return true if requires_source_file?(spec_file, source_file)
|
|
384
|
-
|
|
385
|
-
required_files(spec_file).any? do |required_file|
|
|
386
|
-
requires_source_file_transitively?(required_file, source_file, visited)
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
def required_files(spec_file)
|
|
391
|
-
File.read(spec_file).lines.filter_map do |line|
|
|
392
|
-
match = line.match(REQUIRE_DIRECTIVE_PATTERN)
|
|
393
|
-
next unless match
|
|
394
|
-
|
|
395
|
-
resolve_required_file(spec_file, match[1].to_s, match[2].to_s)
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
def resolve_required_file(spec_file, method_name, required_path)
|
|
400
|
-
candidates =
|
|
401
|
-
if method_name == "require_relative"
|
|
402
|
-
relative_candidates(spec_file, required_path)
|
|
403
|
-
else
|
|
404
|
-
require_candidates(spec_file, required_path)
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
candidates.find { |candidate| File.file?(candidate) }
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
def relative_candidates(spec_file, required_path)
|
|
411
|
-
expand_candidates(File.dirname(spec_file), required_path)
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def require_candidates(spec_file, required_path)
|
|
415
|
-
([File.dirname(spec_file), Dir.pwd] + $LOAD_PATH).flat_map do |base_path|
|
|
416
|
-
expand_candidates(base_path, required_path)
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def expand_candidates(base_path, required_path)
|
|
421
|
-
[
|
|
422
|
-
File.expand_path(required_path, base_path),
|
|
423
|
-
File.expand_path("#{required_path}.rb", base_path)
|
|
424
|
-
].uniq
|
|
90
|
+
status = runner.send(:run_specs, RSpec.world.ordered_example_groups)
|
|
91
|
+
exit(status.is_a?(Integer) ? status : (status == true ? 0 : 1))
|
|
92
|
+
RUBY
|
|
425
93
|
end
|
|
426
94
|
|
|
427
95
|
def spawn_suite_process(test_files, log_paths)
|
|
@@ -438,168 +106,24 @@ module Henitai
|
|
|
438
106
|
end
|
|
439
107
|
end
|
|
440
108
|
|
|
441
|
-
def run_in_child(mutant:, test_files:, log_paths:)
|
|
442
|
-
Thread.report_on_exception = false
|
|
443
|
-
with_subprocess_env do
|
|
444
|
-
scenario_log_support.with_coverage_dir(mutant.id) do
|
|
445
|
-
scenario_log_support.capture_child_output(log_paths) do
|
|
446
|
-
return 2 if Mutant::Activator.activate!(mutant) == :compile_error
|
|
447
|
-
|
|
448
|
-
run_tests(test_files)
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
end
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
def read_log_file(path)
|
|
455
|
-
return "" unless File.exist?(path)
|
|
456
|
-
|
|
457
|
-
File.read(path)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
def write_combined_log(path, stdout, stderr)
|
|
461
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
462
|
-
File.write(path, combined_log(stdout, stderr))
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def combined_log(stdout, stderr)
|
|
466
|
-
[
|
|
467
|
-
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
468
|
-
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
469
|
-
].compact.join("\n")
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
# Stores the child-process log helpers shared by the integration specs.
|
|
474
|
-
class ScenarioLogSupport
|
|
475
|
-
def read_log_file(path)
|
|
476
|
-
return "" unless File.exist?(path)
|
|
477
|
-
|
|
478
|
-
File.read(path)
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
def write_combined_log(path, stdout, stderr)
|
|
482
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
483
|
-
File.write(path, combined_log(stdout, stderr))
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def combined_log(stdout, stderr)
|
|
487
|
-
[
|
|
488
|
-
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
489
|
-
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
490
|
-
].compact.join("\n")
|
|
491
|
-
end
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# Prepended onto SimpleCov's singleton class to turn start into a no-op
|
|
495
|
-
# during mutant child runs. Using prepend avoids "method redefined" warnings.
|
|
496
|
-
module SimpleCovStartSuppressor
|
|
497
|
-
def start(*_args) = nil
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# Minitest integration adapter.
|
|
501
|
-
#
|
|
502
|
-
# Coverage formatter injection remains implemented in the RSpec child
|
|
503
|
-
# runner. Minitest shares selection and execution semantics, but per-test
|
|
504
|
-
# coverage collection is not yet wired into this path.
|
|
505
|
-
class Minitest < Rspec
|
|
506
|
-
def per_test_coverage_supported?
|
|
507
|
-
true
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def run_mutant(mutant:, test_files:, timeout:)
|
|
511
|
-
setup_load_path
|
|
512
|
-
super
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
def run_in_child(mutant:, test_files:, log_paths:)
|
|
516
|
-
ENV["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
517
|
-
preload_environment
|
|
518
|
-
super
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
522
|
-
log_paths = scenario_log_paths("baseline")
|
|
523
|
-
pid = spawn_suite_process(test_files, log_paths)
|
|
524
|
-
wait_result = wait_with_timeout(pid, timeout)
|
|
525
|
-
build_result(wait_result, log_paths)
|
|
526
|
-
ensure
|
|
527
|
-
cleanup_suite_process(pid, wait_result)
|
|
528
|
-
end
|
|
529
|
-
|
|
530
109
|
private
|
|
531
110
|
|
|
532
|
-
def suite_command(test_files)
|
|
533
|
-
["bundle", "exec", "ruby", "-I", "test",
|
|
534
|
-
"-r", "henitai/minitest_simplecov",
|
|
535
|
-
"-r", "henitai/minitest_coverage_hook",
|
|
536
|
-
"-e", "ARGV.each { |f| require File.expand_path(f) }",
|
|
537
|
-
*test_files]
|
|
538
|
-
end
|
|
539
|
-
|
|
540
111
|
def run_tests(test_files)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
112
|
+
require "rspec/core"
|
|
113
|
+
::RSpec.__send__(:configuration).fail_if_no_examples = true
|
|
114
|
+
debug_child_rspec_trace(test_files:, rspec_options: [], rspec_argv: test_files)
|
|
115
|
+
debug_child_example_count("before_run") # steep:ignore Ruby::NoMethod
|
|
116
|
+
debug_child_puts("[henitai-debug-child] runner_run_start")
|
|
117
|
+
status = run_rspec_runner(test_files)
|
|
118
|
+
debug_child_puts("[henitai-debug-child] runner_run_return status=#{status.inspect}")
|
|
119
|
+
debug_child_example_count("after_run") # steep:ignore Ruby::NoMethod
|
|
120
|
+
debug_child_rspec_exit(status)
|
|
546
121
|
return status if status.is_a?(Integer)
|
|
547
122
|
|
|
548
123
|
status == true ? 0 : 1
|
|
549
124
|
end
|
|
550
|
-
|
|
551
|
-
def preload_environment
|
|
552
|
-
env_file = File.expand_path("config/environment.rb")
|
|
553
|
-
require env_file if File.exist?(env_file)
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
def setup_load_path
|
|
557
|
-
test_dir = File.expand_path("test")
|
|
558
|
-
$LOAD_PATH.unshift(test_dir) unless $LOAD_PATH.include?(test_dir)
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
def suppress_simplecov!
|
|
562
|
-
require "simplecov"
|
|
563
|
-
sc = Object.const_get(:SimpleCov) # steep:ignore Ruby::UnknownConstant
|
|
564
|
-
return if sc.singleton_class.ancestors.include?(SimpleCovStartSuppressor)
|
|
565
|
-
|
|
566
|
-
sc.singleton_class.prepend(SimpleCovStartSuppressor)
|
|
567
|
-
rescue LoadError, NameError
|
|
568
|
-
nil
|
|
569
|
-
end
|
|
570
|
-
|
|
571
|
-
def subprocess_env
|
|
572
|
-
env = super
|
|
573
|
-
env["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
574
|
-
env["PARALLEL_WORKERS"] = "1"
|
|
575
|
-
env
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
def spawn_suite_process(test_files, log_paths)
|
|
579
|
-
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
580
|
-
File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
581
|
-
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
582
|
-
Process.spawn(
|
|
583
|
-
subprocess_env,
|
|
584
|
-
*suite_command(test_files),
|
|
585
|
-
out: stdout_file,
|
|
586
|
-
err: stderr_file
|
|
587
|
-
)
|
|
588
|
-
end
|
|
589
|
-
end
|
|
590
|
-
end
|
|
591
|
-
|
|
592
|
-
def cleanup_suite_process(pid, wait_result)
|
|
593
|
-
return unless pid
|
|
594
|
-
|
|
595
|
-
cleanup_child_process(pid)
|
|
596
|
-
reap_child(pid) if wait_result.nil?
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
def spec_files
|
|
600
|
-
(Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb"))
|
|
601
|
-
.reject { |f| f.start_with?("test/system/") }
|
|
602
|
-
end
|
|
603
125
|
end
|
|
604
126
|
end
|
|
605
127
|
end
|
|
128
|
+
|
|
129
|
+
require_relative "integration/minitest"
|