-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
formula_assertions: add pty_spawn_output
#21340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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_outputmethod 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.
| Timeout.timeout(timeout) { stdout_thread.join } | ||
| rescue Timeout::Error | ||
| # Ignore error and let ensure clean up | ||
| ensure | ||
| stdout.close | ||
| stdin.close | ||
| Process.wait(pid) |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
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.
| 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 |
| 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 |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
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.
| stdout.close | ||
| stdin.close |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
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.
| 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 |
|
|
||
| if stdin_data | ||
| sleep(stdin_delay) | ||
| stdin.write(stdin_data) |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
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.
| stdin.write(stdin_data) | |
| begin | |
| stdin.write(stdin_data) | |
| rescue Errno::EPIPE, IOError | |
| # Ignore errors when the child process closes stdin early. | |
| end |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
p-linnane
left a comment
There was a problem hiding this 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.
MikeMcQuaid
left a comment
There was a problem hiding this 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?
Still need to figure out why the |
|
This is a great addition, thanks! |
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).