summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/mixlib/shellout_spec.rb183
-rw-r--r--spec/spec_helper.rb2
2 files changed, 175 insertions, 10 deletions
diff --git a/spec/mixlib/shellout_spec.rb b/spec/mixlib/shellout_spec.rb
index e20a85c..277393b 100644
--- a/spec/mixlib/shellout_spec.rb
+++ b/spec/mixlib/shellout_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'logger'
describe Mixlib::ShellOut do
let(:shell_cmd) { options ? shell_cmd_with_options : shell_cmd_without_options }
@@ -343,7 +344,9 @@ describe Mixlib::ShellOut do
let(:options) { { :cwd => cwd } }
context 'when running under Unix', :unix_only do
- let(:cwd) { '/bin' }
+ # Use /bin for tests only if it is not a symlink. Some
+ # distributions (e.g. Fedora) symlink it to /usr/bin
+ let(:cwd) { File.symlink?('/bin') ? '/tmp' : '/bin' }
let(:cmd) { 'pwd' }
it "should chdir to the working directory" do
@@ -513,7 +516,6 @@ describe Mixlib::ShellOut do
end
context 'when not using a batch file' do
- let(:watch) { lambda { |a| ap a } }
let(:cmd) { "#{executable_file_name} #{script_name}" }
let(:executable_file_name) { "\"#{dir}/Ruby Parser.exe\"".tap(&make_executable!) }
@@ -755,6 +757,24 @@ describe Mixlib::ShellOut do
lambda { executed_cmd.invalid!("I expected this to exit 42, not 0") }.should raise_error(Mixlib::ShellOut::ShellCommandFailed)
end
end
+
+ describe "#error?" do
+ context 'when exiting with invalid code' do
+ let(:exit_code) { 2 }
+
+ it "should return true" do
+ executed_cmd.error?.should be_true
+ end
+ end
+
+ context 'when exiting with valid code' do
+ let(:exit_code) { 0 }
+
+ it "should return false" do
+ executed_cmd.error?.should be_false
+ end
+ end
+ end
end
context "when handling the subprocess" do
@@ -782,7 +802,7 @@ describe Mixlib::ShellOut do
end
end
- context "when running a command that doesn't exist" do
+ context "when running a command that doesn't exist", :unix_only do
let(:cmd) { "/bin/this-is-not-a-real-command" }
@@ -830,13 +850,160 @@ describe Mixlib::ShellOut do
end
end
- context 'with subprocess that takes longer than timeout' do
- let(:cmd) { ruby_eval.call('sleep 2') }
- let(:options) { { :timeout => 0.1 } }
+ context "when the child process dies immediately" do
+ let(:cmd) { [ 'exit' ] }
+
+ it "handles ESRCH from getpgid of a zombie", :unix_only do
+ Process.stub(:setsid) { exit!(4) }
+
+ # there is a small race condition here if the child doesn't get
+ # scheduled and call exit! before the parent can call getpgid, so run
+ # this a few times to make sure we've created the reproduction case
+ # correctly.
+ 5.times do
+ s = Mixlib::ShellOut.new(cmd)
+ s.run_command # should not raise Errno::ESRCH
+ end
+
+ end
+
+ end
+
+ context 'with subprocess that takes longer than timeout', :unix_only do
+ def ruby_wo_shell(code)
+ parts = %w[ruby]
+ parts << "--disable-gems" if ruby_19?
+ parts << "-e"
+ parts << code
+ end
+
+ let(:cmd) do
+ ruby_wo_shell(<<-CODE)
+ STDOUT.sync = true
+ trap(:TERM) { puts "got term"; exit!(123) }
+ sleep 10
+ CODE
+ end
+ let(:options) { { :timeout => 1 } }
it "should raise CommandTimeout" do
lambda { executed_cmd }.should raise_error(Mixlib::ShellOut::CommandTimeout)
end
+
+ it "should ask the process nicely to exit" do
+ # note: let blocks don't correctly memoize if an exception is raised,
+ # so can't use executed_cmd
+ lambda { shell_cmd.run_command }.should raise_error(Mixlib::ShellOut::CommandTimeout)
+ shell_cmd.stdout.should include("got term")
+ shell_cmd.exitstatus.should == 123
+ end
+
+ context "and the child is unresponsive" do
+ let(:cmd) do
+ ruby_wo_shell(<<-CODE)
+ STDOUT.sync = true
+ trap(:TERM) { puts "nanana cant hear you" }
+ sleep 10
+ CODE
+ end
+
+ it "should KILL the wayward child" do
+ # note: let blocks don't correctly memoize if an exception is raised,
+ # so can't use executed_cmd
+ lambda { shell_cmd.run_command}.should raise_error(Mixlib::ShellOut::CommandTimeout)
+ shell_cmd.stdout.should include("nanana cant hear you")
+ shell_cmd.status.termsig.should == 9
+ end
+
+ context "and a logger is configured" do
+ let(:log_output) { StringIO.new }
+ let(:logger) { Logger.new(log_output) }
+ let(:options) { {:timeout => 1, :logger => logger} }
+
+ it "should log messages about killing the child process" do
+ # note: let blocks don't correctly memoize if an exception is raised,
+ # so can't use executed_cmd
+ lambda { shell_cmd.run_command}.should raise_error(Mixlib::ShellOut::CommandTimeout)
+ shell_cmd.stdout.should include("nanana cant hear you")
+ shell_cmd.status.termsig.should == 9
+
+ log_output.string.should include("Command execeded allowed execution time, sending TERM")
+ log_output.string.should include("Command execeded allowed execution time, sending KILL")
+ end
+
+ end
+ end
+
+ context "and the child process forks grandchildren" do
+ let(:cmd) do
+ ruby_wo_shell(<<-CODE)
+ STDOUT.sync = true
+ trap(:TERM) { print "got term in child\n"; exit!(123) }
+ fork do
+ trap(:TERM) { print "got term in grandchild\n"; exit!(142) }
+ sleep 10
+ end
+ sleep 10
+ CODE
+ end
+
+ it "should TERM the wayward child and grandchild" do
+ # note: let blocks don't correctly memoize if an exception is raised,
+ # so can't use executed_cmd
+ lambda { shell_cmd.run_command}.should raise_error(Mixlib::ShellOut::CommandTimeout)
+ shell_cmd.stdout.should include("got term in child")
+ shell_cmd.stdout.should include("got term in grandchild")
+ end
+
+ end
+ context "and the child process forks grandchildren that don't respond to TERM" do
+ let(:cmd) do
+ ruby_wo_shell(<<-CODE)
+ STDOUT.sync = true
+
+ trap(:TERM) { print "got term in child\n"; exit!(123) }
+ fork do
+ trap(:TERM) { print "got term in grandchild\n" }
+ sleep 10
+ end
+ sleep 10
+ CODE
+ end
+
+
+ it "should TERM the wayward child and grandchild, then KILL whoever is left" do
+ # note: let blocks don't correctly memoize if an exception is raised,
+ # so can't use executed_cmd
+ lambda { shell_cmd.run_command}.should raise_error(Mixlib::ShellOut::CommandTimeout)
+
+ begin
+
+ # A little janky. We get the process group id out of the command
+ # object, then try to kill a process in it to make sure none
+ # exists. Trusting the system under test like this isn't great but
+ # it's difficult to test otherwise.
+ child_pgid = shell_cmd.send(:child_pgid)
+ initial_process_listing = `ps -j`
+
+ shell_cmd.stdout.should include("got term in child")
+ shell_cmd.stdout.should include("got term in grandchild")
+
+ Process.kill(:INT, child_pgid) # should raise ESRCH
+
+ # Debug the failure:
+ puts "child pgid=#{child_pgid.inspect}"
+ Process.wait
+ puts "collected process: #{$?.inspect}"
+ puts "initial process listing:\n#{initial_process_listing}"
+ puts "current process listing:"
+ puts `ps -j`
+ raise "Failed to kill all expected processes"
+ rescue Errno::ESRCH
+ # this is what we want
+ end
+ end
+
+ end
end
context 'with subprocess that exceeds buffersize' do
@@ -1070,8 +1237,8 @@ describe Mixlib::ShellOut do
# test for for_fd returning a valid File object, but close
# throwing EBADF.
it "should not throw an exception if fd.close throws EBADF" do
- fd = mock('File')
- fd.stub!(:close).at_least(:once).and_raise(Errno::EBADF)
+ fd = double('File')
+ fd.stub(:close).at_least(:once).and_raise(Errno::EBADF)
File.should_receive(:for_fd).at_least(:once).and_return(fd)
shellout = Mixlib::ShellOut.new()
shellout.instance_variable_set(:@process_status_pipe, [ 98, 99 ])
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8d2b7c9..9d51c5e 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -7,8 +7,6 @@ require 'tempfile'
require 'timeout'
-WATCH = lambda { |x| ap x } unless defined?(WATCH)
-
# Load everything from spec/support
# Do not change the gsub.
Dir["spec/support/**/*.rb"].map { |f| f.gsub(%r{.rb$}, '') }.each { |f| require f }