diff options
Diffstat (limited to 'spec/mixlib/shellout_spec.rb')
-rw-r--r-- | spec/mixlib/shellout_spec.rb | 922 |
1 files changed, 922 insertions, 0 deletions
diff --git a/spec/mixlib/shellout_spec.rb b/spec/mixlib/shellout_spec.rb new file mode 100644 index 0000000..00701a6 --- /dev/null +++ b/spec/mixlib/shellout_spec.rb @@ -0,0 +1,922 @@ +require 'spec_helper' + +describe Mixlib::ShellOut do + let(:shell_cmd) { options ? shell_cmd_with_options : shell_cmd_without_options } + let(:executed_cmd) { shell_cmd.tap(&:run_command) } + let(:stdout) { executed_cmd.stdout } + let(:stderr) { executed_cmd.stderr } + let(:chomped_stdout) { stdout.chomp } + let(:stripped_stdout) { stdout.strip } + let(:exit_status) { executed_cmd.status.exitstatus } + + let(:shell_cmd_without_options) { Mixlib::ShellOut.new(cmd) } + let(:shell_cmd_with_options) { Mixlib::ShellOut.new(cmd, options) } + let(:cmd) { ruby_eval.call(ruby_code) } + let(:ruby_code) { raise 'define let(:ruby_code)' } + let(:options) { nil } + + # On some testing environments, we have gems that creates a deprecation notice sent + # out on STDERR. To fix that, we disable gems on Ruby 1.9.2 + let(:ruby_eval) { lambda { |code| "ruby #{disable_gems} -e '#{code}'" } } + let(:disable_gems) { ( ruby_19? ? '--disable-gems' : '') } + + context 'when instantiating' do + subject { shell_cmd } + let(:cmd) { 'apt-get install chef' } + + it "should set the command" do + subject.command.should eql(cmd) + end + + context 'with default settings' do + its(:cwd) { should be_nil } + its(:user) { should be_nil } + its(:group) { should be_nil } + its(:umask) { should be_nil } + its(:timeout) { should eql(600) } + its(:valid_exit_codes) { should eql([0]) } + its(:live_stream) { should be_nil } + its(:input) { should be_nil } + + it "should set default environmental variables" do + shell_cmd.environment.should == {"LC_ALL" => "C"} + end + end + + context 'when setting accessors' do + subject { shell_cmd.send(accessor) } + + let(:shell_cmd) { blank_shell_cmd.tap(&with_overrides) } + let(:blank_shell_cmd) { Mixlib::ShellOut.new('apt-get install chef') } + let(:with_overrides) { lambda { |shell_cmd| shell_cmd.send("#{accessor}=", value) } } + + context 'when setting user' do + let(:accessor) { :user } + let(:value) { 'root' } + + it "should set the user" do + should eql(value) + end + + context 'with an integer value for user' do + let(:value) { 0 } + it "should use the user-supplied uid" do + shell_cmd.uid.should eql(value) + end + end + + context 'with string value for user' do + let(:value) { username } + + let(:username) { user_info.name } + let(:expected_uid) { user_info.uid } + let(:user_info) { Etc.getpwent } + + it "should compute the uid of the user", :unix_only => true do + shell_cmd.uid.should eql(expected_uid) + end + end + + end + + context 'when setting group' do + let(:accessor) { :group } + let(:value) { 'wheel' } + + it "should set the group" do + should eql(value) + end + + context 'with integer value for group' do + let(:value) { 0 } + it "should use the user-supplied gid" do + shell_cmd.gid.should eql(value) + end + end + + context 'with string value for group' do + let(:value) { groupname } + let(:groupname) { group_info.name } + let(:expected_gid) { group_info.gid } + let(:group_info) { Etc.getgrent } + + it "should compute the gid of the user", :unix_only => true do + shell_cmd.gid.should eql(expected_gid) + end + end + end + + context 'when setting the umask' do + let(:accessor) { :umask } + + context 'with octal integer' do + let(:value) { 007555} + + it 'should set the umask' do + should eql(value) + end + end + + context 'with decimal integer' do + let(:value) { 2925 } + + it 'should sets the umask' do + should eql(005555) + end + end + + context 'with string' do + let(:value) { '7777' } + + it 'should sets the umask' do + should eql(007777) + end + end + end + + context 'when setting read timeout' do + let(:accessor) { :timeout } + let(:value) { 10 } + + it 'should set the read timeout' do + should eql(value) + end + end + + context 'when setting valid exit codes' do + let(:accessor) { :valid_exit_codes } + let(:value) { [0, 23, 42] } + + it "should set the valid exit codes" do + should eql(value) + end + end + + context 'when setting a live stream' do + let(:accessor) { :live_stream } + let(:value) { stream } + let(:stream) { StringIO.new } + + it "should set the live stream" do + should eql(value) + end + end + + context 'when setting an input' do + let(:accessor) { :input } + let(:value) { "Random content #{rand(1000000)}" } + + it "should set the input" do + should eql(value) + end + end + end + + context "with options hash" do + let(:cmd) { 'brew install couchdb' } + let(:options) { { :cwd => cwd, :user => user, :group => group, :umask => umask, + :timeout => timeout, :environment => environment, :returns => valid_exit_codes, + :live_stream => stream, :input => input } } + + let(:cwd) { '/tmp' } + let(:user) { 'toor' } + let(:group) { 'wheel' } + let(:umask) { '2222' } + let(:timeout) { 5 } + let(:environment) { { 'RUBY_OPTS' => '-w' } } + let(:valid_exit_codes) { [ 0, 1, 42 ] } + let(:stream) { StringIO.new } + let(:input) { 1.upto(10).map { "Data #{rand(100000)}" }.join("\n") } + + it "should set the working directory" do + shell_cmd.cwd.should eql(cwd) + end + + it "should set the user" do + shell_cmd.user.should eql(user) + end + + it "should set the group" do + shell_cmd.group.should eql(group) + end + + it "should set the umask" do + shell_cmd.umask.should eql(002222) + end + + it "should set the timout" do + shell_cmd.timeout.should eql(timeout) + end + + it "should add environment settings to the default" do + shell_cmd.environment.should eql({'LC_ALL' => 'C', 'RUBY_OPTS' => '-w'}) + end + + context 'when setting custom environments' do + context 'when setting the :env option' do + let(:options) { { :env => environment } } + + it "should also set the enviroment" do + shell_cmd.environment.should eql({'LC_ALL' => 'C', 'RUBY_OPTS' => '-w'}) + end + end + + context 'when :environment is set to nil' do + let(:options) { { :environment => nil } } + + it "should not set any environment" do + shell_cmd.environment.should == {} + end + end + + context 'when :env is set to nil' do + let(:options) { { :env => nil } } + + it "should not set any environment" do + shell_cmd.environment.should eql({}) + end + end + end + + it "should set valid exit codes" do + shell_cmd.valid_exit_codes.should eql(valid_exit_codes) + end + + it "should set the live stream" do + shell_cmd.live_stream.should eql(stream) + end + + it "should set the input" do + shell_cmd.input.should eql(input) + end + + context 'with an invalid option' do + let(:options) { { :frab => :job } } + let(:invalid_option_exception) { Mixlib::ShellOut::InvalidCommandOption } + let(:exception_message) { "option ':frab' is not a valid option for Mixlib::ShellOut" } + + it "should raise InvalidCommandOPtion" do + lambda { shell_cmd }.should raise_error(invalid_option_exception, exception_message) + end + end + end + + context "with array of command and args" do + let(:cmd) { [ 'ruby', '-e', %q{'puts "hello"'} ] } + + context 'without options' do + let(:options) { nil } + + it "should set the command to the array of command and args" do + shell_cmd.command.should eql(cmd) + end + end + + context 'with options' do + let(:options) { {:cwd => '/tmp', :user => 'nobody'} } + + it "should set the command to the array of command and args" do + shell_cmd.command.should eql(cmd) + end + + it "should evaluate the options" do + shell_cmd.cwd.should eql('/tmp') + shell_cmd.user.should eql('nobody') + end + end + end + end + + context 'when executing the command' do + let(:dir) { Dir.mktmpdir } + let(:dump_file) { "#{dir}/out.txt" } + let(:dump_file_content) { stdout; IO.read(dump_file) } + + context 'with a current working directory' do + subject { File.expand_path(chomped_stdout) } + let(:fully_qualified_cwd) { File.expand_path(cwd) } + let(:options) { { :cwd => cwd } } + + context 'when running under Unix', :unix_only => true do + let(:cwd) { '/bin' } + let(:cmd) { 'pwd' } + + it "should chdir to the working directory" do + should eql(fully_qualified_cwd) + end + end + + context 'when running under Windows', :windows_only => true do + let(:cwd) { Dir.tmpdir } + let(:cmd) { 'echo %cd%' } + + it "should chdir to the working directory" do + should eql(fully_qualified_cwd) + end + end + end + + context 'when handling locale' do + subject { stripped_stdout } + let(:cmd) { ECHO_LC_ALL } + let(:options) { { :environment => { 'LC_ALL' => locale } } } + + context 'without specifying environment' do + let(:options) { nil } + it "should use the C locale by default" do + should eql('C') + end + end + + context 'with locale' do + let(:locale) { 'es' } + + it "should use the requested locale" do + should eql(locale) + end + end + + context 'with LC_ALL set to nil' do + let(:locale) { nil } + + context 'when running under Unix', :unix_only => true do + let(:parent_locale) { ENV['LC_ALL'].to_s.strip } + + it "should use the parent process's locale" do + should eql(parent_locale) + end + end + + context 'when running under Windows', :windows_only => true do + # On windows, if an environmental variable is not set, it returns the key + let(:parent_locale) { (ENV['LC_ALL'] || '%LC_ALL%').to_s.strip } + + it "should use the parent process's locale" do + should eql(parent_locale) + end + end + end + end + + context "with a live stream" do + let(:stream) { StringIO.new } + let(:ruby_code) { 'puts "hello"' } + let(:options) { { :live_stream => stream } } + + it "should copy the child's stdout to the live stream" do + shell_cmd.run_command + stream.string.should eql("hello#{LINE_ENDING}") + end + end + + # FIXME: Add Windows support + context "with an input", :unix_only => true do + subject { stdout } + + let(:input) { 'hello' } + let(:ruby_code) { 'STDIN.sync = true; STDOUT.sync = true; puts gets' } + let(:options) { { :input => input } } + + it "should copy the input to the child's stdin" do + should eql("hello#{LINE_ENDING}") + end + end + + context "when running different types of command" do + let(:script) { open_file.tap(&write_file).tap(&:close).tap(&make_executable) } + let(:file_name) { "#{dir}/Setup Script.cmd" } + let(:script_name) { "\"#{script.path}\"" } + + let(:open_file) { File.open(file_name, 'w') } + let(:write_file) { lambda { |f| f.write(script_content) } } + let(:make_executable) { lambda { |f| File.chmod(0755, f.path) } } + + context 'with spaces in the path' do + subject { chomped_stdout } + let(:cmd) { script_name } + + + context 'when running under Unix', :unix_only => true do + let(:script_content) { 'echo blah' } + + it 'should execute' do + should eql('blah') + end + end + + context 'when running under Windows', :windows_only => true do + let(:cmd) { "#{script_name} #{argument}" } + let(:script_content) { '@echo %1' } + let(:argument) { rand(10000).to_s } + + it 'should execute' do + should eql(argument) + end + + context 'with multiple quotes in the command and args' do + context 'when using a batch file' do + let(:argument) { "\"Random #{rand(10000)}\"" } + + it 'should execute' do + should eql(argument) + end + 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!) } + let(:make_executable!) { lambda { |filename| Mixlib::ShellOut.new("copy \"#{full_path_to_ruby}\" #{filename}").run_command } } + let(:script_content) { "print \"#{expected_output}\"" } + let(:expected_output) { "Random #{rand(10000)}" } + + let(:full_path_to_ruby) { ENV['PATH'].split(';').map(&try_ruby).reject(&:nil?).first } + let(:try_ruby) { lambda { |path| "#{path}\\ruby.exe" if File.executable? "#{path}\\ruby.exe" } } + + it 'should execute' do + should eql(expected_output) + end + end + end + end + end + + + context 'with lots of long arguments' do + subject { chomped_stdout } + + # This number was chosen because it seems to be an actual maximum + # in Windows--somewhere around 6-7K of command line + let(:echotext) { 10000.upto(11340).map(&:to_s).join(' ') } + let(:cmd) { "echo #{echotext}" } + + it 'should execute' do + should eql(echotext) + end + end + + context 'with special characters' do + subject { stdout } + + let(:special_characters) { '<>&|&&||;' } + let(:ruby_code) { "print \"#{special_characters}\"" } + + it 'should execute' do + should eql(special_characters) + end + end + + + context 'with backslashes' do + subject { stdout } + let(:backslashes) { %q{\\"\\\\} } + let(:cmd) { ruby_eval.call("print \"#{backslashes}\"") } + + it 'should execute' do + should eql("\"\\") + end + end + + context 'with pipes' do + let(:input_script) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" } + let(:output_script) { "print STDIN.read.length" } + let(:cmd) { ruby_eval.call(input_script) + " | " + ruby_eval.call(output_script) } + + it 'should execute' do + stdout.should eql('4') + end + + it 'should handle stderr' do + stderr.should eql('false') + end + end + + context 'with stdout and stderr file pipes' do + let(:code) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" } + let(:cmd) { ruby_eval.call(code) + " > #{dump_file}" } + + it 'should execute' do + stdout.should eql('') + end + + it 'should handle stderr' do + stderr.should eql('false') + end + + it 'should write to file pipe' do + dump_file_content.should eql('true') + end + end + + context 'with stdin file pipe' do + let(:code) { "STDIN.sync = true; STDOUT.sync = true; STDERR.sync = true; print gets; STDERR.print false" } + let(:cmd) { ruby_eval.call(code) + " < #{dump_file_path}" } + let(:file_content) { "Random content #{rand(100000)}" } + + let(:dump_file_path) { dump_file.path } + let(:dump_file) { open_file.tap(&write_file).tap(&:close) } + let(:file_name) { "#{dir}/input" } + + let(:open_file) { File.open(file_name, 'w') } + let(:write_file) { lambda { |f| f.write(file_content) } } + + it 'should execute' do + stdout.should eql(file_content) + end + + it 'should handle stderr' do + stderr.should eql('false') + end + end + + context 'with stdout and stderr file pipes' do + let(:code) { "STDOUT.sync = true; STDERR.sync = true; print true; STDERR.print false" } + let(:cmd) { ruby_eval.call(code) + " > #{dump_file} 2>&1" } + + it 'should execute' do + stdout.should eql('') + end + + it 'should write to file pipe' do + dump_file_content.should eql('truefalse') + end + end + + context 'with &&' do + subject { stdout } + let(:cmd) { ruby_eval.call('print "foo"') + ' && ' + ruby_eval.call('print "bar"') } + + it 'should execute' do + should eql('foobar') + end + end + + context 'with ||' do + let(:cmd) { ruby_eval.call('print "foo"; exit 1') + ' || ' + ruby_eval.call('print "bar"') } + + it 'should execute' do + stdout.should eql('foobar') + end + + it 'should exit with code 0' do + exit_status.should eql(0) + end + end + end + + context "when handling process exit codes" do + let(:cmd) { ruby_eval.call("exit #{exit_code}") } + + context 'with normal exit status' do + let(:exit_code) { 0 } + + it "should not raise error" do + lambda { executed_cmd.error! }.should_not raise_error + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + end + + context 'with nonzero exit status' do + let(:exit_code) { 2 } + let(:exception_message_format) { Regexp.escape(executed_cmd.format_for_exception) } + + it "should raise ShellCommandFailed" do + lambda { executed_cmd.error! }.should raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "includes output with exceptions from #error!" do + begin + executed_cmd.error! + rescue Mixlib::ShellOut::ShellCommandFailed => e + e.message.should match(exception_message_format) + end + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + end + + context 'with valid exit codes' do + let(:cmd) { ruby_eval.call("exit #{exit_code}" ) } + let(:options) { { :returns => valid_exit_codes } } + + context 'when exiting with valid code' do + let(:valid_exit_codes) { 42 } + let(:exit_code) { 42 } + + it "should not raise error" do + lambda { executed_cmd.error! }.should_not raise_error + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + end + + context 'when exiting with invalid code' do + let(:valid_exit_codes) { [ 0, 1, 42 ] } + let(:exit_code) { 2 } + + it "should raise ShellCommandFailed" do + lambda { executed_cmd.error! }.should raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + + context 'with input data' do + let(:options) { { :returns => valid_exit_codes, :input => input } } + let(:input) { "Random data #{rand(1000000)}" } + + it "should raise ShellCommandFailed" do + lambda { executed_cmd.error! }.should raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + end + end + + context 'when exiting with invalid code 0' do + let(:valid_exit_codes) { 42 } + let(:exit_code) { 0 } + + it "should raise ShellCommandFailed" do + lambda { executed_cmd.error! }.should raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "should set the exit status of the command" do + exit_status.should eql(exit_code) + end + end + end + + describe "#invalid!" do + let(:exit_code) { 0 } + + it "should raise ShellCommandFailed" do + lambda { executed_cmd.invalid!("I expected this to exit 42, not 0") }.should raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + end + end + + context "when handling the subprocess" do + context 'with STDOUT and STDERR' do + let(:ruby_code) { 'STDERR.puts :hello; STDOUT.puts :world' } + + # We could separate this into two examples, but we want to make + # sure that stderr and stdout gets collected without stepping + # on each other. + it "should collect all of STDOUT and STDERR" do + stderr.should eql("hello#{LINE_ENDING}") + stdout.should eql("world#{LINE_ENDING}") + end + end + + context 'with forking subprocess that does not close stdout and stderr' do + let(:ruby_code) { "exit if fork; 10.times { sleep 1 }" } + + it "should not hang" do + proc do + Timeout.timeout(2) do + executed_cmd + end + end.should_not raise_error + end + end + + context 'with subprocess that takes longer than timeout' do + let(:cmd) { ruby_eval.call('sleep 2') } + let(:options) { { :timeout => 0.1 } } + + it "should raise CommandTimeout" do + lambda { executed_cmd }.should raise_error(Mixlib::ShellOut::CommandTimeout) + end + end + + context 'with subprocess that exceeds buffersize' do + let(:ruby_code) { 'print("X" * 16 * 1024); print("." * 1024)' } + + it "should still reads all of the output" do + stdout.should match(/X{16384}\.{1024}/) + end + end + + context 'with subprocess that returns nothing' do + let(:ruby_code) { 'exit 0' } + + it 'should return an empty string for stdout' do + stdout.should eql('') + end + + it 'should return an empty string for stderr' do + stderr.should eql('') + end + end + + context 'with subprocess that closes stdin and continues writing to stdout' do + let(:ruby_code) { "STDIN.close; sleep 0.5; STDOUT.puts :win" } + let(:options) { { :input => "Random data #{rand(100000)}" } } + + it 'should not hang or lose outupt' do + stdout.should eql("win#{LINE_ENDING}") + end + end + + context 'with subprocess that closes stdout and continues writing to stderr' do + let(:ruby_code) { "STDOUT.close; sleep 0.5; STDERR.puts :win" } + + it 'should not hang or lose outupt' do + stderr.should eql("win#{LINE_ENDING}") + end + end + + context 'with subprocess that closes stderr and continues writing to stdout' do + let(:ruby_code) { "STDERR.close; sleep 0.5; STDOUT.puts :win" } + + it 'should not hang or lose outupt' do + stdout.should eql("win#{LINE_ENDING}") + end + end + + # Regression test: + # + # We need to ensure that stderr is removed from the list of file + # descriptors that we attempt to select() on in the case that: + # + # a) STDOUT closes first + # b) STDERR closes + # c) The program does not exit for some time after (b) occurs. + # + # Otherwise, we will attempt to read from the closed STDOUT pipe over and + # over again and generate lots of garbage, which will not be collected + # since we have to turn GC off to avoid segv. + context 'with subprocess that closes STDOUT before closing STDERR' do + subject { unclosed_pipes } + let(:ruby_code) { %q{STDOUT.puts "F" * 4096; STDOUT.close; sleep 0.1; STDERR.puts "foo"; STDERR.close; sleep 0.1; exit} } + let(:unclosed_pipes) { executed_cmd.send(:open_pipes) } + + it 'should not hang' do + should be_empty + end + end + + context 'with subprocess reading lots of data from stdin', :unix_only => true do + subject { stdout.to_i } + let(:ruby_code) { 'STDOUT.print gets.size' } + let(:options) { { :input => input } } + let(:input) { 'f' * 20_000 } + let(:input_size) { input.size } + + it 'should not hang' do + should eql(input_size) + end + end + + context 'with subprocess writing lots of data to both stdout and stderr' do + let(:expected_output_with) { lambda { |chr| (chr * 20_000) + "#{LINE_ENDING}" + (chr * 20_000) + "#{LINE_ENDING}" } } + + context 'when writing to STDOUT first' do + let(:ruby_code) { %q{puts "f" * 20_000; STDERR.puts "u" * 20_000; puts "f" * 20_000; STDERR.puts "u" * 20_000} } + + it "should not deadlock" do + stdout.should eql(expected_output_with.call('f')) + stderr.should eql(expected_output_with.call('u')) + end + end + + context 'when writing to STDERR first' do + let(:ruby_code) { %q{STDERR.puts "u" * 20_000; puts "f" * 20_000; STDERR.puts "u" * 20_000; puts "f" * 20_000} } + + it "should not deadlock" do + stdout.should eql(expected_output_with.call('f')) + stderr.should eql(expected_output_with.call('u')) + end + end + end + + context 'with subprocess piping lots of data through stdin, stdout, and stderr', :unix_only => true do + let(:multiplier) { 20_000 } + let(:expected_output_with) { lambda { |chr| (chr * multiplier) + "#{LINE_ENDING}" + (chr * multiplier) + "#{LINE_ENDING}" } } + # Use regex to work across Ruby versions + let(:ruby_code) { 'while(input = gets) do ( input =~ /^f/ ? STDOUT : STDERR ).puts input; end' } + let(:options) { { :input => input } } + + context 'when writing to STDOUT first' do + let(:input) { [ 'f' * multiplier, 'u' * multiplier, 'f' * multiplier, 'u' * multiplier ].join(LINE_ENDING) } + + it "should not deadlock" do + stdout.should eql(expected_output_with.call('f')) + stderr.should eql(expected_output_with.call('u')) + end + end + + context 'when writing to STDERR first' do + let(:input) { [ 'u' * multiplier, 'f' * multiplier, 'u' * multiplier, 'f' * multiplier ].join(LINE_ENDING) } + + it "should not deadlock" do + stdout.should eql(expected_output_with.call('f')) + stderr.should eql(expected_output_with.call('u')) + end + end + end + + context 'when subprocess closes prematurely' do + context 'with input data' do + let(:ruby_code) { 'bad_ruby { [ } ]' } + let(:options) { { :input => input } } + let(:input) { [ 'f' * 20_000, 'u' * 20_000, 'f' * 20_000, 'u' * 20_000 ].join(LINE_ENDING) } + + # Should the exception be handled? + it 'should raise error' do + lambda { executed_cmd }.should raise_error(Errno::EPIPE) + end + end + end + + context 'when subprocess writes, pauses, then continues writing' do + subject { stdout } + let(:ruby_code) { %q{puts "before"; sleep 0.5; puts "after"} } + + it 'should not hang or lose output' do + should eql("before#{LINE_ENDING}after#{LINE_ENDING}") + end + end + + context 'when subprocess pauses before writing' do + subject { stdout } + let(:ruby_code) { 'sleep 0.5; puts "missed_the_bus"' } + + it 'should not hang or lose output' do + should eql("missed_the_bus#{LINE_ENDING}") + end + end + + context 'when subprocess pauses before reading from stdin', :unix_only => true do + subject { stdout.to_i } + let(:ruby_code) { 'sleep 0.5; print gets.size ' } + let(:input) { 'c' * 1024 } + let(:input_size) { input.size } + let(:options) { { :input => input } } + + it 'should not hang or lose output' do + should eql(input_size) + end + end + + context 'when execution fails' do + let(:cmd) { "fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu" } + + it "should recover the error message" do + lambda { executed_cmd }.should raise_error(Errno::ENOENT) + end + + context 'with input', :unix_only => true do + let(:options) { {:input => input } } + let(:input) { "Random input #{rand(1000000)}" } + + it "should recover the error message" do + lambda { executed_cmd }.should raise_error(Errno::ENOENT) + end + end + end + + context 'without input data', :unix_only => true do + context 'with subprocess that expects stdin' do + let(:ruby_code) { %q{print STDIN.eof?.to_s} } + + # If we don't have anything to send to the subprocess, we need to close + # stdin so that the subprocess won't wait for input. + it 'should close stdin' do + stdout.should eql("true") + end + end + end + end + + describe "#format_for_exception" do + let(:ruby_code) { %q{STDERR.puts "msg_in_stderr"; puts "msg_in_stdout"} } + let(:exception_output) { executed_cmd.format_for_exception.split("\n") } + let(:expected_output) { [ + "---- Begin output of #{cmd} ----", + %q{STDOUT: msg_in_stdout}, + %q{STDERR: msg_in_stderr}, + "---- End output of #{cmd} ----", + "Ran #{cmd} returned 0" + ] } + + it "should format exception messages" do + exception_output.each_with_index do |output_line, i| + output_line.should eql(expected_output[i]) + end + end + end + end +end |