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])


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

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])


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

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

#combined_log(stdout, stderr) ⇒ String

Parameters:

  • (String)
  • (String)

Returns:

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



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

Parameters:

  • (String)

Returns:

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

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)


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.

Parameters:

  • (Hash[Symbol, untyped])


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.

Parameters:

  • (Object)
  • (Object)


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.

Parameters:

  • (Hash[Symbol, untyped])


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_streamIO

Returns:

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

Returns:

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

This method returns an undefined value.

Parameters:

  • (Hash[Symbol, untyped])


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

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)


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