summaryrefslogtreecommitdiff
path: root/test/ruby/test_process.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/ruby/test_process.rb')
-rw-r--r--test/ruby/test_process.rb492
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