diff options
Diffstat (limited to 'spec')
-rw-r--r-- | spec/mixlib/shellout_spec.rb | 183 | ||||
-rw-r--r-- | spec/spec_helper.rb | 2 |
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 } |