Skip to content

Conversation

@cho-m
Copy link
Member

@cho-m cho-m commented Dec 30, 2025

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) with your changes locally?

To simplify some PTY usage in Homebrew/core.

Defaulting winsize to 80x24 as traditional default (also used by Terminal.app).

Copilot AI review requested due to automatic review settings December 30, 2025 00:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new pty_spawn_output method to the Homebrew::Assertions module to simplify PTY (pseudo-terminal) usage in Homebrew/core. The method spawns commands in a PTY environment, which is useful for testing interactive applications that behave differently when not connected to a terminal.

  • Adds pty_spawn_output method with support for window sizing, stdin data, timeouts, and exit status assertions
  • Provides comprehensive test coverage for the core functionality
  • Uses traditional 80x24 terminal dimensions as the default

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
Library/Homebrew/formula_assertions.rb Implements the pty_spawn_output method with PTY spawning, thread-based output capture, configurable window size, stdin handling, and timeout support
Library/Homebrew/test/formula_assertions_spec.rb Adds RSpec tests verifying exit code assertions, window size configuration, and timeout behavior

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +111 to +117
Timeout.timeout(timeout) { stdout_thread.join }
rescue Timeout::Error
# Ignore error and let ensure clean up
ensure
stdout.close
stdin.close
Process.wait(pid)
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling Process.wait(pid) without ensuring the process has terminated could cause the method to hang. If the timeout fires but the process is still running, consider using Process.kill to terminate the process before waiting, or use Process.wait with the Process::WNOHANG flag to avoid blocking.

Suggested change
Timeout.timeout(timeout) { stdout_thread.join }
rescue Timeout::Error
# Ignore error and let ensure clean up
ensure
stdout.close
stdin.close
Process.wait(pid)
begin
Timeout.timeout(timeout) { stdout_thread.join }
rescue Timeout::Error
# Ignore error and let ensure clean up, but make sure the process is signaled
begin
Process.kill("TERM", pid)
rescue Errno::ESRCH
# Process already exited
end
ensure
stdout.close
stdin.close
begin
Process.wait(pid)
rescue Errno::ECHILD
# Process already reaped
end
end

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +26
describe "#pty_spawn_output" do
it "can verify the return code" do
expect { pty_spawn_output("true", 0) }.not_to raise_error
expect { pty_spawn_output("true", 1) }.to raise_error(Minitest::Assertion)
expect { pty_spawn_output("false", 0) }.to raise_error(Minitest::Assertion)
expect { pty_spawn_output("false", 1) }.not_to raise_error
end

it "can adjust the window size" do
expect(pty_spawn_output("stty size", winsize: [10, 20]).chomp).to eql("10 20")
end

it "can terminate long running commands" do
time = Benchmark.measure do
expect { pty_spawn_output("sleep 10", timeout: 0.5) }.not_to raise_error
end
expect(time.real).to be < 2 # allow some overhead in case of slow tests
end
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stdin_data and stdin_delay parameters are not covered by any tests. Consider adding test cases that verify stdin data is correctly sent to the spawned process and that the delay works as expected.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +116
stdout.close
stdin.close
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ensure block closes stdout before ensuring the stdout_thread has terminated. If the timeout fires while the thread is still reading from stdout, closing it could cause race conditions. The thread should be explicitly terminated or joined before closing the streams to ensure clean shutdown.

Suggested change
stdout.close
stdin.close
# Ensure the stdout_thread is not reading when closing the streams.
if defined?(stdout_thread) && stdout_thread&.alive?
stdout_thread.kill
stdout_thread.join
end
begin
stdout.close
rescue IOError
# stdout may already be closed
end
begin
stdin.close
rescue IOError
# stdin may already be closed
end

Copilot uses AI. Check for mistakes.

if stdin_data
sleep(stdin_delay)
stdin.write(stdin_data)
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing to stdin without handling potential exceptions could cause the method to fail unexpectedly. If the child process closes its stdin or terminates before the write completes, this could raise Errno::EPIPE or IOError. Consider wrapping the write operation in a rescue block to handle these cases gracefully.

Suggested change
stdin.write(stdin_data)
begin
stdin.write(stdin_data)
rescue Errno::EPIPE, IOError
# Ignore errors when the child process closes stdin early.
end

Copilot uses AI. Check for mistakes.
@cho-m cho-m marked this pull request as draft December 30, 2025 00:45
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Member

@p-linnane p-linnane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome. Usually these tests can be a pain, so would love to see them simplified like this.

Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! Can you open a homebrew/core PR using this (which will obviously be 🔴 for now) just to see how it'll look there?

@cho-m
Copy link
Member Author

cho-m commented Dec 30, 2025

Great idea! Can you open a homebrew/core PR using this (which will obviously be 🔴 for now) just to see how it'll look there?


Still need to figure out why the stty size is not working on Linux. And check on some exception handling mentioned by Copilot.

@SMillerDev
Copy link
Member

This is a great addition, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants