diff options
Diffstat (limited to 'test/ruby/test_process.rb')
| -rw-r--r-- | test/ruby/test_process.rb | 492 |
1 files changed, 424 insertions, 68 deletions
diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index 7301b45a9b..30427aeec1 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -208,59 +208,72 @@ class TestProcess < Test::Unit::TestCase n = max IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| - assert_equal("[#{n}, #{n}]\n", io.read) + "puts Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| + assert_equal("#{n}\n#{n}\n", io.read) } n = 0 IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| - assert_equal("[#{n}, #{n}]\n", io.read) + "puts Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| + assert_equal("#{n}\n#{n}\n", io.read) } n = max IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE)", :rlimit_core=>[n]]) {|io| - assert_equal("[#{n}, #{n}]", io.read.chomp) + "puts Process.getrlimit(:CORE)", :rlimit_core=>[n]]) {|io| + assert_equal("#{n}\n#{n}\n", io.read) } m, n = 0, max IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE)", :rlimit_core=>[m,n]]) {|io| - assert_equal("[#{m}, #{n}]", io.read.chomp) + "puts Process.getrlimit(:CORE)", :rlimit_core=>[m,n]]) {|io| + assert_equal("#{m}\n#{n}\n", io.read) } m, n = 0, 0 IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE)", :rlimit_core=>[m,n]]) {|io| - assert_equal("[#{m}, #{n}]", io.read.chomp) + "puts Process.getrlimit(:CORE)", :rlimit_core=>[m,n]]) {|io| + assert_equal("#{m}\n#{n}\n", io.read) } n = max IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE), Process.getrlimit(:CPU)", + "puts Process.getrlimit(:CORE), Process.getrlimit(:CPU)", :rlimit_core=>n, :rlimit_cpu=>3600]) {|io| - assert_equal("[#{n}, #{n}]\n[3600, 3600]", io.read.chomp) + assert_equal("#{n}\n#{n}\n""3600\n3600\n", io.read) } assert_raise(ArgumentError) do system(RUBY, '-e', 'exit', 'rlimit_bogus'.to_sym => 123) end - assert_separately([],<<-"end;") # [ruby-core:82033] [Bug #13744] - assert(system("#{RUBY}", "-e", - "exit([3600,3600] == Process.getrlimit(:CPU))", - 'rlimit_cpu'.to_sym => 3600)) - assert_raise(ArgumentError) do - system("#{RUBY}", '-e', 'exit', :rlimit_bogus => 123) - end + assert_separately([],"#{<<~"begin;"}\n#{<<~'end;'}", 'rlimit_cpu'.to_sym => 3600) + BUG = "[ruby-core:82033] [Bug #13744]" + begin; + assert_equal([3600,3600], Process.getrlimit(:CPU), BUG) end; - assert_raise(ArgumentError, /rlimit_cpu/) { + assert_raise_with_message(ArgumentError, /bogus/) do + system(RUBY, '-e', 'exit', :rlimit_bogus => 123) + end + + assert_raise_with_message(ArgumentError, /rlimit_cpu/) { system(RUBY, '-e', 'exit', "rlimit_cpu\0".to_sym => 3600) } end - MANDATORY_ENVS = %w[RUBYLIB] + def test_overwrite_ENV + assert_separately([],"#{<<~"begin;"}\n#{<<~"end;"}") + BUG = "[ruby-core:105223] [Bug #18164]" + begin; + $VERBOSE = nil + ENV = {} + pid = spawn({}, *#{TRUECOMMAND.inspect}) + ENV.replace({}) + assert_kind_of(Integer, pid, BUG) + end; + end + + MANDATORY_ENVS = %w[RUBYLIB MJIT_SEARCH_BUILD_DIR] case RbConfig::CONFIG['target_os'] when /linux/ MANDATORY_ENVS << 'LD_PRELOAD' @@ -272,6 +285,9 @@ class TestProcess < Test::Unit::TestCase if e = RbConfig::CONFIG['LIBPATHENV'] MANDATORY_ENVS << e end + if e = RbConfig::CONFIG['PRELOADENV'] and !e.empty? + MANDATORY_ENVS << e + end PREENVARG = ['-e', "%w[#{MANDATORY_ENVS.join(' ')}].each{|e|ENV.delete(e)}"] ENVARG = ['-e', 'ENV.each {|k,v| puts "#{k}=#{v}" }'] ENVCOMMAND = [RUBY].concat(PREENVARG).concat(ENVARG) @@ -334,6 +350,13 @@ class TestProcess < Test::Unit::TestCase ensure ENV["hmm"] = old end + + assert_raise_with_message(ArgumentError, /fo=fo/) { + system({"fo=fo"=>"ha"}, *ENVCOMMAND) + } + assert_raise_with_message(ArgumentError, /\u{30c0}=\u{30e1}/) { + system({"\u{30c0}=\u{30e1}"=>"ha"}, *ENVCOMMAND) + } end def test_execopt_env_path @@ -459,10 +482,11 @@ class TestProcess < Test::Unit::TestCase def test_execopts_open_chdir_m17n_path with_tmpchdir {|d| Dir.mkdir "テスト" - system(*PWD, :chdir => "テスト", :out => "open_chdir_テスト") + (pwd = PWD.dup).insert(1, '-EUTF-8:UTF-8') + system(*pwd, :chdir => "テスト", :out => "open_chdir_テスト") assert_file.exist?("open_chdir_テスト") assert_file.not_exist?("テスト/open_chdir_テスト") - assert_equal("#{d}/テスト", File.read("open_chdir_テスト").chomp.encode(__ENCODING__)) + assert_equal("#{d}/テスト", File.read("open_chdir_テスト", encoding: "UTF-8").chomp) } end if windows? || Encoding.find('locale') == Encoding::UTF_8 @@ -629,7 +653,7 @@ class TestProcess < Test::Unit::TestCase rescue NotImplementedError return end - assert(FileTest.pipe?("fifo"), "should be pipe") + assert_file.pipe?("fifo") t1 = Thread.new { system(*ECHO["output to fifo"], :out=>"fifo") } @@ -675,14 +699,17 @@ class TestProcess < Test::Unit::TestCase return end IO.popen([RUBY, '-e', <<-'EOS']) {|io| + STDOUT.sync = true trap(:USR1) { print "trap\n" } + puts "start" system("cat", :in => "fifo") EOS - sleep 1 + assert_equal("start\n", io.gets) + sleep 0.2 # wait for the child to stop at opening "fifo" Process.kill(:USR1, io.pid) - sleep 1 + assert_equal("trap\n", io.readpartial(8)) File.write("fifo", "ok\n") - assert_equal("trap\nok\n", io.read) + assert_equal("ok\n", io.read) } } end unless windows? # does not support fifo @@ -755,6 +782,15 @@ class TestProcess < Test::Unit::TestCase Process.wait pid end } + + # ensure standard FDs we redirect to are blocking for compatibility + with_pipes(3) do |pipes| + src = 'p [STDIN,STDOUT,STDERR].map(&:nonblock?)' + rdr = { 0 => pipes[0][0], 1 => pipes[1][1], 2 => pipes[2][1] } + pid = spawn(RUBY, '-rio/nonblock', '-e', src, rdr) + assert_equal("[false, false, false]\n", pipes[1][0].gets) + Process.wait pid + end end end @@ -1002,6 +1038,15 @@ class TestProcess < Test::Unit::TestCase } end + def test_close_others_default_false + IO.pipe do |r,w| + w.close_on_exec = false + src = "IO.new(#{w.fileno}).puts(:hi)" + assert_equal true, system(*%W(#{RUBY} --disable=gems -e #{src})) + assert_equal "hi\n", r.gets + end + end unless windows? # passing non-stdio fds is not supported on Windows + def test_execopts_redirect_self begin with_pipe {|r, w| @@ -1372,6 +1417,14 @@ class TestProcess < Test::Unit::TestCase } end + def test_argv0_keep_alive + assert_in_out_err([], <<~REPRO, ['-'], [], "[Bug #15887]") + $0 = "diverge" + 4.times { GC.start } + puts Process.argv0 + REPRO + end + def test_status with_tmpchdir do s = run_in_child("exit 1") @@ -1384,6 +1437,8 @@ class TestProcess < Test::Unit::TestCase assert_equal(s.to_i >> 1, s >> 1) assert_equal(false, s.stopped?) assert_equal(nil, s.stopsig) + + assert_equal(s, Marshal.load(Marshal.dump(s))) end end @@ -1401,6 +1456,8 @@ class TestProcess < Test::Unit::TestCase assert_equal(expected, [s.exited?, s.signaled?, s.stopped?, s.success?], "[s.exited?, s.signaled?, s.stopped?, s.success?]") + + assert_equal(s, Marshal.load(Marshal.dump(s))) end end @@ -1415,6 +1472,27 @@ class TestProcess < Test::Unit::TestCase "[s.exited?, s.signaled?, s.stopped?, s.success?]") assert_equal("#<Process::Status: pid #{ s.pid } SIGQUIT (signal #{ s.termsig })>", s.inspect.sub(/ \(core dumped\)(?=>\z)/, '')) + + assert_equal(s, Marshal.load(Marshal.dump(s))) + end + end + + def test_status_fail + ret = Process::Status.wait($$) + assert_instance_of(Process::Status, ret) + assert_equal(-1, ret.pid) + end + + + def test_status_wait + IO.popen([RUBY, "-e", "gets"], "w") do |io| + pid = io.pid + assert_nil(Process::Status.wait(pid, Process::WNOHANG)) + io.puts + ret = Process::Status.wait(pid) + assert_instance_of(Process::Status, ret) + assert_equal(pid, ret.pid) + assert_predicate(ret, :exited?) end end @@ -1449,7 +1527,9 @@ class TestProcess < Test::Unit::TestCase def test_wait_exception bug11340 = '[ruby-dev:49176] [Bug #11340]' t0 = t1 = nil - IO.popen([RUBY, '-e', 'puts;STDOUT.flush;Thread.start{gets;exit};sleep(3)'], 'r+') do |f| + sec = 3 + code = "puts;STDOUT.flush;Thread.start{gets;exit};sleep(#{sec})" + IO.popen([RUBY, '-e', code], 'r+') do |f| pid = f.pid f.gets t0 = Time.now @@ -1463,21 +1543,35 @@ class TestProcess < Test::Unit::TestCase th.kill.join end t1 = Time.now + diff = t1 - t0 + assert_operator(diff, :<, sec, + ->{"#{bug11340}: #{diff} seconds to interrupt Process.wait"}) f.puts end - assert_operator(t1 - t0, :<, 3, - ->{"#{bug11340}: #{t1-t0} seconds to interrupt Process.wait"}) end def test_abort with_tmpchdir do s = run_in_child("abort") - assert_not_equal(0, s.exitstatus) + assert_not_predicate(s, :success?) + write_file("test-script", "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + STDERR.reopen(STDOUT) + begin + raise "[Bug #16424]" + rescue + abort + end + end; + assert_include(IO.popen([RUBY, "test-script"], &:read), "[Bug #16424]") end end def test_sleep assert_raise(ArgumentError) { sleep(1, 1) } + [-1, -1.0, -1r].each do |sec| + assert_raise_with_message(ArgumentError, /not.*negative/) { sleep(sec) } + end end def test_getpgid @@ -1511,8 +1605,16 @@ class TestProcess < Test::Unit::TestCase end def test_maxgroups - assert_kind_of(Integer, Process.maxgroups) + max = Process.maxgroups rescue NotImplementedError + else + assert_kind_of(Integer, max) + assert_predicate(max, :positive?) + skip "not limited to NGROUPS_MAX" if /darwin/ =~ RUBY_PLATFORM + gs = Process.groups + assert_operator(gs.size, :<=, max) + gs[0] ||= 0 + assert_raise(ArgumentError) {Process.groups = gs * (max / gs.size + 1)} end def test_geteuid @@ -1525,7 +1627,7 @@ class TestProcess < Test::Unit::TestCase end def test_seteuid_name - user = ENV["USER"] or return + user = (Etc.getpwuid(Process.euid).name rescue ENV["USER"]) or return assert_nothing_raised(TypeError) {Process.euid = user} rescue NotImplementedError end @@ -1535,10 +1637,39 @@ class TestProcess < Test::Unit::TestCase end def test_setegid + skip "root can use Process.egid on Android platform" if RUBY_PLATFORM =~ /android/ assert_nothing_raised(TypeError) {Process.egid += 0} rescue NotImplementedError end + if Process::UID.respond_to?(:from_name) + def test_uid_from_name + if u = Etc.getpwuid(Process.uid) + assert_equal(Process.uid, Process::UID.from_name(u.name), u.name) + end + assert_raise_with_message(ArgumentError, /\u{4e0d 5b58 5728}/) { + Process::UID.from_name("\u{4e0d 5b58 5728}") + } + end + end + + if Process::GID.respond_to?(:from_name) && !RUBY_PLATFORM.include?("android") + def test_gid_from_name + if g = Etc.getgrgid(Process.gid) + assert_equal(Process.gid, Process::GID.from_name(g.name), g.name) + end + expected_excs = [ArgumentError] + expected_excs << Errno::ENOENT if defined?(Errno::ENOENT) + expected_excs << Errno::ESRCH if defined?(Errno::ESRCH) # WSL 2 actually raises Errno::ESRCH + expected_excs << Errno::EBADF if defined?(Errno::EBADF) + expected_excs << Errno::EPERM if defined?(Errno::EPERM) + exc = assert_raise(*expected_excs) do + Process::GID.from_name("\u{4e0d 5b58 5728}") # fu son zai ("absent" in Kanji) + end + assert_match(/\u{4e0d 5b58 5728}/, exc.message) if exc.is_a?(ArgumentError) + end + end + def test_uid_re_exchangeable_p r = Process::UID.re_exchangeable? assert_include([true, false], r) @@ -1570,19 +1701,28 @@ class TestProcess < Test::Unit::TestCase skip "this fails on FreeBSD and OpenBSD on multithreaded environment" end signal_received = [] - Signal.trap(:CHLD) { signal_received << true } - pid = nil - IO.pipe do |r, w| - pid = fork { r.read(1); exit } - Thread.start { raise } - w.puts + IO.pipe do |sig_r, sig_w| + Signal.trap(:CHLD) do + signal_received << true + sig_w.write('?') + end + pid = nil + IO.pipe do |r, w| + pid = fork { r.read(1); exit } + Thread.start { + Thread.current.report_on_exception = false + raise + } + w.puts + end + Process.wait pid + assert_send [sig_r, :wait_readable, 5], 'self-pipe not readable' end - Process.wait pid - 10.times do - break unless signal_received.empty? - sleep 0.01 + if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # checking -DMJIT_FORCE_ENABLE. It may trigger extra SIGCHLD. + assert_equal [true], signal_received.uniq, "[ruby-core:19744]" + else + assert_equal [true], signal_received, "[ruby-core:19744]" end - assert_equal [true], signal_received, " [ruby-core:19744]" rescue NotImplementedError, ArgumentError ensure begin @@ -1592,6 +1732,9 @@ class TestProcess < Test::Unit::TestCase end def test_no_curdir + if /solaris/i =~ RUBY_PLATFORM + skip "Temporary skip to avoid CI failures after commit to use realpath on required files" + end with_tmpchdir {|d| Dir.mkdir("vd") status = nil @@ -1631,6 +1774,9 @@ class TestProcess < Test::Unit::TestCase end def test_aspawn_too_long_path + if /solaris/i =~ RUBY_PLATFORM && !defined?(Process::RLIMIT_NPROC) + skip "Too exhaustive test on platforms without Process::RLIMIT_NPROC such as Solaris 10" + end bug4315 = '[ruby-core:34833] #7904 [ruby-core:52628] #11613' assert_fail_too_long_path(%w"echo |", bug4315) end @@ -1640,6 +1786,7 @@ class TestProcess < Test::Unit::TestCase min = 1_000 / (cmd.size + sep.size) cmds = Array.new(min, cmd) exs = [Errno::ENOENT] + exs << Errno::EINVAL if windows? exs << Errno::E2BIG if defined?(Errno::E2BIG) opts = {[STDOUT, STDERR]=>File::NULL} opts[:rlimit_nproc] = 128 if defined?(Process::RLIMIT_NPROC) @@ -1670,7 +1817,7 @@ class TestProcess < Test::Unit::TestCase with_tmpchdir do assert_nothing_raised('[ruby-dev:12261]') do - Timeout.timeout(3) do + EnvUtil.timeout(3) do pid = spawn('yes | ls') Process.waitpid pid end @@ -1691,6 +1838,8 @@ class TestProcess < Test::Unit::TestCase end def test_daemon_noclose + pend "macOS 15 beta is not working with this test" if /darwin/ =~ RUBY_PLATFORM && /15/ =~ `sw_vers -productVersion` + data = IO.popen("-", "r+") do |f| break f.read if f Process.daemon(false, true) @@ -1737,12 +1886,12 @@ class TestProcess < Test::Unit::TestCase puts Dir.entries("/proc/self/task") - %W[. ..] end bug4920 = '[ruby-dev:43873]' - assert_equal(2, data.size, bug4920) + assert_include(1..2, data.size, bug4920) assert_not_include(data.map(&:to_i), pid) end else # darwin def test_daemon_no_threads - data = Timeout.timeout(3) do + data = EnvUtil.timeout(3) do IO.popen("-") do |f| break f.readlines.map(&:chomp) if f th = Thread.start {sleep 3} @@ -1791,6 +1940,16 @@ class TestProcess < Test::Unit::TestCase end end + def test_popen_reopen + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + io = File.open(IO::NULL) + io2 = io.dup + IO.popen("echo") {|f| io.reopen(f)} + io.reopen(io2) + end; + end + def test_execopts_new_pgroup return unless windows? @@ -1801,22 +1960,23 @@ class TestProcess < Test::Unit::TestCase end def test_execopts_uid + skip "root can use uid option of Kernel#system on Android platform" if RUBY_PLATFORM =~ /android/ feature6975 = '[ruby-core:47414]' [30000, [Process.uid, ENV["USER"]]].each do |uid, user| if user assert_nothing_raised(feature6975) do begin - system(*TRUECOMMAND, uid: user) - rescue Errno::EPERM, NotImplementedError + system(*TRUECOMMAND, uid: user, exception: true) + rescue Errno::EPERM, Errno::EACCES, NotImplementedError end end end assert_nothing_raised(feature6975) do begin - system(*TRUECOMMAND, uid: uid) - rescue Errno::EPERM, NotImplementedError + system(*TRUECOMMAND, uid: uid, exception: true) + rescue Errno::EPERM, Errno::EACCES, NotImplementedError end end @@ -1824,7 +1984,7 @@ class TestProcess < Test::Unit::TestCase begin u = IO.popen([RUBY, "-e", "print Process.uid", uid: user||uid], &:read) assert_equal(uid.to_s, u, feature6975) - rescue Errno::EPERM, NotImplementedError + rescue Errno::EPERM, Errno::EACCES, NotImplementedError end end end @@ -1832,9 +1992,15 @@ class TestProcess < Test::Unit::TestCase def test_execopts_gid skip "Process.groups not implemented on Windows platform" if windows? + skip "root can use Process.groups on Android platform" if RUBY_PLATFORM =~ /android/ feature6975 = '[ruby-core:47414]' - [30000, *Process.groups.map {|g| g = Etc.getgrgid(g); [g.name, g.gid]}].each do |group, gid| + groups = Process.groups.map do |g| + g = Etc.getgrgid(g) rescue next + [g.name, g.gid] + end + groups.compact! + [30000, *groups].each do |group, gid| assert_nothing_raised(feature6975) do begin system(*TRUECOMMAND, gid: group) @@ -2010,7 +2176,11 @@ EOS def test_clock_gettime_GETTIMEOFDAY_BASED_CLOCK_REALTIME n = :GETTIMEOFDAY_BASED_CLOCK_REALTIME - t = Process.clock_gettime(n) + begin + t = Process.clock_gettime(n) + rescue Errno::EINVAL + return + end assert_kind_of(Float, t, "Process.clock_gettime(:#{n})") end @@ -2088,7 +2258,11 @@ EOS def test_clock_getres_GETTIMEOFDAY_BASED_CLOCK_REALTIME n = :GETTIMEOFDAY_BASED_CLOCK_REALTIME - t = Process.clock_getres(n) + begin + t = Process.clock_getres(n) + rescue Errno::EINVAL + return + end assert_kind_of(Float, t, "Process.clock_getres(:#{n})") assert_equal(1000, Process.clock_getres(n, :nanosecond)) end @@ -2154,7 +2328,7 @@ EOS end def test_deadlock_by_signal_at_forking - assert_separately(["-", RUBY], <<-INPUT, timeout: 80) + assert_separately(%W(--disable=gems - #{RUBY}), <<-INPUT, timeout: 100) ruby = ARGV.shift GC.start # reduce garbage GC.disable # avoid triggering CoW after forks @@ -2162,10 +2336,8 @@ EOS parent = $$ 100.times do |i| pid = fork {Process.kill(:QUIT, parent)} - IO.popen(ruby, 'r+'){} + IO.popen([ruby, -'--disable=gems'], -'r+'){} Process.wait(pid) - $stdout.puts - $stdout.flush end INPUT end if defined?(fork) @@ -2175,7 +2347,7 @@ EOS th = Process.detach(pid) assert_equal pid, th.pid status = th.value - assert status.success?, status.inspect + assert_predicate status, :success? end if defined?(fork) def test_kill_at_spawn_failure @@ -2183,7 +2355,9 @@ EOS th = nil x = with_tmpchdir {|d| prog = "#{d}/notexist" - th = Thread.start {system(prog);sleep} + q = Thread::Queue.new + th = Thread.start {system(prog);q.push(nil);sleep} + q.pop th.kill th.join(0.1) } @@ -2234,7 +2408,7 @@ EOS def test_signals_work_after_exec_fail r, w = IO.pipe pid = status = nil - Timeout.timeout(30) do + EnvUtil.timeout(30) do pid = fork do r.close begin @@ -2268,7 +2442,7 @@ EOS def test_threading_works_after_exec_fail r, w = IO.pipe pid = status = nil - Timeout.timeout(30) do + EnvUtil.timeout(90) do pid = fork do r.close begin @@ -2276,16 +2450,16 @@ EOS rescue SystemCallError w.syswrite("exec failed\n") end - run = true - th1 = Thread.new { i = 0; i += 1 while run; i } - th2 = Thread.new { j = 0; j += 1 while run && Thread.pass.nil?; j } + q = Thread::Queue.new + th1 = Thread.new { i = 0; i += 1 while q.empty?; i } + th2 = Thread.new { j = 0; j += 1 while q.empty? && Thread.pass.nil?; j } sleep 0.5 - run = false + q << true w.syswrite "#{th1.value} #{th2.value}\n" end w.close assert_equal "exec failed\n", r.gets - vals = r.gets.chomp.split.map!(&:to_i) + vals = r.gets.split.map!(&:to_i) assert_operator vals[0], :>, vals[1], vals.inspect _, status = Process.waitpid2(pid) end @@ -2301,6 +2475,15 @@ EOS r.close if r end if defined?(fork) + def test_rescue_exec_fail + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + assert_raise(Errno::ENOENT) do + exec("", in: "") + end + end; + end + def test_many_args bug11418 = '[ruby-core:70251] [Bug #11418]' assert_in_out_err([], <<-"end;", ["x"]*256, [], bug11418, timeout: 60) @@ -2338,4 +2521,177 @@ EOS assert_equal pid, Timeout.timeout(30) { Process.wait(pid) } end; end + + if Process.respond_to?(:initgroups) + def test_initgroups + assert_raise(ArgumentError) do + Process.initgroups("\0", 0) + end + end + end + + def test_last_status + Process.wait spawn(RUBY, "-e", "exit 13") + assert_same(Process.last_status, $?) + end + + def test_last_status_failure + assert_nil system("sad") + assert_not_predicate $?, :success? + assert_equal $?.exitstatus, 127 + end + + def test_exec_failure_leaves_no_child + assert_raise(Errno::ENOENT) do + spawn('inexistent_command') + end + assert_empty(Process.waitall) + end + + def test__fork + r, w = IO.pipe + pid = Process._fork + if pid == 0 + begin + r.close + w << "ok: #$$" + w.close + ensure + exit! + end + else + w.close + assert_equal("ok: #{pid}", r.read) + r.close + Process.waitpid(pid) + end + end if Process.respond_to?(:_fork) + + def test__fork_hook + %w(fork Process.fork).each do |method| + feature17795 = '[ruby-core:103400] [Feature #17795]' + assert_in_out_err([], <<-"end;", [], [], feature17795, timeout: 60) do |r, e| + module ForkHook + def _fork + p :before + ret = super + p :after + ret + end + end + + Process.singleton_class.prepend(ForkHook) + + pid = #{ method } + p pid + Process.waitpid(pid) if pid + end; + assert_equal([], e) + assert_equal(":before", r.shift) + assert_equal(":after", r.shift) + s = r.map {|s| s.chomp }.sort #=> [pid, ":after", "nil"] + assert_match(/^\d+$/, s[0]) # pid + assert_equal(":after", s[1]) + assert_equal("nil", s[2]) + end + end + end if Process.respond_to?(:_fork) + + def test__fork_hook_popen + feature17795 = '[ruby-core:103400] [Feature #17795]' + assert_in_out_err([], <<-"end;", %w(:before :after :after foo bar), [], feature17795, timeout: 60) + module ForkHook + def _fork + p :before + ret = super + p :after + ret + end + end + + Process.singleton_class.prepend(ForkHook) + + IO.popen("-") {|io| + if !io + puts "foo" + else + puts io.read + "bar" + end + } + end; + end if Process.respond_to?(:_fork) + + def test__fork_wrong_type_hook + feature17795 = '[ruby-core:103400] [Feature #17795]' + assert_in_out_err([], <<-"end;", ["OK"], [], feature17795, timeout: 60) + module ForkHook + def _fork + "BOO" + end + end + + Process.singleton_class.prepend(ForkHook) + + begin + fork + rescue TypeError + puts "OK" + end + end; + end if Process.respond_to?(:_fork) + + def test_concurrent_group_and_pid_wait + # Use a pair of pipes that will make long_pid exit when this test exits, to avoid + # leaking temp processes. + long_rpipe, long_wpipe = IO.pipe + short_rpipe, short_wpipe = IO.pipe + # This process should run forever + long_pid = fork do + [short_rpipe, short_wpipe, long_wpipe].each(&:close) + long_rpipe.read + end + # This process will exit + short_pid = fork do + [long_rpipe, long_wpipe, short_wpipe].each(&:close) + short_rpipe.read + end + t1, t2, t3 = nil + EnvUtil.timeout(5) do + t1 = Thread.new do + Process.waitpid long_pid + end + # Wait for us to be blocking in a call to waitpid2 + Thread.pass until t1.stop? + short_wpipe.close # Make short_pid exit + + # The short pid has exited, so -1 should pick that up. + assert_equal short_pid, Process.waitpid(-1) + + # Terminate t1 for the next phase of the test. + t1.kill + t1.join + + t2 = Thread.new do + Process.waitpid -1 + rescue Errno::ECHILD + nil + end + Thread.pass until t2.stop? + t3 = Thread.new do + Process.waitpid long_pid + rescue Errno::ECHILD + nil + end + Thread.pass until t3.stop? + + # it's actually nondeterministic which of t2 or t3 will receive the wait (this + # nondeterminism comes from the behaviour of the underlying system calls) + long_wpipe.close + assert_equal [long_pid], [t2, t3].map(&:value).compact + end + ensure + [t1, t2, t3].each { _1&.kill rescue nil } + [t1, t2, t3].each { _1&.join rescue nil } + [long_rpipe, long_wpipe, short_rpipe, short_wpipe].each { _1&.close rescue nil } + end if defined?(fork) end |
