require_relative '../../spec_helper' describe "Process.exec" do it "raises Errno::ENOENT for an empty string" do -> { Process.exec "" }.should raise_error(Errno::ENOENT) end it "raises Errno::ENOENT for a command which does not exist" do -> { Process.exec "bogus-noent-script.sh" }.should raise_error(Errno::ENOENT) end it "raises an ArgumentError if the command includes a null byte" do -> { Process.exec "\000" }.should raise_error(ArgumentError) end unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable platform_is_not :windows do it "raises Errno::EACCES when the file does not have execute permissions" do -> { Process.exec __FILE__ }.should raise_error(Errno::EACCES) end end platform_is :windows do it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do -> { Process.exec __FILE__ }.should raise_error(SystemCallError) { |e| [Errno::EACCES, Errno::ENOEXEC].should include(e.class) } end end end it "raises Errno::EACCES when passed a directory" do -> { Process.exec File.dirname(__FILE__) }.should raise_error(Errno::EACCES) end it "runs the specified command, replacing current process" do ruby_exe('Process.exec "echo hello"; puts "fail"', escape: true).should == "hello\n" end it "sets the current directory when given the :chdir option" do tmpdir = tmp("")[0..-2] platform_is_not :windows do ruby_exe("Process.exec(\"pwd\", chdir: #{tmpdir.inspect})", escape: true).should == "#{tmpdir}\n" end platform_is :windows do ruby_exe("Process.exec(\"cd\", chdir: #{tmpdir.inspect})", escape: true).tr('\\', '/').should == "#{tmpdir}\n" end end it "flushes STDOUT upon exit when it's not set to sync" do ruby_exe("STDOUT.sync = false; STDOUT.write 'hello'").should == "hello" end it "flushes STDERR upon exit when it's not set to sync" do ruby_exe("STDERR.sync = false; STDERR.write 'hello'", args: "2>&1").should == "hello" end describe "with a single argument" do before :each do @dir = tmp("exec_with_dir", false) Dir.mkdir @dir @name = "some_file" @path = tmp("exec_with_dir/#{@name}", false) touch @path end after :each do rm_r @path rm_r @dir end platform_is_not :windows do it "subjects the specified command to shell expansion" do result = Dir.chdir(@dir) do ruby_exe('Process.exec "echo *"', escape: true) end result.chomp.should == @name end it "creates an argument array with shell parsing semantics for whitespace" do ruby_exe('Process.exec "echo a b c d"', escape: true).should == "a b c d\n" end end platform_is :windows do # There is no shell expansion on Windows it "does not subject the specified command to shell expansion on Windows" do result = Dir.chdir(@dir) do ruby_exe('Process.exec "echo *"', escape: true) end result.should == "*\n" end it "does not create an argument array with shell parsing semantics for whitespace on Windows" do ruby_exe('Process.exec "echo a b c d"', escape: true).should == "a b c d\n" end end end describe "with multiple arguments" do it "does not subject the arguments to shell expansion" do cmd = '"echo", "*"' platform_is :windows do cmd = '"cmd.exe", "/C", "echo", "*"' end ruby_exe("Process.exec #{cmd}", escape: true).should == "*\n" end end describe "(environment variables)" do before :each do ENV["FOO"] = "FOO" end after :each do ENV["FOO"] = nil end var = '$FOO' platform_is :windows do var = '%FOO%' end it "sets environment variables in the child environment" do ruby_exe('Process.exec({"FOO" => "BAR"}, "echo ' + var + '")', escape: true).should == "BAR\n" end it "unsets environment variables whose value is nil" do platform_is_not :windows do ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")', escape: true).should == "\n" end platform_is :windows do # On Windows, echo-ing a non-existent env var is treated as echo-ing any other string of text ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")', escape: true).should == var + "\n" end end it "coerces environment argument using to_hash" do ruby_exe('o = Object.new; def o.to_hash; {"FOO" => "BAR"}; end; Process.exec(o, "echo ' + var + '")', escape: true).should == "BAR\n" end it "unsets other environment variables when given a true :unsetenv_others option" do platform_is_not :windows do ruby_exe('Process.exec("echo ' + var + '", unsetenv_others: true)', escape: true).should == "\n" end platform_is :windows do ruby_exe('Process.exec("' + ENV['COMSPEC'].gsub('\\', '\\\\\\') + ' /C echo ' + var + '", unsetenv_others: true)', escape: true).should == var + "\n" end end end describe "with a command array" do it "uses the first element as the command name and the second as the argv[0] value" do platform_is_not :windows do ruby_exe('Process.exec(["/bin/sh", "argv_zero"], "-c", "echo $0")', escape: true).should == "argv_zero\n" end platform_is :windows do ruby_exe('Process.exec(["cmd.exe", "/C"], "/C", "echo", "argv_zero")', escape: true).should == "argv_zero\n" end end it "coerces the argument using to_ary" do platform_is_not :windows do ruby_exe('o = Object.new; def o.to_ary; ["/bin/sh", "argv_zero"]; end; Process.exec(o, "-c", "echo $0")', escape: true).should == "argv_zero\n" end platform_is :windows do ruby_exe('o = Object.new; def o.to_ary; ["cmd.exe", "/C"]; end; Process.exec(o, "/C", "echo", "argv_zero")', escape: true).should == "argv_zero\n" end end it "raises an ArgumentError if the Array does not have exactly two elements" do -> { Process.exec([]) }.should raise_error(ArgumentError) -> { Process.exec([:a]) }.should raise_error(ArgumentError) -> { Process.exec([:a, :b, :c]) }.should raise_error(ArgumentError) end end platform_is_not :windows do describe "with an options Hash" do describe "with Integer option keys" do before :each do @name = tmp("exec_fd_map.txt") @child_fd_file = tmp("child_fd_file.txt") end after :each do rm_r @name, @child_fd_file end it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do map_fd_fixture = fixture __FILE__, "map_fd.rb" cmd = <<-EOC f = File.open(#{@name.inspect}, "w+") File.open(#{__FILE__.inspect}, "r") do |io| child_fd = io.fileno File.open(#{@child_fd_file.inspect}, "w") { |io| io.print child_fd } Process.exec "#{ruby_cmd(map_fd_fixture)} \#{child_fd}", { child_fd => f } end EOC ruby_exe(cmd, escape: true) child_fd = IO.read(@child_fd_file).to_i child_fd.to_i.should > STDERR.fileno File.read(@name).should == "writing to fd: #{child_fd}" end it "lets the process after exec have specified file descriptor despite close_on_exec" do map_fd_fixture = fixture __FILE__, "map_fd.rb" cmd = <<-EOC f = File.open(#{@name.inspect}, 'w+') puts(f.fileno, f.close_on_exec?) STDOUT.flush Process.exec("#{ruby_cmd(map_fd_fixture)} \#{f.fileno}", f.fileno => f.fileno) EOC output = ruby_exe(cmd, escape: true) child_fd, close_on_exec = output.split child_fd.to_i.should > STDERR.fileno close_on_exec.should == 'true' File.read(@name).should == "writing to fd: #{child_fd}" end it "sets close_on_exec to false on specified fd even when it fails" do cmd = <<-EOC f = File.open(#{__FILE__.inspect}, 'r') puts(f.close_on_exec?) Process.exec('/', f.fileno => f.fileno) rescue nil puts(f.close_on_exec?) EOC output = ruby_exe(cmd, escape: true) output.split.should == ['true', 'false'] end end end end end