diff options
Diffstat (limited to 'test/ruby/test_process.rb')
| -rw-r--r-- | test/ruby/test_process.rb | 1507 |
1 files changed, 1229 insertions, 278 deletions
diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index a99e979e88..b3a88b664c 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -1,7 +1,8 @@ +# coding: utf-8 +# frozen_string_literal: false require 'test/unit' require 'tempfile' require 'timeout' -require_relative 'envutil' require 'rbconfig' class TestProcess < Test::Unit::TestCase @@ -22,12 +23,6 @@ class TestProcess < Test::Unit::TestCase return /mswin|mingw|bccwin/ =~ RUBY_PLATFORM end - def write_file(filename, content) - File.open(filename, "w") {|f| - f << content - } - end - def with_tmpchdir Dir.mktmpdir {|d| d = File.realpath(d) @@ -38,7 +33,7 @@ class TestProcess < Test::Unit::TestCase end def run_in_child(str) # should be called in a temporary directory - write_file("test-script", str) + File.write("test-script", str) Process.wait spawn(RUBY, "test-script") $? end @@ -63,10 +58,15 @@ class TestProcess < Test::Unit::TestCase def test_rlimit_nofile return unless rlimit_exist? + omit "LSAN needs to open proc file" if Test::Sanitizers.lsan_enabled? + with_tmpchdir { - write_file 's', <<-"End" - # if limit=0, this test freeze pn OpenBSD - limit = /openbsd/ =~ RUBY_PLATFORM ? 1 : 0 + File.write 's', <<-"End" + # Too small RLIMIT_NOFILE, such as zero, causes problems. + # [OpenBSD] Setting to zero freezes this test. + # [GNU/Linux] EINVAL on poll(). EINVAL on ruby's internal poll() ruby with "[ASYNC BUG] thread_timer: select". + pipes = IO.pipe + limit = pipes.map {|io| io.fileno }.min result = 1 begin Process.setrlimit(Process::RLIMIT_NOFILE, limit) @@ -116,11 +116,20 @@ class TestProcess < Test::Unit::TestCase } assert_raise(ArgumentError) { Process.getrlimit(:FOO) } assert_raise(ArgumentError) { Process.getrlimit("FOO") } + + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.getrlimit("\u{30eb 30d3 30fc}") } + end end def test_rlimit_value return unless rlimit_exist? + assert_raise(ArgumentError) { Process.setrlimit(:FOO, 0) } assert_raise(ArgumentError) { Process.setrlimit(:CORE, :FOO) } + EnvUtil.with_default_internal(Encoding::UTF_8) do + assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.setrlimit("\u{30eb 30d3 30fc}", 0) } + end + assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.setrlimit(:CORE, "\u{30eb 30d3 30fc}") } with_tmpchdir do s = run_in_child(<<-'End') cur, max = Process.getrlimit(:NOFILE) @@ -131,7 +140,7 @@ class TestProcess < Test::Unit::TestCase exit false end End - assert_not_equal(0, s.exitstatus) + assert_not_predicate(s, :success?) s = run_in_child(<<-'End') cur, max = Process.getrlimit(:NOFILE) Process.setrlimit(:NOFILE, [max-10, cur].min) @@ -141,7 +150,7 @@ class TestProcess < Test::Unit::TestCase exit false end End - assert_not_equal(0, s.exitstatus) + assert_not_predicate(s, :success?) end end @@ -160,7 +169,7 @@ class TestProcess < Test::Unit::TestCase end def test_execopts_pgroup - skip "system(:pgroup) is not supported" if windows? + omit "system(:pgroup) is not supported" if windows? assert_nothing_raised { system(*TRUECOMMAND, :pgroup=>false) } io = IO.popen([RUBY, "-e", "print Process.getpgrp"]) @@ -172,7 +181,11 @@ class TestProcess < Test::Unit::TestCase io.close assert_raise(ArgumentError) { system(*TRUECOMMAND, :pgroup=>-1) } - assert_raise(Errno::EPERM) { Process.wait spawn(*TRUECOMMAND, :pgroup=>2) } + IO.popen([RUBY, '-egets'], 'w') do |f| + assert_raise(Errno::EPERM) { + Process.wait spawn(*TRUECOMMAND, :pgroup=>f.pid) + } + end io1 = IO.popen([RUBY, "-e", "print Process.getpgrp", :pgroup=>true]) io2 = IO.popen([RUBY, "-e", "print Process.getpgrp", :pgroup=>io1.pid]) @@ -193,56 +206,98 @@ class TestProcess < Test::Unit::TestCase max = Process.getrlimit(:CORE).last + # When running under ASAN, we need to set disable_coredump=0 for this test; by default + # the ASAN runtime library sets RLIMIT_CORE to 0, "to avoid dumping a 16T+ core file", and + # that inteferes with this test. + asan_options = ENV['ASAN_OPTIONS'] || '' + asan_options << ':' unless asan_options.empty? + env = { + 'ASAN_OPTIONS' => "#{asan_options}disable_coredump=0" + } + n = max - IO.popen([RUBY, "-e", - "p Process.getrlimit(:CORE)", :rlimit_core=>n]) {|io| - assert_equal("[#{n}, #{n}]\n", io.read) + IO.popen([env, RUBY, "-e", + "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) + IO.popen([env, RUBY, "-e", + "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) + IO.popen([env, RUBY, "-e", + "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) + IO.popen([env, RUBY, "-e", + "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) + IO.popen([env, RUBY, "-e", + "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)", + IO.popen([env, RUBY, "-e", + "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(env, RUBY, '-e', 'exit', 'rlimit_bogus'.to_sym => 123) + end + assert_separately([env],"#{<<~"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_with_message(ArgumentError, /bogus/) do + system(env, RUBY, '-e', 'exit', :rlimit_bogus => 123) + end + + assert_raise_with_message(ArgumentError, /rlimit_cpu/) { + system(env, RUBY, '-e', 'exit', "rlimit_cpu\0".to_sym => 3600) } end - MANDATORY_ENVS = %w[RUBYLIB] - case RbConfig::CONFIG['target_os'] - when /linux/ - MANDATORY_ENVS << 'LD_PRELOAD' - when /mswin|mingw/ - MANDATORY_ENVS.concat(%w[HOME USER TMPDIR]) - when /darwin/ - MANDATORY_ENVS.concat(ENV.keys.grep(/\A__CF_/)) + 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 GEM_HOME GEM_PATH RUBY_FREE_AT_EXIT] if e = RbConfig::CONFIG['LIBPATHENV'] MANDATORY_ENVS << e end + if e = RbConfig::CONFIG['PRELOADENV'] and !e.empty? + MANDATORY_ENVS << e + end + case RbConfig::CONFIG['target_os'] + when /mswin|mingw/ + MANDATORY_ENVS.concat(%w[HOME USER TMPDIR PROCESSOR_ARCHITECTURE]) + when /darwin/ + MANDATORY_ENVS.concat(%w[TMPDIR], ENV.keys.grep(/\A__CF_/)) + # IO.popen([ENV.keys.to_h {|e| [e, nil]}, + # RUBY, "-e", %q[print ENV.keys.join(?\0)]], + # &:read).split(?\0) + 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) @@ -305,6 +360,23 @@ 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 + bug8004 = '[ruby-core:53103] [Bug #8004]' + Dir.mktmpdir do |d| + File.write("#{d}/tmp_script.cmd", ": ;\n", perm: 0o755) + assert_not_nil(pid = Process.spawn({"PATH" => d}, "tmp_script.cmd"), bug8004) + wpid, st = Process.waitpid2(pid) + assert_equal([pid, true], [wpid, st.success?], bug8004) + end end def _test_execopts_env_popen(cmd) @@ -338,7 +410,7 @@ class TestProcess < Test::Unit::TestCase def test_execopts_env_popen_string with_tmpchdir do |d| - open('test-script', 'w') do |f| + File.open('test-script', 'w') do |f| ENVCOMMAND.each_with_index do |cmd, i| next if i.zero? or cmd == "-e" f.puts cmd @@ -350,16 +422,14 @@ class TestProcess < Test::Unit::TestCase def test_execopts_preserve_env_on_exec_failure with_tmpchdir {|d| - write_file 's', <<-"End" + File.write 's', <<-"End" ENV["mgg"] = nil prog = "./nonexistent" begin Process.exec({"mgg" => "mggoo"}, [prog, prog]) rescue Errno::ENOENT end - open('out', 'w') {|f| - f.print ENV["mgg"].inspect - } + File.write('out', ENV["mgg"].inspect) End system(RUBY, 's') assert_equal(nil.inspect, File.read('out'), @@ -369,9 +439,7 @@ class TestProcess < Test::Unit::TestCase def test_execopts_env_single_word with_tmpchdir {|d| - open("test_execopts_env_single_word.rb", "w") {|f| - f.puts "print ENV['hgga']" - } + File.write("test_execopts_env_single_word.rb", "print ENV['hgga']\n") system({"hgga"=>"ugu"}, RUBY, :in => 'test_execopts_env_single_word.rb', :out => 'test_execopts_env_single_word.out') @@ -397,9 +465,13 @@ class TestProcess < Test::Unit::TestCase IO.popen([*PWD, :chdir => d]) {|io| assert_equal(d, io.read.chomp) } - assert_raise(Errno::ENOENT) { + assert_raise_with_message(Errno::ENOENT, %r"d/notexist") { Process.wait Process.spawn(*PWD, :chdir => "d/notexist") } + n = "d/\u{1F37A}" + assert_raise_with_message(Errno::ENOENT, /#{n}/) { + Process.wait Process.spawn(*PWD, :chdir => n) + } } end @@ -413,10 +485,39 @@ class TestProcess < Test::Unit::TestCase } end + def test_execopts_open_chdir_m17n_path + with_tmpchdir {|d| + Dir.mkdir "テスト" + (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_テスト", encoding: "UTF-8").chomp) + } + end if windows? || Encoding.find('locale') == Encoding::UTF_8 + + def test_execopts_open_failure + with_tmpchdir {|d| + assert_raise_with_message(Errno::ENOENT, %r"d/notexist") { + Process.wait Process.spawn(*PWD, :in => "d/notexist") + } + assert_raise_with_message(Errno::ENOENT, %r"d/notexist") { + Process.wait Process.spawn(*PWD, :out => "d/notexist") + } + n = "d/\u{1F37A}" + assert_raise_with_message(Errno::ENOENT, /#{n}/) { + Process.wait Process.spawn(*PWD, :in => n) + } + assert_raise_with_message(Errno::ENOENT, /#{n}/) { + Process.wait Process.spawn(*PWD, :out => n) + } + } + end + UMASK = [RUBY, '-e', 'printf "%04o\n", File.umask'] def test_execopts_umask - skip "umask is not supported" if windows? + omit "umask is not supported" if windows? IO.popen([*UMASK, :umask => 0]) {|io| assert_equal("0000", io.read.chomp) } @@ -454,13 +555,13 @@ class TestProcess < Test::Unit::TestCase SORT = [RUBY, '-e', "puts ARGF.readlines.sort"] CAT = [RUBY, '-e', "IO.copy_stream STDIN, STDOUT"] - def test_execopts_redirect + def test_execopts_redirect_fd with_tmpchdir {|d| Process.wait Process.spawn(*ECHO["a"], STDOUT=>["out", File::WRONLY|File::CREAT|File::TRUNC, 0644]) assert_equal("a", File.read("out").chomp) if windows? # currently telling to child the file modes is not supported. - open("out", "a") {|f| f.write "0\n"} + File.write("out", "0\n", mode: "a") else Process.wait Process.spawn(*ECHO["0"], STDOUT=>["out", File::WRONLY|File::CREAT|File::APPEND, 0644]) assert_equal("a\n0\n", File.read("out")) @@ -526,76 +627,199 @@ class TestProcess < Test::Unit::TestCase assert_equal("bb\naa\n", File.read("out")) system(*SORT, STDIN=>["out"], STDOUT=>"out2") assert_equal("aa\nbb\n", File.read("out2")) + } + end + + def test_execopts_redirect_open_order_normal + minfd = 3 + maxfd = 20 + with_tmpchdir {|d| + opts = {} + minfd.upto(maxfd) {|fd| opts[fd] = ["out#{fd}", "w"] } + system RUBY, "-e", "#{minfd}.upto(#{maxfd}) {|fd| IO.new(fd).print fd.to_s }", opts + minfd.upto(maxfd) {|fd| assert_equal(fd.to_s, File.read("out#{fd}")) } + } + end unless windows? # passing non-stdio fds is not supported on Windows + + def test_execopts_redirect_open_order_reverse + minfd = 3 + maxfd = 20 + with_tmpchdir {|d| + opts = {} + maxfd.downto(minfd) {|fd| opts[fd] = ["out#{fd}", "w"] } + system RUBY, "-e", "#{minfd}.upto(#{maxfd}) {|fd| IO.new(fd).print fd.to_s }", opts + minfd.upto(maxfd) {|fd| assert_equal(fd.to_s, File.read("out#{fd}")) } + } + end unless windows? # passing non-stdio fds is not supported on Windows + + def test_execopts_redirect_open_fifo + with_tmpchdir {|d| + begin + File.mkfifo("fifo") + rescue NotImplementedError + return + end + assert_file.pipe?("fifo") + t1 = Thread.new { + system(*ECHO["output to fifo"], :out=>"fifo") + } + t2 = Thread.new { + IO.popen([*CAT, :in=>"fifo"]) {|f| f.read } + } + _, v2 = assert_join_threads([t1, t2]) + assert_equal("output to fifo\n", v2) + } + end unless windows? # does not support fifo - with_pipe {|r1, w1| - with_pipe {|r2, w2| - opts = {STDIN=>r1, STDOUT=>w2} - opts.merge(w1=>:close, r2=>:close) unless windows? - pid = spawn(*SORT, opts) - r1.close - w2.close - w1.puts "c" - w1.puts "a" - w1.puts "b" - w1.close - assert_equal("a\nb\nc\n", r2.read) - r2.close - Process.wait(pid) + def test_execopts_redirect_open_fifo_interrupt_raise + pid = nil + with_tmpchdir {|d| + begin + File.mkfifo("fifo") + rescue NotImplementedError + return + end + IO.popen([RUBY, '-e', <<-'EOS']) {|io| + class E < StandardError; end + trap(:USR1) { raise E } + begin + puts "start" + STDOUT.flush + system("cat", :in => "fifo") + rescue E + puts "ok" + end + EOS + pid = io.pid + assert_equal("start\n", io.gets) + sleep 0.5 + Process.kill(:USR1, io.pid) + assert_equal("ok\n", io.read) + } + assert_equal(pid, $?.pid) + assert_predicate($?, :success?) + } + ensure + assert_raise(Errno::ESRCH) {Process.kill(:KILL, pid)} if pid + end unless windows? # does not support fifo + + def test_execopts_redirect_open_fifo_interrupt_print + pid = nil + with_tmpchdir {|d| + begin + File.mkfifo("fifo") + rescue NotImplementedError + return + end + IO.popen([RUBY, '-e', <<-'EOS']) {|io| + STDOUT.sync = true + trap(:USR1) { print "trap\n" } + puts "start" + system("cat", :in => "fifo") + EOS + pid = io.pid + assert_equal("start\n", io.gets) + sleep 0.2 # wait for the child to stop at opening "fifo" + Process.kill(:USR1, io.pid) + assert_equal("trap\n", io.readpartial(8)) + sleep 0.2 # wait for the child to return to opening "fifo". + # On arm64-darwin22, often deadlocks while the child is + # opening "fifo". Not sure to where "ok" line being written + # at the next has gone. + File.write("fifo", "ok\n") + assert_equal("ok\n", io.read) + } + assert_equal(pid, $?.pid) + assert_predicate($?, :success?) + } + ensure + if pid + assert_raise(Errno::ESRCH) {Process.kill(:KILL, pid)} + end + end unless windows? # does not support fifo + + def test_execopts_redirect_pipe + with_pipe {|r1, w1| + with_pipe {|r2, w2| + opts = {STDIN=>r1, STDOUT=>w2} + opts.merge(w1=>:close, r2=>:close) unless windows? + pid = spawn(*SORT, opts) + r1.close + w2.close + w1.puts "c" + w1.puts "a" + w1.puts "b" + w1.close + assert_equal("a\nb\nc\n", r2.read) + r2.close + Process.wait(pid) + } + } + + unless windows? + # passing non-stdio fds is not supported on Windows + with_pipes(5) {|pipes| + ios = pipes.flatten + h = {} + ios.length.times {|i| h[ios[i]] = ios[(i-1)%ios.length] } + h2 = h.invert + _rios = pipes.map {|r, w| r } + wios = pipes.map {|r, w| w } + child_wfds = wios.map {|w| h2[w].fileno } + pid = spawn(RUBY, "-e", + "[#{child_wfds.join(',')}].each {|fd| IO.new(fd, 'w').puts fd }", h) + pipes.each {|r, w| + assert_equal("#{h2[w].fileno}\n", r.gets) } + Process.wait pid; } - unless windows? - # passing non-stdio fds is not supported on Windows - with_pipes(5) {|pipes| - ios = pipes.flatten - h = {} - ios.length.times {|i| h[ios[i]] = ios[(i-1)%ios.length] } - h2 = h.invert - _rios = pipes.map {|r, w| r } - wios = pipes.map {|r, w| w } - child_wfds = wios.map {|w| h2[w].fileno } - pid = spawn(RUBY, "-e", - "[#{child_wfds.join(',')}].each {|fd| IO.new(fd, 'w').puts fd }", h) - pipes.each {|r, w| - assert_equal("#{h2[w].fileno}\n", r.gets) - } - Process.wait pid; + with_pipes(5) {|pipes| + ios = pipes.flatten + h = {} + ios.length.times {|i| h[ios[i]] = ios[(i+1)%ios.length] } + h2 = h.invert + _rios = pipes.map {|r, w| r } + wios = pipes.map {|r, w| w } + child_wfds = wios.map {|w| h2[w].fileno } + pid = spawn(RUBY, "-e", + "[#{child_wfds.join(',')}].each {|fd| IO.new(fd, 'w').puts fd }", h) + pipes.each {|r, w| + assert_equal("#{h2[w].fileno}\n", r.gets) } + Process.wait pid + } + + closed_fd = nil + with_pipes(5) {|pipes| + io = pipes.last.last + closed_fd = io.fileno + } + assert_raise(Errno::EBADF) { Process.wait spawn(*TRUECOMMAND, closed_fd=>closed_fd) } - with_pipes(5) {|pipes| - ios = pipes.flatten - h = {} - ios.length.times {|i| h[ios[i]] = ios[(i+1)%ios.length] } - h2 = h.invert - _rios = pipes.map {|r, w| r } - wios = pipes.map {|r, w| w } - child_wfds = wios.map {|w| h2[w].fileno } - pid = spawn(RUBY, "-e", - "[#{child_wfds.join(',')}].each {|fd| IO.new(fd, 'w').puts fd }", h) - pipes.each {|r, w| - assert_equal("#{h2[w].fileno}\n", r.gets) - } + with_pipe {|r, w| + if w.respond_to?(:"close_on_exec=") + w.close_on_exec = true + pid = spawn(RUBY, "-e", "IO.new(#{w.fileno}, 'w').print 'a'", w=>w) + w.close + assert_equal("a", r.read) Process.wait pid - } + end + } - closed_fd = nil - with_pipes(5) {|pipes| - io = pipes.last.last - closed_fd = io.fileno - } - assert_raise(Errno::EBADF) { Process.wait spawn(*TRUECOMMAND, closed_fd=>closed_fd) } - - with_pipe {|r, w| - if w.respond_to?(:"close_on_exec=") - w.close_on_exec = true - pid = spawn(RUBY, "-e", "IO.new(#{w.fileno}, 'w').print 'a'", w=>w) - w.close - assert_equal("a", r.read) - 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 + def test_execopts_redirect_symbol + with_tmpchdir {|d| system(*ECHO["funya"], :out=>"out") assert_equal("funya\n", File.read("out")) system(RUBY, '-e', 'STDOUT.reopen(STDERR); puts "henya"', :err=>"out") @@ -606,6 +830,27 @@ class TestProcess < Test::Unit::TestCase } end + def test_execopts_redirect_nonascii_path + bug9946 = '[ruby-core:63185] [Bug #9946]' + with_tmpchdir {|d| + path = "t-\u{30c6 30b9 30c8 f6}.txt" + system(*ECHO["a"], out: path) + assert_file.for(bug9946).exist?(path) + assert_equal("a\n", File.read(path), bug9946) + } + end + + def test_execopts_redirect_to_out_and_err + with_tmpchdir {|d| + ret = system(RUBY, "-e", 'STDERR.print "e"; STDOUT.print "o"', [:out, :err] => "foo") + assert_equal(true, ret) + assert_equal("eo", File.read("foo")) + ret = system(RUBY, "-e", 'STDERR.print "E"; STDOUT.print "O"', [:err, :out] => "bar") + assert_equal(true, ret) + assert_equal("EO", File.read("bar")) + } + end + def test_execopts_redirect_dup2_child with_tmpchdir {|d| Process.wait spawn(RUBY, "-e", "STDERR.print 'err'; STDOUT.print 'out'", @@ -616,7 +861,7 @@ class TestProcess < Test::Unit::TestCase STDERR=>"out", STDOUT=>[:child, STDERR]) assert_equal("errout", File.read("out")) - skip "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? + omit "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? Process.wait spawn(RUBY, "-e", "STDERR.print 'err'; STDOUT.print 'out'", STDOUT=>"out", STDERR=>[:child, 3], @@ -638,7 +883,7 @@ class TestProcess < Test::Unit::TestCase def test_execopts_exec with_tmpchdir {|d| - write_file("s", 'exec "echo aaa", STDOUT=>"foo"') + File.write("s", 'exec "echo aaa", STDOUT=>"foo"') pid = spawn RUBY, 's' Process.wait pid assert_equal("aaa\n", File.read("foo")) @@ -650,6 +895,11 @@ class TestProcess < Test::Unit::TestCase IO.popen("#{RUBY} -e 'puts :foo'") {|io| assert_equal("foo\n", io.read) } assert_raise(Errno::ENOENT) { IO.popen(["echo bar"]) {} } # assuming "echo bar" command not exist. IO.popen(ECHO["baz"]) {|io| assert_equal("baz\n", io.read) } + } + end + + def test_execopts_popen_stdio + with_tmpchdir {|d| assert_raise(ArgumentError) { IO.popen([*ECHO["qux"], STDOUT=>STDOUT]) {|io| } } @@ -659,7 +909,12 @@ class TestProcess < Test::Unit::TestCase assert_raise(ArgumentError) { IO.popen([*ECHO["fuga"], STDOUT=>"out"]) {|io| } } - skip "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? + } + end + + def test_execopts_popen_extra_fd + omit "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? + with_tmpchdir {|d| with_pipe {|r, w| IO.popen([RUBY, '-e', 'IO.new(3, "w").puts("a"); puts "b"', 3=>w]) {|io| assert_equal("b\n", io.read) @@ -675,19 +930,33 @@ class TestProcess < Test::Unit::TestCase } end - def test_popen_fork - IO.popen("-") {|io| - if !io - puts "fooo" - else - assert_equal("fooo\n", io.read) + if Process.respond_to?(:fork) + def test_popen_fork + IO.popen("-") do |io| + if !io + puts "fooo" + else + assert_equal("fooo\n", io.read) + end end - } - rescue NotImplementedError + end + + def test_popen_fork_ensure + IO.popen("-") do |io| + if !io + STDERR.reopen(STDOUT) + raise "fooo" + else + assert_empty io.read + end + end + rescue RuntimeError + abort "[Bug #20995] should not reach here" + end end def test_fd_inheritance - skip "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? + omit "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? with_pipe {|r, w| system(RUBY, '-e', 'IO.new(ARGV[0].to_i, "w").puts(:ba)', w.fileno.to_s, w=>w) w.close @@ -702,7 +971,7 @@ class TestProcess < Test::Unit::TestCase } with_pipe {|r, w| with_tmpchdir {|d| - write_file("s", <<-"End") + File.write("s", <<-"End") exec(#{RUBY.dump}, '-e', 'IO.new(ARGV[0].to_i, "w").puts("bu") rescue nil', #{w.fileno.to_s.dump}, :close_others=>false) @@ -715,11 +984,14 @@ class TestProcess < Test::Unit::TestCase } with_pipe {|r, w| io = IO.popen([RUBY, "-e", "STDERR.reopen(STDOUT); IO.new(#{w.fileno}, 'w').puts('me')"]) - w.close - errmsg = io.read - assert_equal("", r.read) - assert_not_equal("", errmsg) - Process.wait + begin + w.close + errmsg = io.read + assert_equal("", r.read) + assert_not_equal("", errmsg) + ensure + io.close + end } with_pipe {|r, w| errmsg = `#{RUBY} -e "STDERR.reopen(STDOUT); IO.new(#{w.fileno}, 'w').puts(123)"` @@ -730,7 +1002,7 @@ class TestProcess < Test::Unit::TestCase end def test_execopts_close_others - skip "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? + omit "inheritance of fd other than stdin,stdout and stderr is not supported" if windows? with_tmpchdir {|d| with_pipe {|r, w| system(RUBY, '-e', 'STDERR.reopen("err", "w"); IO.new(ARGV[0].to_i, "w").puts("ma")', w.fileno.to_s, :close_others=>true) @@ -753,7 +1025,7 @@ class TestProcess < Test::Unit::TestCase assert_equal("bi\n", r.read) } with_pipe {|r, w| - write_file("s", <<-"End") + File.write("s", <<-"End") exec(#{RUBY.dump}, '-e', 'STDERR.reopen("err", "w"); IO.new(ARGV[0].to_i, "w").puts("mu")', #{w.fileno.to_s.dump}, @@ -767,34 +1039,52 @@ class TestProcess < Test::Unit::TestCase } with_pipe {|r, w| io = IO.popen([RUBY, "-e", "STDERR.reopen(STDOUT); IO.new(#{w.fileno}, 'w').puts('me')", :close_others=>true]) - w.close - errmsg = io.read - assert_equal("", r.read) - assert_not_equal("", errmsg) - Process.wait + begin + w.close + errmsg = io.read + assert_equal("", r.read) + assert_not_equal("", errmsg) + ensure + io.close + end } with_pipe {|r, w| w.close_on_exec = false io = IO.popen([RUBY, "-e", "STDERR.reopen(STDOUT); IO.new(#{w.fileno}, 'w').puts('mo')", :close_others=>false]) - w.close - errmsg = io.read - assert_equal("mo\n", r.read) - assert_equal("", errmsg) - Process.wait + begin + w.close + errmsg = io.read + assert_equal("mo\n", r.read) + assert_equal("", errmsg) + ensure + io.close + end } with_pipe {|r, w| w.close_on_exec = false io = IO.popen([RUBY, "-e", "STDERR.reopen(STDOUT); IO.new(#{w.fileno}, 'w').puts('mo')", :close_others=>nil]) - w.close - errmsg = io.read - assert_equal("mo\n", r.read) - assert_equal("", errmsg) - Process.wait + begin + w.close + errmsg = io.read + assert_equal("mo\n", r.read) + assert_equal("", errmsg) + ensure + io.close + end } } 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| @@ -806,9 +1096,9 @@ class TestProcess < Test::Unit::TestCase } } rescue NotImplementedError - skip "IO#close_on_exec= is not supported" + omit "IO#close_on_exec= is not supported" end - end + end unless windows? # passing non-stdio fds is not supported on Windows def test_execopts_redirect_tempfile bug6269 = '[ruby-core:44181]' @@ -861,7 +1151,7 @@ class TestProcess < Test::Unit::TestCase def test_exec_noshell with_tmpchdir {|d| - write_file("s", <<-"End") + File.write("s", <<-"End") str = "echo non existing command name which contains spaces" STDERR.reopen(STDOUT) begin @@ -877,7 +1167,7 @@ class TestProcess < Test::Unit::TestCase def test_system_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') File.open("result", "w") {|t| t << "haha pid=#{$$} ppid=#{Process.ppid}" } exit 5 End @@ -893,7 +1183,7 @@ class TestProcess < Test::Unit::TestCase def test_spawn_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') File.open("result", "w") {|t| t << "hihi pid=#{$$} ppid=#{Process.ppid}" } exit 6 End @@ -910,7 +1200,7 @@ class TestProcess < Test::Unit::TestCase def test_popen_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') print "fufu pid=#{$$} ppid=#{Process.ppid}" exit 7 End @@ -929,7 +1219,7 @@ class TestProcess < Test::Unit::TestCase def test_popen_wordsplit_beginning_and_trailing_spaces with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') print "fufumm pid=#{$$} ppid=#{Process.ppid}" exit 7 End @@ -948,7 +1238,7 @@ class TestProcess < Test::Unit::TestCase def test_exec_wordsplit with_tmpchdir {|d| - write_file("script", <<-'End') + File.write("script", <<-'End') File.open("result", "w") {|t| if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM t << "hehe ppid=#{Process.ppid}" @@ -958,7 +1248,7 @@ class TestProcess < Test::Unit::TestCase } exit 6 End - write_file("s", <<-"End") + File.write("s", <<-"End") ruby = #{RUBY.dump} exec "\#{ruby} script" End @@ -979,11 +1269,11 @@ class TestProcess < Test::Unit::TestCase def test_system_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') File.open("result1", "w") {|t| t << "taka pid=#{$$} ppid=#{Process.ppid}" } exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') File.open("result2", "w") {|t| t << "taki pid=#{$$} ppid=#{Process.ppid}" } exit 8 End @@ -999,7 +1289,7 @@ class TestProcess < Test::Unit::TestCase if windows? Dir.mkdir(path = "path with space") - write_file(bat = path + "/bat test.bat", "@echo %1>out") + File.write(bat = path + "/bat test.bat", "@echo %1>out") system(bat, "foo 'bar'") assert_equal(%["foo 'bar'"\n], File.read("out"), '[ruby-core:22960]') system(%[#{bat.dump} "foo 'bar'"]) @@ -1010,11 +1300,11 @@ class TestProcess < Test::Unit::TestCase def test_spawn_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') File.open("result1", "w") {|t| t << "taku pid=#{$$} ppid=#{Process.ppid}" } exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') File.open("result2", "w") {|t| t << "take pid=#{$$} ppid=#{Process.ppid}" } exit 8 End @@ -1031,7 +1321,7 @@ class TestProcess < Test::Unit::TestCase if windows? Dir.mkdir(path = "path with space") - write_file(bat = path + "/bat test.bat", "@echo %1>out") + File.write(bat = path + "/bat test.bat", "@echo %1>out") pid = spawn(bat, "foo 'bar'") Process.wait pid status = $? @@ -1050,11 +1340,11 @@ class TestProcess < Test::Unit::TestCase def test_popen_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') puts "tako pid=#{$$} ppid=#{Process.ppid}" exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') puts "tika pid=#{$$} ppid=#{Process.ppid}" exit 8 End @@ -1069,7 +1359,7 @@ class TestProcess < Test::Unit::TestCase if windows? Dir.mkdir(path = "path with space") - write_file(bat = path + "/bat test.bat", "@echo %1") + File.write(bat = path + "/bat test.bat", "@echo %1") r = IO.popen([bat, "foo 'bar'"]) {|f| f.read} assert_equal(%["foo 'bar'"\n], r, '[ruby-core:22960]') r = IO.popen(%[#{bat.dump} "foo 'bar'"]) {|f| f.read} @@ -1080,15 +1370,15 @@ class TestProcess < Test::Unit::TestCase def test_exec_shell with_tmpchdir {|d| - write_file("script1", <<-'End') + File.write("script1", <<-'End') File.open("result1", "w") {|t| t << "tiki pid=#{$$} ppid=#{Process.ppid}" } exit 7 End - write_file("script2", <<-'End') + File.write("script2", <<-'End') File.open("result2", "w") {|t| t << "tiku pid=#{$$} ppid=#{Process.ppid}" } exit 8 End - write_file("s", <<-"End") + File.write("s", <<-"End") ruby = #{RUBY.dump} exec("\#{ruby} script1 || \#{ruby} script2") End @@ -1113,10 +1403,9 @@ class TestProcess < Test::Unit::TestCase Process.wait spawn([RUBY, "poiu"], "-e", "exit 4") assert_equal(4, $?.exitstatus) - assert_equal("1", IO.popen([[RUBY, "qwerty"], "-e", "print 1"]).read) - Process.wait + assert_equal("1", IO.popen([[RUBY, "qwerty"], "-e", "print 1"]) {|f| f.read }) - write_file("s", <<-"End") + File.write("s", <<-"End") exec([#{RUBY.dump}, "lkjh"], "-e", "exit 5") End pid = spawn RUBY, "s" @@ -1126,7 +1415,7 @@ class TestProcess < Test::Unit::TestCase end def with_stdin(filename) - open(filename) {|f| + File.open(filename) {|f| begin old = STDIN.dup begin @@ -1143,8 +1432,8 @@ class TestProcess < Test::Unit::TestCase def test_argv0_noarg with_tmpchdir {|d| - open("t", "w") {|f| f.print "exit true" } - open("f", "w") {|f| f.print "exit false" } + File.write("t", "exit true") + File.write("f", "exit false") with_stdin("t") { assert_equal(true, system([RUBY, "qaz"])) } with_stdin("f") { assert_equal(false, system([RUBY, "wsx"])) } @@ -1166,6 +1455,19 @@ 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_argv0_frozen + assert_predicate Process.argv0, :frozen? + assert_predicate $0, :frozen? + end + def test_status with_tmpchdir do s = run_in_child("exit 1") @@ -1174,45 +1476,70 @@ class TestProcess < Test::Unit::TestCase assert_equal(s, s) assert_equal(s, s.to_i) - assert_equal(s.to_i & 0x55555555, s & 0x55555555) - 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 def test_status_kill return unless Process.respond_to?(:kill) - return unless Signal.list.include?("QUIT") + return unless Signal.list.include?("KILL") + + # assume the system supports signal if SIGQUIT is available + expected = Signal.list.include?("QUIT") ? [false, true, false, nil] : [true, false, false, true] with_tmpchdir do - write_file("foo", "puts;STDOUT.flush;sleep 30") - pid = nil - IO.pipe do |r, w| - pid = spawn(RUBY, "foo", out: w) - w.close - Thread.new { r.read(1); Process.kill(:SIGQUIT, pid) } - Process.wait(pid) - end - t = Time.now + File.write("foo", "Process.kill(:KILL, $$); exit(42)") + system(RUBY, "foo") s = $? - assert_equal([false, true, false], - [s.exited?, s.signaled?, s.stopped?], - "[s.exited?, s.signaled?, s.stopped?]") - assert_send( - [["#<Process::Status: pid #{ s.pid } SIGQUIT (signal #{ s.termsig })>", - "#<Process::Status: pid #{ s.pid } SIGQUIT (signal #{ s.termsig }) (core dumped)>"], - :include?, - s.inspect]) - assert_equal(false, s.exited?) - assert_equal(nil, s.success?) - EnvUtil.diagnostic_reports("QUIT", RUBY, pid, t) + 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 + + def test_status_quit + return unless Process.respond_to?(:kill) + return unless Signal.list.include?("QUIT") + + with_tmpchdir do + s = assert_in_out_err([], "Signal.trap(:QUIT,'DEFAULT'); Process.kill(:SIGQUIT, $$);sleep 30", //, //, rlimit_core: 0) + assert_equal([false, true, false, nil], + [s.exited?, s.signaled?, s.stopped?, s.success?], + "[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 def test_wait_without_arg with_tmpchdir do - write_file("foo", "sleep 0.1") + File.write("foo", "sleep 0.1") pid = spawn(RUBY, "foo") assert_equal(pid, Process.wait) end @@ -1220,7 +1547,7 @@ class TestProcess < Test::Unit::TestCase def test_wait2 with_tmpchdir do - write_file("foo", "sleep 0.1") + File.write("foo", "sleep 0.1") pid = spawn(RUBY, "foo") assert_equal([pid, 0], Process.wait2) end @@ -1228,7 +1555,7 @@ class TestProcess < Test::Unit::TestCase def test_waitall with_tmpchdir do - write_file("foo", "sleep 0.1") + File.write("foo", "sleep 0.1") ps = (0...3).map { spawn(RUBY, "foo") }.sort ss = Process.waitall.sort ps.zip(ss) do |p1, (p2, s)| @@ -1238,15 +1565,56 @@ class TestProcess < Test::Unit::TestCase end end + def test_wait_exception + bug11340 = '[ruby-dev:49176] [Bug #11340]' + t0 = t1 = nil + 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 + th = Thread.start(Thread.current) do |main| + Thread.pass until main.stop? + main.raise Interrupt + end + begin + assert_raise(Interrupt) {Process.wait(pid)} + ensure + th.kill.join + end + t1 = Time.now + diff = t1 - t0 + assert_operator(diff, :<, sec, + ->{"#{bug11340}: #{diff} seconds to interrupt Process.wait"}) + f.puts + rescue Errno::EPIPE + omit "child process exited already in #{diff} seconds" + end + end + def test_abort with_tmpchdir do s = run_in_child("abort") - assert_not_equal(0, s.exitstatus) + assert_not_predicate(s, :success?) + File.write("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 @@ -1280,8 +1648,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?) + omit "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 @@ -1294,7 +1670,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 @@ -1304,10 +1680,35 @@ class TestProcess < Test::Unit::TestCase end def test_setegid + omit "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 + exc = assert_raise_kind_of(ArgumentError, SystemCallError) { + Process::UID.from_name("\u{4e0d 5b58 5728}") + } + assert_match(/\u{4e0d 5b58 5728}/, exc.message) if exc.is_a?(ArgumentError) + 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 + exc = assert_raise_kind_of(ArgumentError, SystemCallError) 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) @@ -1333,25 +1734,30 @@ class TestProcess < Test::Unit::TestCase end def test_wait_and_sigchild - if /freebsd|openbsd/ =~ RUBY_PLATFORM - # this relates #4173 - # When ruby can use 2 cores, signal and wait4 may miss the signal. - 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 - end - Process.wait pid - 10.times do - break unless signal_received.empty? - sleep 0.01 + IO.pipe do |sig_r, sig_w| + Signal.trap(:CHLD) do + signal_received << true + sig_w.write('?') + end + pid = nil + th = nil + IO.pipe do |r, w| + pid = fork { r.read(1); exit } + th = Thread.start { + Thread.current.report_on_exception = false + raise + } + w.puts + end + Process.wait pid + begin + th.join + rescue Exception + end + assert_send [sig_r, :wait_readable, 5], 'self-pipe not readable' end - assert_equal [true], signal_received, " [ruby-core:19744]" + assert_equal [true], signal_received, "[ruby-core:19744]" rescue NotImplementedError, ArgumentError ensure begin @@ -1366,7 +1772,7 @@ class TestProcess < Test::Unit::TestCase status = nil Dir.chdir("vd") { dir = "#{d}/vd" - # OpenSolaris cannot remove the current directory. + # Windows cannot remove the current directory with permission issues. system(RUBY, "--disable-gems", "-e", "Dir.chdir '..'; Dir.rmdir #{dir.dump}", err: File::NULL) system({"RUBYLIB"=>nil}, RUBY, "--disable-gems", "-e", "exit true") status = $? @@ -1378,16 +1784,16 @@ class TestProcess < Test::Unit::TestCase def test_fallback_to_sh feature = '[ruby-core:32745]' with_tmpchdir do |d| - open("tmp_script.#{$$}", "w") {|f| f.puts ": ;"; f.chmod(0755)} + File.write("tmp_script.#{$$}", ": ;\n", perm: 0o755) assert_not_nil(pid = Process.spawn("./tmp_script.#{$$}"), feature) wpid, st = Process.waitpid2(pid) assert_equal([pid, true], [wpid, st.success?], feature) - open("tmp_script.#{$$}", "w") {|f| f.puts "echo $#: $@"; f.chmod(0755)} + File.write("tmp_script.#{$$}", "echo $#: $@", perm: 0o755) result = IO.popen(["./tmp_script.#{$$}", "a b", "c"]) {|f| f.read} assert_equal("2: a b c\n", result, feature) - open("tmp_script.#{$$}", "w") {|f| f.puts "echo $hghg"; f.chmod(0755)} + File.write("tmp_script.#{$$}", "echo $hghg", perm: 0o755) result = IO.popen([{"hghg" => "mogomogo"}, "./tmp_script.#{$$}", "a b", "c"]) {|f| f.read} assert_equal("mogomogo\n", result, feature) @@ -1400,7 +1806,7 @@ class TestProcess < Test::Unit::TestCase end def test_aspawn_too_long_path - bug4315 = '[ruby-core:34833]' + bug4315 = '[ruby-core:34833] #7904 [ruby-core:52628] #11613' assert_fail_too_long_path(%w"echo |", bug4315) end @@ -1409,14 +1815,23 @@ 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} + if defined?(Process::RLIMIT_NPROC) + opts[:rlimit_nproc] = /openbsd/i =~ RUBY_PLATFORM ? 64 : 128 + end EnvUtil.suppress_warning do assert_raise(*exs, mesg) do begin loop do - Process.spawn(cmds.join(sep), [STDOUT, STDERR]=>File::NULL) + Process.spawn(cmds.join(sep), opts) min = [cmds.size, min].max - cmds *= 100 + begin + cmds *= 100 + rescue ArgumentError + raise NoMemoryError + end end rescue NoMemoryError size = cmds.size @@ -1437,7 +1852,7 @@ class TestProcess < Test::Unit::TestCase with_tmpchdir do assert_nothing_raised('[ruby-dev:12261]') do - timeout(3) do + EnvUtil.timeout(10) do pid = spawn('yes | ls') Process.waitpid pid end @@ -1496,6 +1911,28 @@ class TestProcess < Test::Unit::TestCase assert_not_equal(cpid, dpid) end + def test_daemon_detached + IO.popen("-", "r+") do |f| + if f + assert_equal(f.pid, Process.wait(f.pid)) + + dpid, ppid, dsid = 3.times.map {Integer(f.gets)} + + message = "daemon #{dpid} should be detached" + assert_not_equal($$, ppid, message) # would be 1 almost always + assert_raise(Errno::ECHILD, message) {Process.wait(dpid)} + assert_kind_of(Integer, Process.kill(0, dpid), message) + assert_equal(dpid, dsid) + + break # close f, and let the daemon resume and exit + end + Process.setsid rescue nil + Process.daemon(false, true) + puts $$, Process.ppid, Process.getsid + $stdin.gets # wait for the above assertions using signals + end + end + if File.directory?("/proc/self/task") && /netbsd[a-z]*[1-6]/ !~ RUBY_PLATFORM def test_daemon_no_threads pid, data = IO.popen("-", "r+") do |f| @@ -1504,12 +1941,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} @@ -1529,6 +1966,45 @@ class TestProcess < Test::Unit::TestCase } end + def test_popen_exit + bug11510 = '[ruby-core:70671] [Bug #11510]' + pid = nil + opt = {timeout: 10, stdout_filter: ->(s) {pid = s}} + if windows? + opt[:new_pgroup] = true + else + opt[:pgroup] = true + end + assert_ruby_status(["-", RUBY], <<-'end;', bug11510, **opt) + RUBY = ARGV[0] + th = Thread.start { + Thread.current.abort_on_exception = true + IO.popen([RUBY, "-esleep 15", err: [:child, :out]]) {|f| + STDOUT.puts f.pid + STDOUT.flush + sleep(2) + } + } + sleep(0.001) until th.stop? + end; + assert_match(/\A\d+\Z/, pid) + ensure + if pid + pid = pid.to_i + [:TERM, :KILL].each {|sig| Process.kill(sig, pid) rescue break} + end + end + + def test_popen_reopen + assert_ruby_status([], "#{<<~"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? @@ -1539,22 +2015,23 @@ class TestProcess < Test::Unit::TestCase end def test_execopts_uid + omit "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 @@ -1562,17 +2039,23 @@ 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 end def test_execopts_gid - skip "Process.groups not implemented on Windows platform" if windows? + omit "Process.groups not implemented on Windows platform" if windows? + omit "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) @@ -1584,7 +2067,15 @@ class TestProcess < Test::Unit::TestCase assert_nothing_raised(feature6975) do begin g = IO.popen([RUBY, "-e", "print Process.gid", gid: group], &:read) - assert_equal(gid, g, feature6975) + # AIX allows a non-root process to setgid to its supplementary group, + # while other UNIXes do not. (This might be AIX's violation of the POSIX standard.) + # However, Ruby does not allow a setgid'ed Ruby process to use the -e option. + # As a result, the Ruby process invoked by "IO.popen([RUBY, "-e", ..." above fails + # with a message like "no -e allowed while running setgid (SecurityError)" to stderr, + # the exis status is set to 1, and the variable "g" is set to an empty string. + # To conclude, on AIX, if the "gid" variable is a supplementary group, + # the assert_equal next can fail, so skip it. + assert_equal(gid, g, feature6975) unless $?.exitstatus == 1 && /aix/ =~ RUBY_PLATFORM && gid != Process.gid rescue Errno::EPERM, NotImplementedError end end @@ -1621,9 +2112,9 @@ class TestProcess < Test::Unit::TestCase def test_setsid return unless Process.respond_to?(:setsid) return unless Process.respond_to?(:getsid) - # OpenBSD doesn't allow Process::getsid(pid) when pid is in + # OpenBSD and AIX don't allow Process::getsid(pid) when pid is in # different session. - return if /openbsd/ =~ RUBY_PLATFORM + return if /openbsd|aix/ =~ RUBY_PLATFORM IO.popen([RUBY, "-e", <<EOS]) do|io| Marshal.dump(Process.getsid, STDOUT) @@ -1673,13 +2164,36 @@ EOS end end if windows? + def test_exec_nonascii + bug12841 = '[ruby-dev:49838] [Bug #12841]' + + [ + "\u{7d05 7389}", + "zuf\u{00E4}llige_\u{017E}lu\u{0165}ou\u{010D}k\u{00FD}_\u{10D2 10D0 10DB 10D4 10DD 10E0 10D4 10D1}_\u{0440 0430 0437 043B 043E 0433 0430}_\u{548C 65B0 52A0 5761 4EE5 53CA 4E1C}", + "c\u{1EE7}a", + ].each do |arg| + begin + arg = arg.encode(Encoding.local_charmap) + rescue + else + assert_in_out_err([], "#{<<-"begin;"}\n#{<<-"end;"}", [arg], [], bug12841) + begin; + arg = "#{arg.b}".force_encoding("#{arg.encoding.name}") + exec(ENV["COMSPEC"]||"cmd.exe", "/c", "echo", arg) + end; + end + end + end if windows? + def test_clock_gettime t1 = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) t2 = Time.now; t2 = t2.tv_sec * 1000000000 + t2.tv_nsec t3 = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) assert_operator(t1, :<=, t2) assert_operator(t2, :<=, t3) - assert_raise(Errno::EINVAL) { Process.clock_gettime(:foo) } + assert_raise_with_message(Errno::EINVAL, /:foo/) do + Process.clock_gettime(:foo) + end end def test_clock_gettime_unit @@ -1719,7 +2233,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 @@ -1780,7 +2298,9 @@ EOS rescue Errno::EINVAL else assert_kind_of(Integer, r) - assert_raise(Errno::EINVAL) { Process.clock_getres(:foo) } + assert_raise_with_message(Errno::EINVAL, /:foo/) do + Process.clock_getres(:foo) + end end def test_clock_getres_constants @@ -1797,7 +2317,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 @@ -1863,51 +2387,478 @@ EOS end def test_deadlock_by_signal_at_forking - ruby = EnvUtil.rubybin - er, ew = IO.pipe - unless runner = IO.popen("-") - er.close + assert_ruby_status(%W(- #{RUBY}), <<-INPUT, timeout: 100) + ruby = ARGV.shift + GC.start # reduce garbage + GC.disable # avoid triggering CoW after forks + trap(:QUIT) {} + parent = $$ + 100.times do |i| + pid = fork {Process.kill(:QUIT, parent)} + IO.popen([ruby, -'--disable=gems'], -'r+'){} + Process.wait(pid) + end + INPUT + end if defined?(fork) + + def test_process_detach + pid = fork {} + th = Process.detach(pid) + assert_equal pid, th.pid + status = th.value + assert_predicate status, :success? + end if defined?(fork) + + def test_kill_at_spawn_failure + bug11166 = '[ruby-core:69304] [Bug #11166]' + th = nil + x = with_tmpchdir {|d| + prog = "#{d}/notexist" + q = Thread::Queue.new + th = Thread.start {system(prog);q.push(nil);sleep} + q.pop + th.kill + th.join(0.1) + } + assert_equal(th, x, bug11166) + end if defined?(fork) + + def test_exec_fd_3_redirect + # ensure we can redirect anything to fd=3 in a child process. + # fd=3 is a commonly reserved FD for the timer thread pipe in the + # parent, but fd=3 is the first FD used by the sd_listen_fds function + # for systemd + assert_separately(['-', RUBY], <<-INPUT, timeout: 60) + ruby = ARGV.shift begin - $stderr.reopen($stdout) - trap(:QUIT) {} - 100.times do |i| - pid = fork {Process.kill(:QUIT, Process.ppid)} - IO.popen(ruby, 'r+'){} - Process.wait(pid) - $stdout.puts - $stdout.flush + a = IO.pipe + b = IO.pipe + pid = fork do + exec ruby, '-e', 'print IO.for_fd(3).read(1)', 3 => a[0], 1 => b[1] end + b[1].close + a[0].close + a[1].write('.') + assert_equal ".", b[0].read(1) ensure - ew.puts([Marshal.dump($!)].pack("m0")) if $! - ew.close + Process.wait(pid) if pid + a.each(&:close) if a + b.each(&:close) if b + end + INPUT + end if defined?(fork) + + def test_exec_close_reserved_fd + cmd = ".#{File::ALT_SEPARATOR || File::SEPARATOR}bug11353" + with_tmpchdir { + (3..6).each do |i| + ret = run_in_child(<<-INPUT) + begin + $VERBOSE = nil + Process.exec('#{cmd}', 'dummy', #{i} => :close) + rescue SystemCallError + end + INPUT + assert_equal(0, ret) + end + } + end + + def test_signals_work_after_exec_fail + r, w = IO.pipe + pid = status = nil + EnvUtil.timeout(30) do + pid = fork do + r.close + begin + trap(:USR1) { w.syswrite("USR1\n"); exit 0 } + exec "/path/to/non/existent/#$$/#{rand}.ex" + rescue SystemCallError + w.syswrite("exec failed\n") + end + sleep + exit 1 end - exit!(true) + w.close + assert_equal "exec failed\n", r.gets + Process.kill(:USR1, pid) + assert_equal "USR1\n", r.gets + assert_nil r.gets + _, status = Process.waitpid2(pid) end - ew.close + assert_predicate status, :success? + rescue Timeout::Error begin - loop do - Timeout.timeout(5) do - runner.readpartial(100) + Process.kill(:KILL, pid) + rescue Errno::ESRCH + end + raise + ensure + w.close if w + r.close if r + end if defined?(fork) + + def test_threading_works_after_exec_fail + r, w = IO.pipe + pid = status = nil + EnvUtil.timeout(90) do + pid = fork do + r.close + begin + exec "/path/to/non/existent/#$$/#{rand}.ex" + rescue SystemCallError + w.syswrite("exec failed\n") end + 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 + q << true + w.syswrite "#{th1.value} #{th2.value}\n" end - rescue EOFError => e - _, status = Process.wait2(runner.pid) - rescue Timeout::Error => e - Process.kill(:INT, runner.pid) - raise Marshal.load(er.read.unpack("m")[0]) + w.close + assert_equal "exec failed\n", r.gets + vals = r.gets.split.map!(&:to_i) + assert_operator vals[0], :>, vals[1], vals.inspect + _, status = Process.waitpid2(pid) end - assert_predicate(status, :success?) + assert_predicate status, :success? + rescue Timeout::Error + begin + Process.kill(:KILL, pid) + rescue Errno::ESRCH + end + raise ensure - er.close unless er.closed? - ew.close unless ew.closed? - if runner + w.close if w + 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) + bin = "#{EnvUtil.rubybin}" + args = Array.new(256) {"x"} + GC.stress = true + system(bin, "--disable=gems", "-w", "-e", "puts ARGV", *args) + end; + end + + def test_to_hash_on_arguments + all_assertions do |a| + %w[Array String].each do |type| + a.for(type) do + assert_separately(['-', EnvUtil.rubybin], <<~"END;") + class #{type} + def to_hash + raise "[Bug-12355]: #{type}#to_hash is called" + end + end + ex = ARGV[0] + assert_equal(true, system([ex, ex], "-e", "")) + END; + end + end + end + end + + def test_forked_child_handles_signal + omit "fork not supported" unless Process.respond_to?(:fork) + assert_normal_exit(<<-"end;", '[ruby-core:82883] [Bug #13916]') + require 'timeout' + pid = fork { sleep } + Process.kill(:TERM, pid) + 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 - Process.kill(:TERM, runner.pid) - sleep 1 - Process.kill(:KILL, runner.pid) - rescue Errno::ESRCH + r.close + w << "ok: #$$" + w.close + ensure + exit! end - runner.close + else + w.close + assert_equal("ok: #{pid}", r.read) + r.close + Process.waitpid(pid) + end + end if Process.respond_to?(:_fork) + + def test__fork_pid_cache + _parent_pid = Process.pid + r, w = IO.pipe + pid = Process._fork + if pid == 0 + begin + r.close + w << "ok: #{Process.pid}" + 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_warmup_promote_all_objects_to_oldgen + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + require 'objspace' + begin; + obj = Object.new + + assert_not_include(ObjectSpace.dump(obj), '"old":true') + Process.warmup + assert_include(ObjectSpace.dump(obj), '"old":true') + end; + end + + def test_warmup_run_major_gc_and_compact + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + # Run a GC to ensure that we are not in the middle of a GC run + GC.start + + major_gc_count = GC.stat(:major_gc_count) + compact_count = GC.stat(:compact_count) + Process.warmup + assert_equal major_gc_count + 1, GC.stat(:major_gc_count) + assert_equal compact_count + 1, GC.stat(:compact_count) + end; + end + + def test_warmup_precompute_string_coderange + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + require 'objspace' + begin; + obj = "a" * 12 + obj.force_encoding(Encoding::UTF_16LE) + obj.force_encoding(Encoding::BINARY) + assert_include(ObjectSpace.dump(obj), '"coderange":"unknown"') + Process.warmup + assert_include(ObjectSpace.dump(obj), '"coderange":"7bit"') + end; + end + + def test_warmup_frees_pages + assert_separately([{"RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO" => "1.0"}, "-W0"], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + GC.start + + TIMES = 100_000 + ary = Array.new(TIMES) + TIMES.times do |i| + ary[i] = Object.new + end + ary.clear + ary = nil + + # Disable GC so we can make sure GC only runs in Process.warmup + GC.disable + + total_slots_before = GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots) + + Process.warmup + + # TODO: flaky + # assert_equal(total_slots_before, GC.stat(:heap_available_slots) + GC.stat(:heap_allocatable_slots)) + + assert_equal(0, GC.stat(:heap_empty_pages)) + assert_operator(GC.stat(:total_freed_pages), :>, 0) + end; + end + + 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) + + def test_handle_interrupt_with_fork + Thread.handle_interrupt(RuntimeError => :never) do + Thread.current.raise(RuntimeError, "Queued error") + + assert_predicate Thread, :pending_interrupt? + + pid = Process.fork do + if Thread.pending_interrupt? + exit 1 + end + end + + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + + assert_predicate Thread, :pending_interrupt? end + rescue RuntimeError + # Ignore. end if defined?(fork) end |
