Files
brew/Library/Homebrew/test/utils/fork_spec.rb
T
Mike McQuaid b859f85125 Preserve build errors with Pathname arguments
- Stringify `BuildError` commands and arguments so JSON reporting
  preserves the original failure when formulae pass `Pathname`s.
- Assert reconstructed fields so error equality cannot hide data loss.

Fixes #24291
2026-10-17 20:06:30 +01:00

169 lines
5.3 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "system_command"
require "utils/fork"
require "timeout"
RSpec.describe Utils do
describe "::child_error_hash" do
it "preserves build error details" do
error = BuildError.new(nil, "make", ["install"], { "PATH" => "/bin" })
expect(described_class.child_error_hash(error)).to include(
"cmd" => "make", "args" => ["install"], "env" => { "PATH" => "/bin" },
)
end
it "serialises the class, message and backtrace that the parent reads back" do
error = RuntimeError.new("child failed")
error.set_backtrace ["/some/file.rb:1:in 'block'"]
expect(described_class.child_error_hash(error)).to eq(
"json_class" => "RuntimeError",
"m" => "child failed",
"b" => ["/some/file.rb:1:in 'block'"],
)
end
end
describe "::report_forked_child_error" do
it "preserves a BuildError with Pathname commands and arguments" do
error = BuildError.new(nil, Pathname("/bin/false"), ["arg1", 2, Pathname("arg 3"), :arg4], { "PATH" => "/bin" })
error.set_backtrace ["/some/file.rb:1:in 'block'"]
IO.pipe do |reader, writer|
described_class.report_forked_child_error(writer, error)
expect(described_class.rewrite_child_error(JSON.parse(reader.read))).to have_attributes(
class: BuildError,
cmd: "/bin/false",
args: ["arg1", "2", "arg 3", "arg4"],
env: { "PATH" => "/bin" },
message: error.message,
backtrace: error.backtrace,
)
end
end
end
describe "#safe_fork" do
it "reports only the parent error when the child reports a failure after disconnection" do
handler = proc do |_message|
raise Errno::EACCES, "/usr/local/bin/kubectl"
end
expect do
Timeout.timeout(10) do
described_class.safe_fork(child_message_handler: handler) do
exec RUBY_PATH.to_s, "-I", HOMEBREW_LIBRARY_PATH.to_s, "-rglobal", "-rutils/fork", "-e", <<~RUBY
channel = Utils.forked_child_channel
channel.puts "privileged step"
channel.flush
exit! 0 if channel.gets
Utils.report_forked_child_error(channel, RuntimeError.new("parent step failed"))
exit! 1
RUBY
end
end
rescue Errno::EACCES => e
warn e.message
end.to output("Permission denied - /usr/local/bin/kubectl\n").to_stderr_from_any_process
end
it "preserves a parent failure while the child error pipe is open" do
IO.pipe do |ready_read, ready_write|
expect do
described_class.safe_fork(yield_parent: true) do |error_pipe|
if error_pipe
ready_read.close
ready_write.puts "ready"
ready_write.close
else
ready_write.close
ready_read.gets
raise "parent failed"
end
end
end.to raise_error(RuntimeError, "parent failed")
end
end
it "responds to messages from the forked child" do
messages = []
handler = proc do |message|
messages << message.chomp
"acknowledged"
end
described_class.safe_fork(child_message_handler: handler) do
channel = described_class.forked_child_channel
channel.puts "privileged step"
channel.flush
raise "parent did not acknowledge child message" if channel.gets != "acknowledged\n"
channel.close
end
expect(messages).to eq(["privileged step"])
end
it "interrupts a child waiting for a parent response without deadlocking" do
expect do
Timeout.timeout(2) do
handler = proc do |_message|
raise Interrupt
end
described_class.safe_fork(child_message_handler: handler) do
channel = described_class.forked_child_channel
channel.puts "privileged step"
channel.flush
channel.gets
channel.close
end
end
end.to raise_error(Interrupt)
end
it "reaps the child when a parent message handler raises" do
child_pid = T.let(nil, T.nilable(Integer))
handler = proc do |message|
child_pid = message.to_i
raise "parent message handler failed"
end
expect do
described_class.safe_fork(child_message_handler: handler) do
channel = described_class.forked_child_channel
channel.puts Process.pid.to_s
channel.flush
channel.close
end
end.to raise_error(RuntimeError, "parent message handler failed")
pid = child_pid
raise "child did not send its process ID" unless pid
expect { Process.waitpid(pid) }.to raise_error(Errno::ECHILD)
end
it "raises a RuntimeError on an error that isn't ErrorDuringExecution" do
expect do
described_class.safe_fork do
raise "this is an exception in the child"
end
end.to raise_error(RuntimeError)
end
it "raises an ErrorDuringExecution on one in the child" do
expect do
described_class.safe_fork do
SystemCommand.safe_system "/usr/bin/false"
end
end.to raise_error(ErrorDuringExecution)
end
end
end