Class: Henitai::Integration::ScenarioLogSupport
- Inherits:
-
Object
- Object
- Henitai::Integration::ScenarioLogSupport
- Defined in:
- lib/henitai/integration/scenario_log_support.rb,
sig/henitai.rbs
Overview
Shared helpers for capturing stdout/stderr from child test processes and for reading and combining the captured log files afterwards.
Constant Summary collapse
- MAX_LOG_BYTES_ENV =
Env var carrying the per-stream capture cap (bytes) into forked children. Set by the execution engine from Configuration#max_log_bytes.
"HENITAI_MAX_LOG_BYTES"- DEFAULT_MAX_LOG_BYTES =
5_000_000- DRAIN_CHUNK_BYTES =
65_536
Instance Method Summary collapse
- #build_child_output_files ⇒ Hash[Symbol, untyped]
- #capture_child_output(log_paths) { ... } ⇒ Object
- #close_child_output(output_files) ⇒ void
- #close_child_output_files ⇒ void
- #combined_log(stdout, stderr) ⇒ String
- #drain_capped(reader, file, cap) ⇒ Object
- #finish_capped_stream(stream) ⇒ Object
- #max_log_bytes ⇒ Object
- #mutation_coverage_dir(mutant_id) ⇒ String
-
#open_capped_stream(path, cap) ⇒ Object
One capture channel: a pipe whose read end is drained (capped) into the log file by a dedicated thread.
-
#open_child_output(log_paths) ⇒ Hash[Symbol, untyped]
Captures via a pipe drained by a background thread that writes to the log file only up to
max_log_bytesand then discards the overflow. - #read_log_file(path) ⇒ String
- #redirect_child_output(output_files) ⇒ void
- #reopen_child_output_stream(stream, original_stream) ⇒ void
- #restore_child_output(output_files) ⇒ void
- #stderr_stream ⇒ IO
- #stdout_stream ⇒ IO
- #sync_child_output_files ⇒ void
- #with_coverage_dir(mutant_id) { ... } ⇒ Object
- #write_combined_log(path, stdout, stderr) ⇒ void
Instance Method Details
#build_child_output_files ⇒ Hash[Symbol, untyped]
438 |
# File 'sig/henitai.rbs', line 438
def build_child_output_files: (Hash[Symbol, String]) -> Hash[Symbol, untyped]
|
#capture_child_output(log_paths) { ... } ⇒ Object
16 17 18 19 20 21 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 16 def capture_child_output(log_paths) output_files = open_child_output(log_paths) yield ensure close_child_output(output_files) end |
#close_child_output(output_files) ⇒ void
This method returns an undefined value.
53 54 55 56 57 58 59 60 61 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 53 def close_child_output(output_files) return unless output_files restore_child_output(output_files) finish_capped_stream(output_files[:stdout]) finish_capped_stream(output_files[:stderr]) output_files[:original_stdout]&.close output_files[:original_stderr]&.close end |
#close_child_output_files ⇒ void
This method returns an undefined value.
443 |
# File 'sig/henitai.rbs', line 443
def close_child_output_files: (Hash[Symbol, untyped]) -> void
|
#combined_log(stdout, stderr) ⇒ String
140 141 142 143 144 145 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 140 def combined_log(stdout, stderr) [ (stdout.empty? ? nil : "stdout:\n#{stdout}"), (stderr.empty? ? nil : "stderr:\n#{stderr}") ].compact.join("\n") end |
#drain_capped(reader, file, cap) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 76 def drain_capped(reader, file, cap) written = 0 truncated = false loop do chunk = reader.readpartial(DRAIN_CHUNK_BYTES) next unless written < cap slice = chunk.byteslice(0, cap - written) file.write(slice) written += slice.bytesize next unless written >= cap && !truncated file.write("\n[henitai] output truncated at #{cap} bytes\n") truncated = true end rescue IOError # EOFError (a subclass) on writer close, or a closed pipe: draining done. nil end |
#finish_capped_stream(stream) ⇒ Object
96 97 98 99 100 101 102 103 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 96 def finish_capped_stream(stream) return unless stream stream[:writer].close stream[:thread].join stream[:reader].close stream[:file].close end |
#max_log_bytes ⇒ Object
123 124 125 126 127 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 123 def max_log_bytes raw = ENV.fetch(MAX_LOG_BYTES_ENV, nil) value = raw.to_i value.positive? ? value : DEFAULT_MAX_LOG_BYTES end |
#mutation_coverage_dir(mutant_id) ⇒ String
149 150 151 152 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 149 def mutation_coverage_dir(mutant_id) reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports") File.join(reports_dir, "mutation-coverage", mutant_id.to_s) end |
#open_capped_stream(path, cap) ⇒ Object
One capture channel: a pipe whose read end is drained (capped) into the log file by a dedicated thread. Returns the pieces close needs. Binary mode: readpartial yields ASCII-8BIT chunks, and a text-mode file would transcode them (raising Encoding::UndefinedConversionError on multibyte output when Encoding.default_internal is set, as Rails does).
68 69 70 71 72 73 74 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 68 def open_capped_stream(path, cap) file = File.new(path, "wb") file.sync = true reader, writer = IO.pipe thread = Thread.new { drain_capped(reader, file, cap) } { file:, reader:, writer:, thread: } end |
#open_child_output(log_paths) ⇒ Hash[Symbol, untyped]
Captures via a pipe drained by a background thread that writes to the
log file only up to max_log_bytes and then discards the overflow.
A runaway mutant (e.g. one that recurses henitai into itself and spews
backtraces) can otherwise fill hundreds of MB per child; draining past
the cap keeps the writer from blocking on a full pipe.
40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 40 def open_child_output(log_paths) FileUtils.mkdir_p(File.dirname(log_paths[:log_path])) cap = max_log_bytes output_files = { original_stdout: stdout_stream.dup, original_stderr: stderr_stream.dup, stdout: open_capped_stream(log_paths[:stdout_path], cap), stderr: open_capped_stream(log_paths[:stderr_path], cap) } redirect_child_output(output_files) output_files end |
#read_log_file(path) ⇒ String
129 130 131 132 133 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 129 def read_log_file(path) return "" unless File.exist?(path) File.read(path) end |
#redirect_child_output(output_files) ⇒ void
This method returns an undefined value.
105 106 107 108 109 110 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 105 def redirect_child_output(output_files) reopen_child_output_stream(stdout_stream, output_files[:stdout][:writer]) reopen_child_output_stream(stderr_stream, output_files[:stderr][:writer]) $stdout = stdout_stream $stderr = stderr_stream end |
#reopen_child_output_stream(stream, original_stream) ⇒ void
This method returns an undefined value.
119 120 121 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 119 def reopen_child_output_stream(stream, original_stream) stream.reopen(original_stream) if original_stream end |
#restore_child_output(output_files) ⇒ void
This method returns an undefined value.
112 113 114 115 116 117 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 112 def restore_child_output(output_files) reopen_child_output_stream(stdout_stream, output_files[:original_stdout]) reopen_child_output_stream(stderr_stream, output_files[:original_stderr]) $stdout = stdout_stream $stderr = stderr_stream end |
#stderr_stream ⇒ IO
158 159 160 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 158 def stderr_stream @stderr_stream ||= IO.for_fd(2) end |
#stdout_stream ⇒ IO
154 155 156 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 154 def stdout_stream @stdout_stream ||= IO.for_fd(1) end |
#sync_child_output_files ⇒ void
This method returns an undefined value.
439 |
# File 'sig/henitai.rbs', line 439
def sync_child_output_files: (Hash[Symbol, untyped]) -> void
|
#with_coverage_dir(mutant_id) { ... } ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 23 def with_coverage_dir(mutant_id) original_coverage_dir = ENV.fetch("HENITAI_COVERAGE_DIR", nil) ENV["HENITAI_COVERAGE_DIR"] = mutation_coverage_dir(mutant_id) yield ensure if original_coverage_dir.nil? ENV.delete("HENITAI_COVERAGE_DIR") else ENV["HENITAI_COVERAGE_DIR"] = original_coverage_dir end end |
#write_combined_log(path, stdout, stderr) ⇒ void
This method returns an undefined value.
135 136 137 138 |
# File 'lib/henitai/integration/scenario_log_support.rb', line 135 def write_combined_log(path, stdout, stderr) FileUtils.mkdir_p(File.dirname(path)) File.write(path, combined_log(stdout, stderr)) end |