brew/Library/Homebrew/test/system_command_spec.rb

283 lines
7.7 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
describe SystemCommand do
describe "#initialize" do
2018-07-30 10:11:00 +02:00
subject(:command) {
described_class.new(
"env",
2018-11-02 17:18:07 +00:00
args: env_args,
env: env,
2018-07-30 10:11:00 +02:00
must_succeed: true,
2018-11-02 17:18:07 +00:00
sudo: sudo,
2018-07-30 10:11:00 +02:00
)
}
2018-09-20 09:07:56 +01:00
let(:env_args) { ["bash", "-c", 'printf "%s" "${A?}" "${B?}" "${C?}"'] }
let(:env) { { "A" => "1", "B" => "2", "C" => "3" } }
let(:sudo) { false }
2018-07-11 09:14:53 +02:00
context "when given some environment variables" do
its("run!.stdout") { is_expected.to eq("123") }
describe "the resulting command line" do
it "includes the given variables explicitly" do
expect(Open3)
.to receive(:popen3)
2018-08-29 19:23:30 +02:00
.with(an_instance_of(Hash), ["env", "env"], "A=1", "B=2", "C=3", "env", *env_args, {})
.and_call_original
2018-07-30 10:11:00 +02:00
command.run!
end
end
end
2018-07-30 10:11:00 +02:00
context "when given an environment variable which is set to nil" do
let(:env) { { "A" => "1", "B" => "2", "C" => nil } }
it "unsets them" do
expect {
command.run!
}.to raise_error(/C: parameter null or not set/)
end
end
2018-07-11 09:14:53 +02:00
context "when given some environment variables and sudo: true" do
2018-07-30 10:11:00 +02:00
let(:sudo) { true }
describe "the resulting command line" do
it "includes the given variables explicitly" do
expect(Open3)
.to receive(:popen3)
2018-08-29 19:23:30 +02:00
.with(an_instance_of(Hash), ["/usr/bin/sudo", "/usr/bin/sudo"], "-E", "--",
"env", "A=1", "B=2", "C=3", "env", *env_args, {})
.and_wrap_original do |original_popen3, *_, &block|
original_popen3.call("true", &block)
end
2018-07-30 10:11:00 +02:00
command.run!
end
end
end
end
2018-07-11 09:14:53 +02:00
context "when the exit code is 0" do
2016-08-18 22:11:42 +03:00
describe "its result" do
subject { described_class.run("true") }
2016-08-18 22:11:42 +03:00
it { is_expected.to be_a_success }
its(:exit_status) { is_expected.to eq(0) }
end
end
2018-07-11 09:14:53 +02:00
context "when the exit code is 1" do
let(:command) { "false" }
2016-08-18 22:11:42 +03:00
2018-07-11 09:14:53 +02:00
context "and the command must succeed" do
2016-08-18 22:11:42 +03:00
it "throws an error" do
expect {
described_class.run!(command)
2018-07-16 23:17:16 +02:00
}.to raise_error(ErrorDuringExecution)
2016-08-18 22:11:42 +03:00
end
end
2018-07-11 09:14:53 +02:00
context "and the command does not have to succeed" do
2016-08-18 22:11:42 +03:00
describe "its result" do
subject { described_class.run(command) }
it { is_expected.not_to be_a_success }
its(:exit_status) { is_expected.to eq(1) }
end
end
end
2018-07-11 09:14:53 +02:00
context "when given a pathname" do
2016-08-18 22:11:42 +03:00
let(:command) { "/bin/ls" }
let(:path) { Pathname(Dir.mktmpdir) }
before do
FileUtils.touch(path.join("somefile"))
end
describe "its result" do
subject { described_class.run(command, args: [path]) }
it { is_expected.to be_a_success }
its(:stdout) { is_expected.to eq("somefile\n") }
end
end
2018-07-11 09:14:53 +02:00
context "with both STDOUT and STDERR output from upstream" do
2016-08-18 22:11:42 +03:00
let(:command) { "/bin/bash" }
let(:options) {
{ args: [
2016-10-14 20:33:16 +02:00
"-c",
"for i in $(seq 1 2 5); do echo $i; echo $(($i + 1)) >&2; done",
] }
2016-08-18 22:11:42 +03:00
}
shared_examples "it returns '1 2 3 4 5 6'" do
describe "its result" do
2017-07-29 19:55:05 +02:00
subject { described_class.run(command, options) }
2016-08-18 22:11:42 +03:00
it { is_expected.to be_a_success }
its(:stdout) { is_expected.to eq([1, 3, 5, nil].join("\n")) }
its(:stderr) { is_expected.to eq([2, 4, 6, nil].join("\n")) }
end
end
2018-07-11 09:14:53 +02:00
context "with default options" do
2016-08-18 22:11:42 +03:00
it "echoes only STDERR" do
2018-06-14 22:45:07 +02:00
expected = [2, 4, 6].map { |i| "#{i}\n" }.join
2016-08-18 22:11:42 +03:00
expect {
described_class.run(command, options)
2018-06-14 22:45:07 +02:00
}.to output(expected).to_stderr
2016-08-18 22:11:42 +03:00
end
include_examples("it returns '1 2 3 4 5 6'")
end
2018-07-11 09:14:53 +02:00
context "with print_stdout" do
2016-08-18 22:11:42 +03:00
before do
options.merge!(print_stdout: true)
end
it "echoes both STDOUT and STDERR" do
2018-06-14 22:45:07 +02:00
expect { described_class.run(command, options) }
.to output("1\n3\n5\n").to_stdout
.and output("2\n4\n6\n").to_stderr
2016-08-18 22:11:42 +03:00
end
include_examples("it returns '1 2 3 4 5 6'")
end
2018-07-11 09:14:53 +02:00
context "without print_stderr" do
2016-08-18 22:11:42 +03:00
before do
options.merge!(print_stderr: false)
end
it "echoes nothing" do
expect {
described_class.run(command, options)
}.to output("").to_stdout
end
include_examples("it returns '1 2 3 4 5 6'")
end
2018-07-11 09:14:53 +02:00
context "with print_stdout but without print_stderr" do
2016-08-18 22:11:42 +03:00
before do
options.merge!(print_stdout: true, print_stderr: false)
end
it "echoes only STDOUT" do
2018-06-14 22:48:37 +02:00
expected = [1, 3, 5].map { |i| "#{i}\n" }.join
2016-08-18 22:11:42 +03:00
expect {
described_class.run(command, options)
}.to output(expected).to_stdout
end
include_examples("it returns '1 2 3 4 5 6'")
end
end
2018-07-11 09:14:53 +02:00
context "with a very long STDERR output" do
2016-08-18 22:11:42 +03:00
let(:command) { "/bin/bash" }
let(:options) {
{ args: [
2016-10-14 20:33:16 +02:00
"-c",
"for i in $(seq 1 2 100000); do echo $i; echo $(($i + 1)) >&2; done",
] }
2016-08-18 22:11:42 +03:00
}
it "returns without deadlocking" do
wait(30).for {
2017-07-29 19:55:05 +02:00
described_class.run(command, options)
2016-08-18 22:11:42 +03:00
}.to be_a_success
end
end
2018-07-11 09:14:53 +02:00
context "when given an invalid variable name" do
it "raises an ArgumentError" do
expect { described_class.run("true", env: { "1ABC" => true }) }
.to raise_error(ArgumentError, /variable name/)
end
end
2018-07-11 09:14:53 +02:00
it "looks for executables in a custom PATH" do
2018-07-11 09:14:53 +02:00
mktmpdir do |path|
(path/"tool").write <<~SH
#!/bin/sh
echo Hello, world!
SH
FileUtils.chmod "+x", path/"tool"
expect(described_class.run("tool", env: { "PATH" => path }).stdout).to include "Hello, world!"
end
end
describe "#run" do
it "does not raise a `SystemCallError` when the executable does not exist" do
expect {
described_class.run("non_existent_executable")
}.not_to raise_error
end
2018-07-30 10:11:00 +02:00
it 'does not format `stderr` when it starts with \r' do
expect {
system_command \
"bash",
args: [
"-c",
'printf "\r%s" "################### 27.6%" 1>&2',
]
}.to output( \
"\r################### 27.6%",
).to_stderr
2018-07-30 10:11:00 +02:00
end
2018-08-29 19:23:30 +02:00
context "when given an executable with spaces and no arguments" do
let(:executable) { mktmpdir/"App Uninstaller" }
2018-09-20 09:07:56 +01:00
before do
2018-08-29 19:23:30 +02:00
executable.write <<~SH
#!/usr/bin/env bash
true
SH
FileUtils.chmod "+x", executable
end
it "does not interpret the executable as a shell line" do
expect(system_command(executable)).to be_a_success
end
end
context "when given arguments with secrets" do
it "does not leak the secrets" do
redacted_msg = /#{Regexp.escape("username:******")}/
expect do
described_class.run! "curl",
args: %w[--user username:hunter2],
verbose: true,
secrets: %w[hunter2]
end.to raise_error.with_message(redacted_msg).and output(redacted_msg).to_stdout
end
it "does not leak the secrets set by environment" do
redacted_msg = /#{Regexp.escape("username:******")}/
expect do
begin
ENV["PASSWORD"] = "hunter2"
described_class.run! "curl",
args: %w[--user username:hunter2],
verbose: true
ensure
ENV.delete "PASSWORD"
end
end.to raise_error.with_message(redacted_msg).and output(redacted_msg).to_stdout
end
end
end
2016-08-18 22:11:42 +03:00
end