henitai 0.2.0 → 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 +26 -1
- data/README.md +15 -3
- 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 +16 -404
- data/lib/henitai/configuration.rb +2 -1
- 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/eager_load.rb +36 -5
- data/lib/henitai/execution_engine.rb +4 -3
- 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_test_selection.rb +135 -0
- data/lib/henitai/integration/scenario_log_support.rb +116 -0
- data/lib/henitai/integration.rb +22 -846
- data/lib/henitai/mutant/activator.rb +1 -79
- data/lib/henitai/mutant/parameter_source.rb +98 -0
- data/lib/henitai/mutant.rb +1 -0
- data/lib/henitai/mutant_history_store/sql.rb +72 -0
- data/lib/henitai/mutant_history_store.rb +5 -69
- data/lib/henitai/per_test_coverage_collector.rb +3 -1
- data/lib/henitai/process_worker_runner.rb +48 -334
- data/lib/henitai/reporter.rb +20 -8
- data/lib/henitai/result.rb +17 -15
- data/lib/henitai/runner.rb +59 -182
- 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/survivor_rerun_strategy.rb +195 -0
- data/lib/henitai/unparse_helper.rb +5 -2
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +2 -0
- data/sig/configuration_validator.rbs +46 -22
- data/sig/henitai.rbs +158 -73
- metadata +25 -2
data/lib/henitai/integration.rb
CHANGED
|
@@ -4,6 +4,13 @@ require "fileutils"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
require_relative "process_wakeup"
|
|
6
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"
|
|
7
14
|
|
|
8
15
|
module Henitai
|
|
9
16
|
# Namespace for test-framework integrations.
|
|
@@ -20,294 +27,6 @@ module Henitai
|
|
|
20
27
|
# Built-in integrations:
|
|
21
28
|
# rspec — RSpec 3.x
|
|
22
29
|
module Integration
|
|
23
|
-
# Shared helpers for capturing stdout/stderr from child test processes.
|
|
24
|
-
class ScenarioLogSupport
|
|
25
|
-
def capture_child_output(log_paths)
|
|
26
|
-
output_files = open_child_output(log_paths)
|
|
27
|
-
yield
|
|
28
|
-
ensure
|
|
29
|
-
close_child_output(output_files)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def with_coverage_dir(mutant_id)
|
|
33
|
-
original_coverage_dir = ENV.fetch("HENITAI_COVERAGE_DIR", nil)
|
|
34
|
-
ENV["HENITAI_COVERAGE_DIR"] = mutation_coverage_dir(mutant_id)
|
|
35
|
-
yield
|
|
36
|
-
ensure
|
|
37
|
-
if original_coverage_dir.nil?
|
|
38
|
-
ENV.delete("HENITAI_COVERAGE_DIR")
|
|
39
|
-
else
|
|
40
|
-
ENV["HENITAI_COVERAGE_DIR"] = original_coverage_dir
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def open_child_output(log_paths)
|
|
45
|
-
FileUtils.mkdir_p(File.dirname(log_paths[:log_path]))
|
|
46
|
-
output_files = build_child_output_files(log_paths)
|
|
47
|
-
sync_child_output_files(output_files)
|
|
48
|
-
redirect_child_output(output_files)
|
|
49
|
-
output_files
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def close_child_output(output_files)
|
|
53
|
-
return unless output_files
|
|
54
|
-
|
|
55
|
-
restore_child_output(output_files)
|
|
56
|
-
close_child_output_files(output_files)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def build_child_output_files(log_paths)
|
|
60
|
-
{
|
|
61
|
-
original_stdout: stdout_stream.dup,
|
|
62
|
-
original_stderr: stderr_stream.dup,
|
|
63
|
-
stdout_file: File.new(log_paths[:stdout_path], "w"),
|
|
64
|
-
stderr_file: File.new(log_paths[:stderr_path], "w")
|
|
65
|
-
}
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def sync_child_output_files(output_files)
|
|
69
|
-
output_files[:stdout_file].sync = true
|
|
70
|
-
output_files[:stderr_file].sync = true
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def redirect_child_output(output_files)
|
|
74
|
-
reopen_child_output_stream(stdout_stream, output_files[:stdout_file])
|
|
75
|
-
reopen_child_output_stream(stderr_stream, output_files[:stderr_file])
|
|
76
|
-
$stdout = stdout_stream
|
|
77
|
-
$stderr = stderr_stream
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def restore_child_output(output_files)
|
|
81
|
-
reopen_child_output_stream(stdout_stream, output_files[:original_stdout])
|
|
82
|
-
reopen_child_output_stream(stderr_stream, output_files[:original_stderr])
|
|
83
|
-
$stdout = stdout_stream
|
|
84
|
-
$stderr = stderr_stream
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def reopen_child_output_stream(stream, original_stream)
|
|
88
|
-
stream.reopen(original_stream) if original_stream
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def close_child_output_files(output_files)
|
|
92
|
-
%i[stdout_file stderr_file original_stdout original_stderr].each do |key|
|
|
93
|
-
output_files[key]&.close
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
def mutation_coverage_dir(mutant_id)
|
|
100
|
-
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
101
|
-
File.join(reports_dir, "mutation-coverage", mutant_id.to_s)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def stdout_stream
|
|
105
|
-
@stdout_stream ||= IO.for_fd(1)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def stderr_stream
|
|
109
|
-
@stderr_stream ||= IO.for_fd(2)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Shared debug helpers for child-run diagnostics.
|
|
114
|
-
# Debug helpers are intentionally grouped here so the child-run diagnostics
|
|
115
|
-
# stay isolated from the main integration flow.
|
|
116
|
-
# rubocop:disable Metrics/ModuleLength
|
|
117
|
-
module ChildDebugSupport
|
|
118
|
-
private
|
|
119
|
-
|
|
120
|
-
def run_rspec_runner(test_files)
|
|
121
|
-
debug_child_puts("[henitai-debug-child] build_rspec_runner_start")
|
|
122
|
-
runner = build_rspec_runner
|
|
123
|
-
debug_child_puts("[henitai-debug-child] build_rspec_runner_return")
|
|
124
|
-
debug_child_puts("[henitai-debug-child] configure_rspec_runner_start")
|
|
125
|
-
configure_rspec_runner(runner)
|
|
126
|
-
debug_child_puts("[henitai-debug-child] configure_rspec_runner_return")
|
|
127
|
-
load_rspec_spec_files(test_files)
|
|
128
|
-
run_rspec_specs(runner)
|
|
129
|
-
rescue SystemExit => e
|
|
130
|
-
debug_child_puts("[henitai-debug-child] runner_run_system_exit status=#{e.status.inspect}")
|
|
131
|
-
raise
|
|
132
|
-
ensure
|
|
133
|
-
debug_child_puts("[henitai-debug-child] runner_run_ensure")
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def build_rspec_runner
|
|
137
|
-
# @type var empty_args: Array[String]
|
|
138
|
-
empty_args = []
|
|
139
|
-
configuration_options = ::RSpec::Core.const_get(:ConfigurationOptions).new(empty_args)
|
|
140
|
-
::RSpec::Core::Runner.__send__(:new, configuration_options)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def configure_rspec_runner(runner)
|
|
144
|
-
debug_child_puts("[henitai-debug-child] trap_interrupt_start")
|
|
145
|
-
::RSpec::Core::Runner.__send__(:trap_interrupt)
|
|
146
|
-
debug_child_puts("[henitai-debug-child] trap_interrupt_return")
|
|
147
|
-
debug_child_puts("[henitai-debug-child] runner_configure_start")
|
|
148
|
-
runner.send(:configure, $stderr, $stdout)
|
|
149
|
-
debug_child_puts("[henitai-debug-child] runner_configure_return")
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def load_rspec_spec_files(test_files)
|
|
153
|
-
debug_child_puts("[henitai-debug-child] load_spec_files_start")
|
|
154
|
-
::RSpec.__send__(:configuration).files_to_run = test_files.map do |file|
|
|
155
|
-
File.expand_path(file)
|
|
156
|
-
end
|
|
157
|
-
::RSpec.__send__(:configuration).load_spec_files
|
|
158
|
-
debug_child_example_count("after_load")
|
|
159
|
-
debug_child_puts("[henitai-debug-child] load_spec_files_return")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def run_rspec_specs(runner)
|
|
163
|
-
debug_child_puts("[henitai-debug-child] run_specs_start")
|
|
164
|
-
result = runner.send(:run_specs, ::RSpec.__send__(:world).ordered_example_groups)
|
|
165
|
-
debug_child_puts("[henitai-debug-child] run_specs_return result=#{result.inspect}")
|
|
166
|
-
result
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def debug_child? = ENV["HENITAI_DEBUG_CHILD"] == "1"
|
|
170
|
-
|
|
171
|
-
def debug_child_puts(message)
|
|
172
|
-
$stdout.puts(message)
|
|
173
|
-
$stdout.flush
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def debug_child_rspec_trace(test_files:, rspec_options:, rspec_argv:)
|
|
177
|
-
return unless debug_child?
|
|
178
|
-
|
|
179
|
-
files_exist = test_files.map { |f| [f, File.exist?(f)] }.inspect
|
|
180
|
-
loaded_features = loaded_feature_map(test_files).inspect # steep:ignore Ruby::NoMethod
|
|
181
|
-
|
|
182
|
-
debug_child_puts(
|
|
183
|
-
"[henitai-debug-child] cwd=#{Dir.pwd}\n" \
|
|
184
|
-
"[henitai-debug-child] files_exist=#{files_exist}\n" \
|
|
185
|
-
"[henitai-debug-child] loaded_features_check=#{loaded_features}\n" \
|
|
186
|
-
"[henitai-debug-child] test_files=#{test_files.inspect}\n" \
|
|
187
|
-
"[henitai-debug-child] rspec_options=#{rspec_options.inspect}\n" \
|
|
188
|
-
"[henitai-debug-child] rspec_argv=#{rspec_argv.inspect}"
|
|
189
|
-
)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def debug_child_rspec_exit(status)
|
|
193
|
-
return unless debug_child?
|
|
194
|
-
|
|
195
|
-
debug_child_puts("[henitai-debug-child] RSpec result=#{status.inspect}")
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def suppress_simplecov!
|
|
199
|
-
CoverageRuntimeSuppressors.suppress_simplecov!
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def suppress_coverage!
|
|
203
|
-
CoverageRuntimeSuppressors.suppress_coverage!
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def debug_child_example_count(stage) # steep:ignore Ruby::UndeclaredMethodDefinition
|
|
207
|
-
return unless debug_child?
|
|
208
|
-
|
|
209
|
-
count = rspec_world_example_count
|
|
210
|
-
debug_child_puts(
|
|
211
|
-
"[henitai-debug-child] rspec_world_example_count_#{stage}=#{count.inspect}"
|
|
212
|
-
)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def debug_child_activation_start(mutant_id)
|
|
216
|
-
return unless debug_child?
|
|
217
|
-
|
|
218
|
-
debug_child_puts("[henitai-debug-child] activate_start mutant=#{mutant_id}")
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def debug_child_activation_end(activation_result, test_files:)
|
|
222
|
-
return unless debug_child?
|
|
223
|
-
|
|
224
|
-
debug_child_puts(
|
|
225
|
-
"[henitai-debug-child] activate_end result=#{activation_result.inspect}\n" \
|
|
226
|
-
"[henitai-debug-child] run_tests_start test_files=#{test_files.inspect}"
|
|
227
|
-
)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def debug_child_mutant_meta(mutant)
|
|
231
|
-
stable_id = mutant.respond_to?(:stable_id) ? mutant.stable_id : nil
|
|
232
|
-
operator = mutant.respond_to?(:operator) ? mutant.operator : nil
|
|
233
|
-
has_subject_expression =
|
|
234
|
-
mutant.respond_to?(:subject) && mutant.subject.respond_to?(:expression)
|
|
235
|
-
subject_expression = has_subject_expression ? mutant.subject.expression : nil
|
|
236
|
-
location = mutant.respond_to?(:location) ? mutant.location.inspect : nil
|
|
237
|
-
|
|
238
|
-
debug_child_puts(
|
|
239
|
-
"[henitai-debug-child] mutant_meta stableId=#{stable_id}\n" \
|
|
240
|
-
"[henitai-debug-child] mutant_meta operator=#{operator}\n" \
|
|
241
|
-
"[henitai-debug-child] mutant_meta subject=#{subject_expression}\n" \
|
|
242
|
-
"[henitai-debug-child] mutant_meta location=#{location}\n"
|
|
243
|
-
)
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def debug_child_activation_check
|
|
247
|
-
location = begin
|
|
248
|
-
Henitai::Runner.instance_method(:resolve_subjects).source_location&.join(":")
|
|
249
|
-
rescue StandardError
|
|
250
|
-
nil
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
debug_child_puts(
|
|
254
|
-
"[henitai-debug-child] activation_check resolve_subjects_location=#{location}\n"
|
|
255
|
-
)
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def loaded_feature_map(test_files) = test_files.map { |file| [file, loaded_feature?(file)] } # steep:ignore Ruby::UndeclaredMethodDefinition
|
|
259
|
-
|
|
260
|
-
def loaded_feature?(file) # steep:ignore Ruby::UndeclaredMethodDefinition
|
|
261
|
-
expanded = File.expand_path(file)
|
|
262
|
-
candidates = [expanded, "#{expanded}.rb", file, "#{file}.rb"].uniq
|
|
263
|
-
$LOADED_FEATURES.any? do |feature|
|
|
264
|
-
normalized = begin
|
|
265
|
-
File.expand_path(feature)
|
|
266
|
-
rescue StandardError
|
|
267
|
-
feature
|
|
268
|
-
end
|
|
269
|
-
candidates.include?(feature) || candidates.include?(normalized)
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def rspec_world_example_count # steep:ignore Ruby::UndeclaredMethodDefinition
|
|
274
|
-
world = ::RSpec.__send__(:world)
|
|
275
|
-
world.example_count
|
|
276
|
-
rescue StandardError
|
|
277
|
-
nil
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def debug_child_timeout_dump(pid)
|
|
281
|
-
return unless debug_child?
|
|
282
|
-
|
|
283
|
-
debug_child_puts("[henitai-debug-child] timeout_signal_sent pid=#{pid}")
|
|
284
|
-
Process.kill(:USR1, pid)
|
|
285
|
-
pause(0.2)
|
|
286
|
-
rescue Errno::ESRCH
|
|
287
|
-
nil
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def install_debug_timeout_trap
|
|
291
|
-
Signal.trap("USR1") { debug_child_thread_dump("timeout") }
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def debug_child_thread_dump(reason)
|
|
295
|
-
return unless debug_child?
|
|
296
|
-
|
|
297
|
-
debug_child_puts("[henitai-debug-child] thread_dump reason=#{reason}")
|
|
298
|
-
Thread.list.each_with_index do |thread, index|
|
|
299
|
-
debug_child_puts(
|
|
300
|
-
"[henitai-debug-child] thread index=#{index} id=#{thread.object_id} " \
|
|
301
|
-
"status=#{thread.status.inspect}"
|
|
302
|
-
)
|
|
303
|
-
Array(thread.backtrace).each do |line|
|
|
304
|
-
debug_child_puts("[henitai-debug-child] #{line}")
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
# rubocop:enable Metrics/ModuleLength
|
|
310
|
-
|
|
311
30
|
# Integration adapter for RSpec.
|
|
312
31
|
#
|
|
313
32
|
# This class exists as the stable public entry point for the RSpec
|
|
@@ -325,226 +44,16 @@ module Henitai
|
|
|
325
44
|
raise ArgumentError, "Unknown integration: #{name}. Available: #{available}"
|
|
326
45
|
end
|
|
327
46
|
|
|
328
|
-
# Base class for all integrations.
|
|
329
|
-
class Base
|
|
330
|
-
include ChildDebugSupport
|
|
331
|
-
|
|
332
|
-
# @param subject [Subject]
|
|
333
|
-
# @return [Array<String>] paths to test files that cover this subject
|
|
334
|
-
def select_tests(subject)
|
|
335
|
-
raise NotImplementedError
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
# @return [Array<String>] all test files for the configured framework
|
|
339
|
-
def test_files
|
|
340
|
-
raise NotImplementedError
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# Run test files in a child process with the mutant active.
|
|
344
|
-
#
|
|
345
|
-
# @param mutant [Mutant]
|
|
346
|
-
# @param test_files [Array<String>]
|
|
347
|
-
# @param timeout [Float] seconds
|
|
348
|
-
# @return [ScenarioExecutionResult]
|
|
349
|
-
def run_mutant(mutant:, test_files:, timeout:)
|
|
350
|
-
raise NotImplementedError
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Fork a child process for the mutant without waiting for it to finish.
|
|
354
|
-
# Returns a ChildHandle carrying the OS pid and log file paths.
|
|
355
|
-
# The caller is responsible for waiting and cleanup.
|
|
356
|
-
#
|
|
357
|
-
# @param mutant [Mutant]
|
|
358
|
-
# @param test_files [Array<String>]
|
|
359
|
-
# @return [RspecProcessRunner::ChildHandle]
|
|
360
|
-
def spawn_mutant(mutant:, test_files:)
|
|
361
|
-
raise NotImplementedError
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def per_test_coverage_supported?
|
|
365
|
-
false
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def wait_with_timeout(pid, timeout)
|
|
369
|
-
wakeup = Henitai.const_get(:ProcessWakeup).new.install
|
|
370
|
-
return Process.last_status if wait_nonblocking(pid)
|
|
371
|
-
|
|
372
|
-
wakeup.wait(timeout)
|
|
373
|
-
wakeup.drain
|
|
374
|
-
return Process.last_status if wait_nonblocking(pid)
|
|
375
|
-
return Process.last_status if wait_nonblocking(pid)
|
|
376
|
-
|
|
377
|
-
handle_timeout(pid)
|
|
378
|
-
ensure
|
|
379
|
-
wakeup&.close
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def reap_child(pid)
|
|
383
|
-
Process.wait(pid)
|
|
384
|
-
rescue Errno::ECHILD, Errno::ESRCH
|
|
385
|
-
nil
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
def cleanup_process_group(pid)
|
|
389
|
-
grace_period = 2.0
|
|
390
|
-
wakeup = Henitai.const_get(:ProcessWakeup).new.install
|
|
391
|
-
Process.kill(:SIGTERM, -pid)
|
|
392
|
-
return if wait_nonblocking(pid)
|
|
393
|
-
|
|
394
|
-
wakeup.wait(grace_period)
|
|
395
|
-
wakeup.drain
|
|
396
|
-
return if wait_nonblocking(pid)
|
|
397
|
-
|
|
398
|
-
Process.kill(:SIGKILL, -pid)
|
|
399
|
-
rescue Errno::EPERM
|
|
400
|
-
cleanup_child_process(pid)
|
|
401
|
-
rescue Errno::ESRCH
|
|
402
|
-
nil
|
|
403
|
-
ensure
|
|
404
|
-
wakeup&.close
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
private
|
|
408
|
-
|
|
409
|
-
def pause(seconds)
|
|
410
|
-
sleep(seconds)
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
def handle_timeout(pid)
|
|
414
|
-
begin
|
|
415
|
-
debug_child_timeout_dump(pid)
|
|
416
|
-
cleanup_process_group(pid)
|
|
417
|
-
ensure
|
|
418
|
-
reap_child(pid)
|
|
419
|
-
end
|
|
420
|
-
:timeout
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def cleanup_child_process(pid)
|
|
424
|
-
grace_period = 2.0
|
|
425
|
-
wakeup = Henitai.const_get(:ProcessWakeup).new.install
|
|
426
|
-
Process.kill(:SIGTERM, pid)
|
|
427
|
-
return if wait_nonblocking(pid)
|
|
428
|
-
|
|
429
|
-
wakeup.wait(grace_period)
|
|
430
|
-
wakeup.drain
|
|
431
|
-
return if wait_nonblocking(pid)
|
|
432
|
-
|
|
433
|
-
Process.kill(:SIGKILL, pid)
|
|
434
|
-
rescue Errno::EPERM, Errno::ESRCH
|
|
435
|
-
nil
|
|
436
|
-
ensure
|
|
437
|
-
wakeup&.close
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
def subprocess_env
|
|
441
|
-
{ "PARALLEL_WORKERS" => "1" }
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def wait_nonblocking(pid)
|
|
445
|
-
Process.wait(pid, Process::WNOHANG)
|
|
446
|
-
rescue Errno::ECHILD, Errno::ESRCH
|
|
447
|
-
nil
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
def scenario_log_support
|
|
451
|
-
@scenario_log_support ||= ScenarioLogSupport.new
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
def with_subprocess_env
|
|
455
|
-
original_env = {} # : Hash[String, String?]
|
|
456
|
-
subprocess_env.each do |key, value|
|
|
457
|
-
original_env[key] = ENV.fetch(key, nil)
|
|
458
|
-
ENV[key] = value
|
|
459
|
-
end
|
|
460
|
-
yield
|
|
461
|
-
ensure
|
|
462
|
-
restore_subprocess_env(original_env)
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def restore_subprocess_env(original_env)
|
|
466
|
-
original_env.each do |key, value|
|
|
467
|
-
if value.nil?
|
|
468
|
-
ENV.delete(key)
|
|
469
|
-
else
|
|
470
|
-
ENV[key] = value
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
def with_non_interactive_stdin
|
|
476
|
-
original_stdin = $stdin
|
|
477
|
-
$stdin = StringIO.new
|
|
478
|
-
yield
|
|
479
|
-
ensure
|
|
480
|
-
$stdin = original_stdin
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
def run_tests(test_files)
|
|
484
|
-
require "rspec/core"
|
|
485
|
-
::RSpec.__send__(:configuration).fail_if_no_examples = true
|
|
486
|
-
debug_child_rspec_trace(test_files:, rspec_options: [], rspec_argv: test_files)
|
|
487
|
-
debug_child_example_count("before_run") # steep:ignore Ruby::NoMethod
|
|
488
|
-
debug_child_puts("[henitai-debug-child] runner_run_start")
|
|
489
|
-
status = run_rspec_runner(test_files)
|
|
490
|
-
debug_child_puts("[henitai-debug-child] runner_run_return status=#{status.inspect}")
|
|
491
|
-
debug_child_example_count("after_run") # steep:ignore Ruby::NoMethod
|
|
492
|
-
debug_child_rspec_exit(status)
|
|
493
|
-
return status if status.is_a?(Integer)
|
|
494
|
-
|
|
495
|
-
status == true ? 0 : 1
|
|
496
|
-
end
|
|
497
|
-
|
|
498
|
-
def run_child_activation_and_tests(mutant:, test_files:, log_paths:)
|
|
499
|
-
scenario_log_support.with_coverage_dir(mutant.id) do
|
|
500
|
-
scenario_log_support.capture_child_output(log_paths) do
|
|
501
|
-
debug_child_mutant_meta(mutant) if debug_child?
|
|
502
|
-
debug_child_activation_start(mutant.id)
|
|
503
|
-
activation_result = Mutant::Activator.activate!(mutant)
|
|
504
|
-
debug_child_activation_check if debug_child?
|
|
505
|
-
debug_child_activation_end(activation_result, test_files:)
|
|
506
|
-
activation_result == :compile_error ? 2 : run_tests(test_files)
|
|
507
|
-
end
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
47
|
# RSpec integration adapter.
|
|
513
48
|
class Rspec < Base
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
(require|require_relative)
|
|
518
|
-
\s*
|
|
519
|
-
(?:\(\s*)?
|
|
520
|
-
["']([^"']+)["']
|
|
521
|
-
\s*\)?
|
|
522
|
-
/x
|
|
49
|
+
include MutantRunSupport
|
|
50
|
+
include RspecChildRunner
|
|
51
|
+
include RspecTestSelection
|
|
523
52
|
|
|
524
|
-
|
|
525
|
-
matches = spec_files.select do |path|
|
|
526
|
-
content = File.read(path)
|
|
527
|
-
selection_patterns(subject).any? { |pattern| content.include?(pattern) }
|
|
528
|
-
rescue StandardError
|
|
529
|
-
false
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
return matches unless matches.empty?
|
|
533
|
-
|
|
534
|
-
fallback_spec_files(subject)
|
|
535
|
-
end
|
|
53
|
+
DEFAULT_SUITE_TIMEOUT = 300.0
|
|
536
54
|
|
|
537
55
|
def test_files = spec_files
|
|
538
56
|
|
|
539
|
-
def spawn_mutant(mutant:, test_files:)
|
|
540
|
-
log_paths = scenario_log_paths(mutant_log_name(mutant))
|
|
541
|
-
RspecProcessRunner.new.spawn_mutant(self, mutant:, test_files:, log_paths:)
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
def run_mutant(mutant:, test_files:, timeout:)
|
|
545
|
-
RspecProcessRunner.new.run_mutant(self, mutant:, test_files:, timeout:)
|
|
546
|
-
end
|
|
547
|
-
|
|
548
57
|
def per_test_coverage_supported?
|
|
549
58
|
true
|
|
550
59
|
end
|
|
@@ -583,130 +92,6 @@ module Henitai
|
|
|
583
92
|
RUBY
|
|
584
93
|
end
|
|
585
94
|
|
|
586
|
-
def scenario_log_paths(name)
|
|
587
|
-
reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
|
|
588
|
-
log_dir = File.join(reports_dir, "mutation-logs")
|
|
589
|
-
{
|
|
590
|
-
stdout_path: File.join(log_dir, "#{name}.stdout.log"),
|
|
591
|
-
stderr_path: File.join(log_dir, "#{name}.stderr.log"),
|
|
592
|
-
log_path: File.join(log_dir, "#{name}.log")
|
|
593
|
-
}
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
def build_result(wait_result, log_paths)
|
|
597
|
-
stdout = read_log_file(log_paths[:stdout_path])
|
|
598
|
-
stderr = read_log_file(log_paths[:stderr_path])
|
|
599
|
-
write_combined_log(log_paths[:log_path], stdout, stderr)
|
|
600
|
-
|
|
601
|
-
ScenarioExecutionResult.build(
|
|
602
|
-
wait_result:,
|
|
603
|
-
stdout:,
|
|
604
|
-
stderr:,
|
|
605
|
-
log_path: log_paths[:log_path]
|
|
606
|
-
)
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
def spec_files
|
|
610
|
-
@spec_files ||= begin
|
|
611
|
-
paths = Dir.glob("spec/**/*_spec.rb")
|
|
612
|
-
paths - excluded_spec_files
|
|
613
|
-
end
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
def fallback_spec_files(subject)
|
|
617
|
-
return [] unless subject.source_file
|
|
618
|
-
|
|
619
|
-
matches = spec_files.select do |path|
|
|
620
|
-
requires_source_file_transitively?(path, subject.source_file)
|
|
621
|
-
rescue StandardError
|
|
622
|
-
false
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
matches.empty? ? spec_files : matches
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
def excluded_spec_files
|
|
629
|
-
@excluded_spec_files ||= rspec_exclude_patterns.flat_map { |pattern| Dir.glob(pattern) }.uniq
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
def rspec_exclude_patterns
|
|
633
|
-
rspec_config_lines.filter_map do |line|
|
|
634
|
-
line[/\A--exclude-pattern\s+(.+)\z/, 1]
|
|
635
|
-
end
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
def rspec_config_lines
|
|
639
|
-
return [] unless File.exist?(rspec_config_path)
|
|
640
|
-
|
|
641
|
-
File.readlines(rspec_config_path, chomp: true).map(&:strip)
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
def rspec_config_path
|
|
645
|
-
".rspec"
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
def selection_patterns(subject)
|
|
649
|
-
[
|
|
650
|
-
subject.expression,
|
|
651
|
-
subject.namespace
|
|
652
|
-
].compact.uniq.sort_by(&:length).reverse
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
def requires_source_file?(spec_file, source_file)
|
|
656
|
-
content = File.read(spec_file)
|
|
657
|
-
basename = File.basename(source_file, ".rb")
|
|
658
|
-
content.include?(basename) || content.include?(source_file)
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
def requires_source_file_transitively?(spec_file, source_file, visited = [])
|
|
662
|
-
normalized_spec_file = File.expand_path(spec_file)
|
|
663
|
-
return false if visited.include?(normalized_spec_file)
|
|
664
|
-
|
|
665
|
-
visited << normalized_spec_file
|
|
666
|
-
return true if requires_source_file?(spec_file, source_file)
|
|
667
|
-
|
|
668
|
-
required_files(spec_file).any? do |required_file|
|
|
669
|
-
requires_source_file_transitively?(required_file, source_file, visited)
|
|
670
|
-
end
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
def required_files(spec_file)
|
|
674
|
-
File.read(spec_file).lines.filter_map do |line|
|
|
675
|
-
match = line.match(REQUIRE_DIRECTIVE_PATTERN)
|
|
676
|
-
next unless match
|
|
677
|
-
|
|
678
|
-
resolve_required_file(spec_file, match[1].to_s, match[2].to_s)
|
|
679
|
-
end
|
|
680
|
-
end
|
|
681
|
-
|
|
682
|
-
def resolve_required_file(spec_file, method_name, required_path)
|
|
683
|
-
candidates =
|
|
684
|
-
if method_name == "require_relative"
|
|
685
|
-
relative_candidates(spec_file, required_path)
|
|
686
|
-
else
|
|
687
|
-
require_candidates(spec_file, required_path)
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
candidates.find { |candidate| File.file?(candidate) }
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
def relative_candidates(spec_file, required_path)
|
|
694
|
-
expand_candidates(File.dirname(spec_file), required_path)
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
def require_candidates(spec_file, required_path)
|
|
698
|
-
([File.dirname(spec_file), Dir.pwd] + $LOAD_PATH).flat_map do |base_path|
|
|
699
|
-
expand_candidates(base_path, required_path)
|
|
700
|
-
end
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
def expand_candidates(base_path, required_path)
|
|
704
|
-
[
|
|
705
|
-
File.expand_path(required_path, base_path),
|
|
706
|
-
File.expand_path("#{required_path}.rb", base_path)
|
|
707
|
-
].uniq
|
|
708
|
-
end
|
|
709
|
-
|
|
710
95
|
def spawn_suite_process(test_files, log_paths)
|
|
711
96
|
File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
712
97
|
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
@@ -721,233 +106,24 @@ module Henitai
|
|
|
721
106
|
end
|
|
722
107
|
end
|
|
723
108
|
|
|
724
|
-
def run_in_child(mutant:, test_files:, log_paths:)
|
|
725
|
-
Thread.report_on_exception = false
|
|
726
|
-
with_subprocess_env do
|
|
727
|
-
suppress_simplecov!
|
|
728
|
-
suppress_coverage!
|
|
729
|
-
install_debug_timeout_trap if debug_child?
|
|
730
|
-
with_non_interactive_stdin do
|
|
731
|
-
run_child_activation_and_tests(mutant:, test_files:, log_paths:)
|
|
732
|
-
end
|
|
733
|
-
end
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
def mutant_log_name(mutant)
|
|
737
|
-
"mutant-#{mutant.id}"
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
def read_log_file(path)
|
|
741
|
-
return "" unless File.exist?(path)
|
|
742
|
-
|
|
743
|
-
File.read(path)
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
def write_combined_log(path, stdout, stderr)
|
|
747
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
748
|
-
File.write(path, combined_log(stdout, stderr))
|
|
749
|
-
end
|
|
750
|
-
|
|
751
|
-
def combined_log(stdout, stderr)
|
|
752
|
-
[
|
|
753
|
-
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
754
|
-
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
755
|
-
].compact.join("\n")
|
|
756
|
-
end
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
# Stores the child-process log helpers shared by the integration specs.
|
|
760
|
-
class ScenarioLogSupport
|
|
761
|
-
def read_log_file(path)
|
|
762
|
-
return "" unless File.exist?(path)
|
|
763
|
-
|
|
764
|
-
File.read(path)
|
|
765
|
-
end
|
|
766
|
-
|
|
767
|
-
def write_combined_log(path, stdout, stderr)
|
|
768
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
769
|
-
File.write(path, combined_log(stdout, stderr))
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
def combined_log(stdout, stderr)
|
|
773
|
-
[
|
|
774
|
-
(stdout.empty? ? nil : "stdout:\n#{stdout}"),
|
|
775
|
-
(stderr.empty? ? nil : "stderr:\n#{stderr}")
|
|
776
|
-
].compact.join("\n")
|
|
777
|
-
end
|
|
778
|
-
end
|
|
779
|
-
|
|
780
|
-
# Prepended onto SimpleCov's singleton class to turn start into a no-op
|
|
781
|
-
# during mutant child runs. Using prepend avoids "method redefined" warnings.
|
|
782
|
-
module SimpleCovStartSuppressor
|
|
783
|
-
def start(*_args) = nil
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
# Suppresses expensive and irrelevant coverage startup/teardown during
|
|
787
|
-
# mutant child runs. Coverage artifacts are only required during the
|
|
788
|
-
# dedicated bootstrap phase.
|
|
789
|
-
module CoverageRuntimeSuppressors
|
|
790
|
-
def self.suppress_simplecov!
|
|
791
|
-
require "simplecov"
|
|
792
|
-
sc = Object.const_get(:SimpleCov) # steep:ignore Ruby::UnknownConstant
|
|
793
|
-
sc.external_at_exit = true if sc.respond_to?(:external_at_exit=)
|
|
794
|
-
return if sc.singleton_class.ancestors.include?(SimpleCovStartSuppressor)
|
|
795
|
-
|
|
796
|
-
sc.singleton_class.prepend(SimpleCovStartSuppressor)
|
|
797
|
-
rescue LoadError, NameError
|
|
798
|
-
nil
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
def self.suppress_coverage!
|
|
802
|
-
require "coverage"
|
|
803
|
-
cov = Object.const_get(:Coverage) # steep:ignore Ruby::UnknownConstant
|
|
804
|
-
return if cov.singleton_class.ancestors.include?(CoverageStartSuppressor)
|
|
805
|
-
|
|
806
|
-
cov.singleton_class.prepend(CoverageStartSuppressor)
|
|
807
|
-
rescue LoadError, NameError
|
|
808
|
-
nil
|
|
809
|
-
end
|
|
810
|
-
end
|
|
811
|
-
|
|
812
|
-
# Prepended onto the coverage gem's Coverage singleton to turn start
|
|
813
|
-
# into a no-op during mutant child runs.
|
|
814
|
-
module CoverageStartSuppressor
|
|
815
|
-
def start(*_args) = nil
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
# Minitest integration adapter.
|
|
819
|
-
#
|
|
820
|
-
# Coverage formatter injection remains implemented in the RSpec child
|
|
821
|
-
# runner. Minitest shares selection and execution semantics, but per-test
|
|
822
|
-
# coverage collection is not yet wired into this path.
|
|
823
|
-
class Minitest < Rspec
|
|
824
|
-
def per_test_coverage_supported?
|
|
825
|
-
true
|
|
826
|
-
end
|
|
827
|
-
|
|
828
|
-
def run_mutant(mutant:, test_files:, timeout:)
|
|
829
|
-
setup_load_path
|
|
830
|
-
super
|
|
831
|
-
end
|
|
832
|
-
|
|
833
|
-
def spawn_mutant(mutant:, test_files:)
|
|
834
|
-
setup_load_path
|
|
835
|
-
super
|
|
836
|
-
end
|
|
837
|
-
|
|
838
|
-
def run_in_child(mutant:, test_files:, log_paths:)
|
|
839
|
-
ENV["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
840
|
-
preload_environment
|
|
841
|
-
super
|
|
842
|
-
end
|
|
843
|
-
|
|
844
|
-
def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
|
|
845
|
-
log_paths = scenario_log_paths("baseline")
|
|
846
|
-
pid = spawn_suite_process(test_files, log_paths)
|
|
847
|
-
wait_result = wait_with_timeout(pid, timeout)
|
|
848
|
-
build_result(wait_result, log_paths)
|
|
849
|
-
ensure
|
|
850
|
-
cleanup_suite_process(pid, wait_result)
|
|
851
|
-
end
|
|
852
|
-
|
|
853
109
|
private
|
|
854
110
|
|
|
855
|
-
def suite_command(test_files)
|
|
856
|
-
["bundle", "exec", "ruby", "-I", "test",
|
|
857
|
-
"-r", "henitai/minitest_simplecov",
|
|
858
|
-
"-r", "henitai/minitest_coverage_hook",
|
|
859
|
-
"-e", "ARGV.each { |f| require File.expand_path(f) }",
|
|
860
|
-
*test_files]
|
|
861
|
-
end
|
|
862
|
-
|
|
863
111
|
def run_tests(test_files)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
test_files
|
|
867
|
-
#
|
|
868
|
-
|
|
869
|
-
status =
|
|
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)
|
|
870
121
|
return status if status.is_a?(Integer)
|
|
871
122
|
|
|
872
123
|
status == true ? 0 : 1
|
|
873
124
|
end
|
|
874
|
-
|
|
875
|
-
def preload_environment
|
|
876
|
-
env_file = File.expand_path("config/environment.rb")
|
|
877
|
-
require env_file if File.exist?(env_file)
|
|
878
|
-
end
|
|
879
|
-
|
|
880
|
-
def setup_load_path
|
|
881
|
-
test_dir = File.expand_path("test")
|
|
882
|
-
$LOAD_PATH.unshift(test_dir) unless $LOAD_PATH.include?(test_dir)
|
|
883
|
-
end
|
|
884
|
-
|
|
885
|
-
def suppress_minitest_autorun!
|
|
886
|
-
require "minitest"
|
|
887
|
-
singleton_class = ::Minitest.singleton_class
|
|
888
|
-
suppressor = @minitest_autorun_suppressor ||= Module.new.tap do |mod|
|
|
889
|
-
mod.define_method(:autorun) { nil }
|
|
890
|
-
end
|
|
891
|
-
return if singleton_class.ancestors.include?(suppressor)
|
|
892
|
-
|
|
893
|
-
singleton_class.prepend(suppressor)
|
|
894
|
-
nil
|
|
895
|
-
rescue LoadError, NameError
|
|
896
|
-
nil
|
|
897
|
-
end
|
|
898
|
-
|
|
899
|
-
def suppress_simplecov!
|
|
900
|
-
require "simplecov"
|
|
901
|
-
sc = Object.const_get(:SimpleCov) # steep:ignore Ruby::UnknownConstant
|
|
902
|
-
return if sc.singleton_class.ancestors.include?(SimpleCovStartSuppressor)
|
|
903
|
-
|
|
904
|
-
sc.singleton_class.prepend(SimpleCovStartSuppressor)
|
|
905
|
-
rescue LoadError, NameError
|
|
906
|
-
nil
|
|
907
|
-
end
|
|
908
|
-
|
|
909
|
-
def suppress_coverage!
|
|
910
|
-
require "coverage"
|
|
911
|
-
cov = Object.const_get(:Coverage) # steep:ignore Ruby::UnknownConstant
|
|
912
|
-
return if cov.singleton_class.ancestors.include?(CoverageStartSuppressor)
|
|
913
|
-
|
|
914
|
-
cov.singleton_class.prepend(CoverageStartSuppressor)
|
|
915
|
-
rescue LoadError, NameError
|
|
916
|
-
nil
|
|
917
|
-
end
|
|
918
|
-
|
|
919
|
-
def subprocess_env
|
|
920
|
-
env = super
|
|
921
|
-
env["RAILS_ENV"] = "test" unless ENV["RAILS_ENV"] == "test"
|
|
922
|
-
env["PARALLEL_WORKERS"] = "1"
|
|
923
|
-
env
|
|
924
|
-
end
|
|
925
|
-
|
|
926
|
-
def spawn_suite_process(test_files, log_paths)
|
|
927
|
-
FileUtils.mkdir_p(File.dirname(log_paths[:stdout_path]))
|
|
928
|
-
File.open(log_paths[:stdout_path], "w") do |stdout_file|
|
|
929
|
-
File.open(log_paths[:stderr_path], "w") do |stderr_file|
|
|
930
|
-
Process.spawn(
|
|
931
|
-
subprocess_env,
|
|
932
|
-
*suite_command(test_files),
|
|
933
|
-
out: stdout_file,
|
|
934
|
-
err: stderr_file
|
|
935
|
-
)
|
|
936
|
-
end
|
|
937
|
-
end
|
|
938
|
-
end
|
|
939
|
-
|
|
940
|
-
def cleanup_suite_process(pid, wait_result)
|
|
941
|
-
return unless pid
|
|
942
|
-
|
|
943
|
-
cleanup_child_process(pid)
|
|
944
|
-
reap_child(pid) if wait_result.nil?
|
|
945
|
-
end
|
|
946
|
-
|
|
947
|
-
def spec_files
|
|
948
|
-
(Dir.glob("test/**/*_test.rb") + Dir.glob("test/**/*_spec.rb"))
|
|
949
|
-
.reject { |f| f.start_with?("test/system/") }
|
|
950
|
-
end
|
|
951
125
|
end
|
|
952
126
|
end
|
|
953
127
|
end
|
|
128
|
+
|
|
129
|
+
require_relative "integration/minitest"
|