Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions Library/Homebrew/test/utils/tty_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# typed: strict
# frozen_string_literal: true

require "io/console"
require "pty"

RSpec.describe Tty do
describe "::strip_ansi" do
it "removes ANSI escape codes from a string" do
Expand Down Expand Up @@ -66,34 +69,63 @@
describe "::size" do
before do
described_class.remove_instance_variable(:@size) if described_class.instance_variable_defined?(:@size)
allow(Utils).to receive(:popen_read_text).and_raise("unexpected subprocess")
end

after do
described_class.remove_instance_variable(:@size) if described_class.instance_variable_defined?(:@size)
end

it "memoises a failed `stty size` probe instead of respawning it" do
expect(Utils).to receive(:popen_read_text).with("/bin/stty", "size", err: File::NULL).once.and_return("")
it "reads and memoises the terminal size without a subprocess" do
PTY.open do |controller, terminal|
controller.winsize = [40, 160]
$stdin.reopen(terminal)
size = described_class.size
controller.winsize = [50, 180]

expect([size, described_class.size]).to eq([[40, 160], [40, 160]])
end
end

it "returns nil when stdin is redirected" do
$stdin.reopen(File::NULL)

# We call this twice to check the failure is memoised
expect(described_class.size).to be_nil
end

it "returns nil when stdin is closed" do
original_stdin = $stdin
$stdin = $stdin.dup
$stdin.close

expect(described_class.size).to be_nil
ensure
$stdin = original_stdin
end

it "memoises a failed terminal size probe" do
allow($stdin).to receive(:tty?).and_return(true)
allow($stdin).to receive(:winsize).and_invoke(proc { raise Errno::ENOTTY }, proc { [40, 160] })

# We call this twice to check the failure is memoised
expect([described_class.size, described_class.size]).to eq([nil, nil])
end

it "does not expose an unfinished size to another thread" do
probe_started = Queue.new
release_probe = Queue.new
allow(Utils).to receive(:popen_read_text).with("/bin/stty", "size", err: File::NULL).and_invoke(
allow($stdin).to receive(:tty?).and_return(true)
allow($stdin).to receive(:winsize).and_invoke(
proc {
probe_started << true
release_probe.pop
"40 160"
[40, 160]
},
proc { "40 160" },
proc { [40, 160] },
)

probing_thread = Thread.new { described_class.size }
probe_started.pop
probe_started.pop(timeout: 5)

expect(described_class.size).to eq([40, 160])
ensure
Expand Down
7 changes: 4 additions & 3 deletions Library/Homebrew/utils/tty.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# typed: strict
# frozen_string_literal: true

require "io/console"
require "utils/popen"

# Various helper functions for interacting with TTYs.
Expand Down Expand Up @@ -141,9 +142,9 @@ def end_synchronized_update
def size
return @size if defined?(@size)

height, width = Utils.popen_read_text("/bin/stty", "size", err: File::NULL).presence&.split&.map(&:to_i)
size = [height, width] if height && width
@size = T.let(size, T.nilable([Integer, Integer]))
@size = T.let(($stdin.winsize if $stdin.tty?), T.nilable([Integer, Integer]))
rescue IOError, SystemCallError
@size = nil
end

sig { returns(Integer) }
Expand Down
Loading