Class: Henitai::Integration::ScenarioLogSupport

Inherits:
Object
  • Object
show all
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

Instance Method Details

#build_child_output_filesHash[Symbol, untyped]

Parameters:

  • (Hash[Symbol, String])

Returns:

  • (Hash[Symbol, untyped])


436
# File 'sig/henitai.rbs', line 436

def build_child_output_files: (Hash[Symbol, String]) -> Hash[Symbol, untyped]

#capture_child_output(log_paths) { ... } ⇒ Object

Parameters:

  • (Hash[Symbol, String])

Yields:

Yield Returns:

  • (Object)

Returns:

  • (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.

Parameters:

  • (Hash[Symbol, untyped], nil)


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_filesvoid

This method returns an undefined value.

Parameters:

  • (Hash[Symbol, untyped])


441
# File 'sig/henitai.rbs', line 441

def close_child_output_files: (Hash[Symbol, untyped]) -> void

#combined_log(stdout, stderr) ⇒ String

Parameters:

  • (String)
  • (String)

Returns:

  • (String)


137
138
139
140
141
142
# File 'lib/henitai/integration/scenario_log_support.rb', line 137

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



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/henitai/integration/scenario_log_support.rb', line 73

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



93
94
95
96
97
98
99
100
# File 'lib/henitai/integration/scenario_log_support.rb', line 93

def finish_capped_stream(stream)
  return unless stream

  stream[:writer].close
  stream[:thread].join
  stream[:reader].close
  stream[:file].close
end

#max_log_bytesObject



120
121
122
123
124
# File 'lib/henitai/integration/scenario_log_support.rb', line 120

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

Parameters:

  • (String)

Returns:

  • (String)


146
147
148
149
# File 'lib/henitai/integration/scenario_log_support.rb', line 146

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.



65
66
67
68
69
70
71
# File 'lib/henitai/integration/scenario_log_support.rb', line 65

def open_capped_stream(path, cap)
  file = File.new(path, "w")
  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.

Parameters:

  • (Hash[Symbol, String])

Returns:

  • (Hash[Symbol, untyped])


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

Parameters:

  • (String)

Returns:

  • (String)


126
127
128
129
130
# File 'lib/henitai/integration/scenario_log_support.rb', line 126

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.

Parameters:

  • (Hash[Symbol, untyped])


102
103
104
105
106
107
# File 'lib/henitai/integration/scenario_log_support.rb', line 102

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.

Parameters:

  • (Object)
  • (Object)


116
117
118
# File 'lib/henitai/integration/scenario_log_support.rb', line 116

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.

Parameters:

  • (Hash[Symbol, untyped])


109
110
111
112
113
114
# File 'lib/henitai/integration/scenario_log_support.rb', line 109

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_streamIO

Returns:

  • (IO)


155
156
157
# File 'lib/henitai/integration/scenario_log_support.rb', line 155

def stderr_stream
  @stderr_stream ||= IO.for_fd(2)
end

#stdout_streamIO

Returns:

  • (IO)


151
152
153
# File 'lib/henitai/integration/scenario_log_support.rb', line 151

def stdout_stream
  @stdout_stream ||= IO.for_fd(1)
end

#sync_child_output_filesvoid

This method returns an undefined value.

Parameters:

  • (Hash[Symbol, untyped])


437
# File 'sig/henitai.rbs', line 437

def sync_child_output_files: (Hash[Symbol, untyped]) -> void

#with_coverage_dir(mutant_id) { ... } ⇒ Object

Parameters:

  • (String)

Yields:

Yield Returns:

  • (Object)

Returns:

  • (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.

Parameters:

  • (String)
  • (String)
  • (String)


132
133
134
135
# File 'lib/henitai/integration/scenario_log_support.rb', line 132

def write_combined_log(path, stdout, stderr)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, combined_log(stdout, stderr))
end