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.rb1742
1 files changed, 1480 insertions, 262 deletions
diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb
index 8b79d35738..b3a88b664c 100644
--- a/test/ruby/test_process.rb
+++ b/test/ruby/test_process.rb
@@ -1,8 +1,8 @@
+# coding: utf-8
+# frozen_string_literal: false
require 'test/unit'
require 'tempfile'
-require 'pathname'
require 'timeout'
-require_relative 'envutil'
require 'rbconfig'
class TestProcess < Test::Unit::TestCase
@@ -17,18 +17,15 @@ class TestProcess < Test::Unit::TestCase
end
def windows?
- return /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
+ self.class.windows?
end
-
- def write_file(filename, content)
- File.open(filename, "w") {|f|
- f << content
- }
+ def self.windows?
+ return /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
end
def with_tmpchdir
Dir.mktmpdir {|d|
- d = Pathname.new(d).realpath.to_s
+ d = File.realpath(d)
Dir.chdir(d) {
yield d
}
@@ -36,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
@@ -61,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)
@@ -114,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)
@@ -129,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)
@@ -139,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
@@ -158,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"])
@@ -170,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])
@@ -191,54 +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])
+ 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)
@@ -301,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)
@@ -334,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
@@ -346,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'),
@@ -365,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')
@@ -393,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
@@ -409,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)
}
@@ -450,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"))
@@ -469,7 +574,7 @@ class TestProcess < Test::Unit::TestCase
# problem occur with valgrind
#Process.wait Process.spawn(*ECHO["a"], STDOUT=>:close, STDERR=>["out", File::WRONLY|File::CREAT|File::TRUNC, 0644])
#p File.read("out")
- #assert(!File.read("out").empty?) # error message such as "-e:1:in `flush': Bad file descriptor (Errno::EBADF)"
+ #assert_not_empty(File.read("out")) # error message such as "-e:1:in `flush': Bad file descriptor (Errno::EBADF)"
Process.wait Process.spawn(*ECHO["c"], STDERR=>STDOUT, STDOUT=>["out", File::WRONLY|File::CREAT|File::TRUNC, 0644])
assert_equal("c", File.read("out").chomp)
File.open("out", "w") {|f|
@@ -522,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
- 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_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
+
+ 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
+ }
- 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)
- }
+ 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
+ }
- 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")
@@ -612,6 +840,17 @@ class TestProcess < Test::Unit::TestCase
}
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'",
@@ -622,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],
@@ -644,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"))
@@ -656,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| }
}
@@ -665,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)
@@ -681,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
@@ -708,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)
@@ -721,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)"`
@@ -736,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)
@@ -759,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},
@@ -773,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|
@@ -812,20 +1096,19 @@ 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]'
- Tempfile.open("execopts") do |tmp|
+ Tempfile.create("execopts") do |tmp|
pid = assert_nothing_raised(ArgumentError, bug6269) do
break spawn(RUBY, "-e", "print $$", out: tmp)
end
Process.wait(pid)
tmp.rewind
assert_equal(pid.to_s, tmp.read)
- tmp.close(true)
end
end
@@ -868,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
@@ -884,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
@@ -892,7 +1175,7 @@ class TestProcess < Test::Unit::TestCase
ret = system(str)
status = $?
assert_equal(false, ret)
- assert(status.exited?)
+ assert_predicate(status, :exited?)
assert_equal(5, status.exitstatus)
assert_equal("haha pid=#{status.pid} ppid=#{$$}", File.read("result"))
}
@@ -900,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
@@ -909,7 +1192,7 @@ class TestProcess < Test::Unit::TestCase
Process.wait pid
status = $?
assert_equal(pid, status.pid)
- assert(status.exited?)
+ assert_predicate(status, :exited?)
assert_equal(6, status.exitstatus)
assert_equal("hihi pid=#{status.pid} ppid=#{$$}", File.read("result"))
}
@@ -917,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
@@ -928,7 +1211,7 @@ class TestProcess < Test::Unit::TestCase
io.close
status = $?
assert_equal(pid, status.pid)
- assert(status.exited?)
+ assert_predicate(status, :exited?)
assert_equal(7, status.exitstatus)
assert_equal("fufu pid=#{status.pid} ppid=#{$$}", result)
}
@@ -936,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
@@ -947,7 +1230,7 @@ class TestProcess < Test::Unit::TestCase
io.close
status = $?
assert_equal(pid, status.pid)
- assert(status.exited?)
+ assert_predicate(status, :exited?)
assert_equal(7, status.exitstatus)
assert_equal("fufumm pid=#{status.pid} ppid=#{$$}", result)
}
@@ -955,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}"
@@ -965,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
@@ -973,7 +1256,7 @@ class TestProcess < Test::Unit::TestCase
Process.wait pid
status = $?
assert_equal(pid, status.pid)
- assert(status.exited?)
+ assert_predicate(status, :exited?)
assert_equal(6, status.exitstatus)
if windows?
expected = "hehe ppid=#{status.pid}"
@@ -986,18 +1269,18 @@ 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
ret = system("#{RUBY} script1 || #{RUBY} script2")
status = $?
assert_equal(false, ret)
- assert(status.exited?)
+ assert_predicate(status, :exited?)
result1 = File.read("result1")
result2 = File.read("result2")
assert_match(/\Ataka pid=\d+ ppid=\d+\z/, result1)
@@ -1006,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'"])
@@ -1017,19 +1300,19 @@ 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
pid = spawn("#{RUBY} script1 || #{RUBY} script2")
Process.wait pid
status = $?
- assert(status.exited?)
- assert(!status.success?)
+ assert_predicate(status, :exited?)
+ assert_not_predicate(status, :success?)
result1 = File.read("result1")
result2 = File.read("result2")
assert_match(/\Ataku pid=\d+ ppid=\d+\z/, result1)
@@ -1038,18 +1321,18 @@ 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 = $?
- assert(status.exited?)
- assert(status.success?)
+ assert_predicate(status, :exited?)
+ assert_predicate(status, :success?)
assert_equal(%["foo 'bar'"\n], File.read("out"), '[ruby-core:22960]')
pid = spawn(%[#{bat.dump} "foo 'bar'"])
Process.wait pid
status = $?
- assert(status.exited?)
- assert(status.success?)
+ assert_predicate(status, :exited?)
+ assert_predicate(status, :success?)
assert_equal(%["foo 'bar'"\n], File.read("out"), '[ruby-core:22960]')
end
}
@@ -1057,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,14 +1352,14 @@ class TestProcess < Test::Unit::TestCase
result = io.read
io.close
status = $?
- assert(status.exited?)
- assert(!status.success?)
+ assert_predicate(status, :exited?)
+ assert_not_predicate(status, :success?)
assert_match(/\Atako pid=\d+ ppid=\d+\ntika pid=\d+ ppid=\d+\n\z/, result)
assert_not_equal(result[/\d+/].to_i, status.pid)
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}
@@ -1087,23 +1370,23 @@ 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
pid = spawn RUBY, "s"
Process.wait pid
status = $?
- assert(status.exited?)
- assert(!status.success?)
+ assert_predicate(status, :exited?)
+ assert_not_predicate(status, :success?)
result1 = File.read("result1")
result2 = File.read("result2")
assert_match(/\Atiki pid=\d+ ppid=\d+\z/, result1)
@@ -1120,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"
@@ -1133,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
@@ -1150,29 +1432,42 @@ 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"])) }
with_stdin("t") { Process.wait spawn([RUBY, "edc"]) }
- assert($?.success?)
+ assert_predicate($?, :success?)
with_stdin("f") { Process.wait spawn([RUBY, "rfv"]) }
- assert(!$?.success?)
+ assert_not_predicate($?, :success?)
with_stdin("t") { IO.popen([[RUBY, "tgb"]]) {|io| assert_equal("", io.read) } }
- assert($?.success?)
+ assert_predicate($?, :success?)
with_stdin("f") { IO.popen([[RUBY, "yhn"]]) {|io| assert_equal("", io.read) } }
- assert(!$?.success?)
+ assert_not_predicate($?, :success?)
status = run_in_child "STDIN.reopen('t'); exec([#{RUBY.dump}, 'ujm'])"
- assert(status.success?)
+ assert_predicate(status, :success?)
status = run_in_child "STDIN.reopen('f'); exec([#{RUBY.dump}, 'ik,'])"
- assert(!status.success?)
+ assert_not_predicate(status, :success?)
}
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")
@@ -1181,39 +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", "sleep 30")
- pid = spawn(RUBY, "foo")
- Thread.new { sleep 1; Process.kill(:SIGQUIT, pid) }
- Process.wait(pid)
+ 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?)
+ 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
@@ -1221,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
@@ -1229,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)|
@@ -1239,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
@@ -1281,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
@@ -1295,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
@@ -1305,28 +1680,53 @@ 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(true == r || false == r)
+ assert_include([true, false], r)
end
def test_gid_re_exchangeable_p
r = Process::GID.re_exchangeable?
- assert(true == r || false == r)
+ assert_include([true, false], r)
end
def test_uid_sid_available?
r = Process::UID.sid_available?
- assert(true == r || false == r)
+ assert_include([true, false], r)
end
def test_gid_sid_available?
r = Process::GID.sid_available?
- assert(true == r || false == r)
+ assert_include([true, false], r)
end
def test_pst_inspect
@@ -1334,18 +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 = fork { sleep 0.1; exit }
- Thread.start { raise }
- Process.wait pid
- sleep 0.1
- assert_equal [true], signal_received, " [ruby-core:19744]"
+ 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]"
rescue NotImplementedError, ArgumentError
ensure
begin
@@ -1360,28 +1772,28 @@ class TestProcess < Test::Unit::TestCase
status = nil
Dir.chdir("vd") {
dir = "#{d}/vd"
- # OpenSolaris cannot remove the current directory.
- system(RUBY, "--disable-gems", "-e", "Dir.chdir '..'; Dir.rmdir #{dir.dump}")
+ # 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 = $?
}
- assert(status.success?, "[ruby-dev:38105]")
+ assert_predicate(status, :success?, "[ruby-dev:38105]")
}
- end unless /mswin|bccwin|mingw/ =~ RUBY_PLATFORM
+ end
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)
@@ -1394,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
@@ -1403,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]=>:close)
+ 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
@@ -1431,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
@@ -1490,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|
@@ -1498,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}
@@ -1519,10 +1962,49 @@ class TestProcess < Test::Unit::TestCase
def test_popen_cloexec
return unless defined? Fcntl::FD_CLOEXEC
IO.popen([RUBY, "-e", ""]) {|io|
- assert(io.close_on_exec?)
+ assert_predicate(io, :close_on_exec?)
}
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?
@@ -1533,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
@@ -1556,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)
@@ -1578,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
@@ -1615,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)
@@ -1641,6 +2138,727 @@ EOS
end
end
+ def test_spawn_nonascii
+ bug1771 = '[ruby-core:24309] [Bug #1771]'
+
+ with_tmpchdir do
+ [
+ "\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 |name|
+ msg = "#{bug1771} #{name}"
+ exename = "./#{name}.exe"
+ FileUtils.cp(ENV["COMSPEC"], exename)
+ assert_equal(true, system("#{exename} /c exit"), msg)
+ system("#{exename} /c exit 12")
+ assert_equal(12, $?.exitstatus, msg)
+ _, status = Process.wait2(Process.spawn("#{exename} /c exit 42"))
+ assert_equal(42, status.exitstatus, msg)
+ assert_equal("ok\n", `#{exename} /c echo ok`, msg)
+ assert_equal("ok\n", IO.popen("#{exename} /c echo ok", &:read), msg)
+ assert_equal("ok\n", IO.popen(%W"#{exename} /c echo ok", &:read), msg)
+ File.binwrite("#{name}.txt", "ok")
+ assert_equal("ok", `type #{name}.txt`)
+ end
+ 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_with_message(Errno::EINVAL, /:foo/) do
+ Process.clock_gettime(:foo)
+ end
+ end
+
+ def test_clock_gettime_unit
+ t0 = Time.now.to_f
+ [
+ [:nanosecond, 1_000_000_000],
+ [:microsecond, 1_000_000],
+ [:millisecond, 1_000],
+ [:second, 1],
+ [:float_microsecond, 1_000_000.0],
+ [:float_millisecond, 1_000.0],
+ [:float_second, 1.0],
+ [nil, 1.0],
+ [:foo],
+ ].each do |unit, num|
+ unless num
+ assert_raise(ArgumentError){ Process.clock_gettime(Process::CLOCK_REALTIME, unit) }
+ next
+ end
+ t1 = Process.clock_gettime(Process::CLOCK_REALTIME, unit)
+ assert_kind_of num.integer? ? Integer : num.class, t1, [unit, num].inspect
+ assert_in_delta t0, t1/num, 1, [unit, num].inspect
+ end
+ end
+
+ def test_clock_gettime_constants
+ Process.constants.grep(/\ACLOCK_/).each {|n|
+ c = Process.const_get(n)
+ begin
+ t = Process.clock_gettime(c)
+ rescue Errno::EINVAL
+ next
+ end
+ assert_kind_of(Float, t, "Process.clock_gettime(Process::#{n})")
+ }
+ end
+
+ def test_clock_gettime_GETTIMEOFDAY_BASED_CLOCK_REALTIME
+ n = :GETTIMEOFDAY_BASED_CLOCK_REALTIME
+ begin
+ t = Process.clock_gettime(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+
+ def test_clock_gettime_TIME_BASED_CLOCK_REALTIME
+ n = :TIME_BASED_CLOCK_REALTIME
+ t = Process.clock_gettime(n)
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+
+ def test_clock_gettime_TIMES_BASED_CLOCK_MONOTONIC
+ n = :TIMES_BASED_CLOCK_MONOTONIC
+ begin
+ t = Process.clock_gettime(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+ def test_clock_gettime_GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID
+ n = :GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID
+ begin
+ t = Process.clock_gettime(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+
+ def test_clock_gettime_TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID
+ n = :TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID
+ begin
+ t = Process.clock_gettime(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+
+ def test_clock_gettime_CLOCK_BASED_CLOCK_PROCESS_CPUTIME_ID
+ n = :CLOCK_BASED_CLOCK_PROCESS_CPUTIME_ID
+ t = Process.clock_gettime(n)
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+
+ def test_clock_gettime_MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC
+ n = :MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC
+ begin
+ t = Process.clock_gettime(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_gettime(:#{n})")
+ end
+
+ def test_clock_getres
+ r = Process.clock_getres(Process::CLOCK_REALTIME, :nanosecond)
+ rescue Errno::EINVAL
+ else
+ assert_kind_of(Integer, r)
+ assert_raise_with_message(Errno::EINVAL, /:foo/) do
+ Process.clock_getres(:foo)
+ end
+ end
+
+ def test_clock_getres_constants
+ Process.constants.grep(/\ACLOCK_/).each {|n|
+ c = Process.const_get(n)
+ begin
+ t = Process.clock_getres(c)
+ rescue Errno::EINVAL
+ next
+ end
+ assert_kind_of(Float, t, "Process.clock_getres(Process::#{n})")
+ }
+ end
+
+ def test_clock_getres_GETTIMEOFDAY_BASED_CLOCK_REALTIME
+ n = :GETTIMEOFDAY_BASED_CLOCK_REALTIME
+ 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
+
+ def test_clock_getres_TIME_BASED_CLOCK_REALTIME
+ n = :TIME_BASED_CLOCK_REALTIME
+ t = Process.clock_getres(n)
+ assert_kind_of(Float, t, "Process.clock_getres(:#{n})")
+ assert_equal(1000000000, Process.clock_getres(n, :nanosecond))
+ end
+
+ def test_clock_getres_TIMES_BASED_CLOCK_MONOTONIC
+ n = :TIMES_BASED_CLOCK_MONOTONIC
+ begin
+ t = Process.clock_getres(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_getres(:#{n})")
+ f = Process.clock_getres(n, :hertz)
+ assert_equal(0, f - f.floor)
+ end
+
+ def test_clock_getres_GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID
+ n = :GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID
+ 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
+
+ def test_clock_getres_TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID
+ n = :TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID
+ begin
+ t = Process.clock_getres(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_getres(:#{n})")
+ f = Process.clock_getres(n, :hertz)
+ assert_equal(0, f - f.floor)
+ end
+
+ def test_clock_getres_CLOCK_BASED_CLOCK_PROCESS_CPUTIME_ID
+ n = :CLOCK_BASED_CLOCK_PROCESS_CPUTIME_ID
+ t = Process.clock_getres(n)
+ assert_kind_of(Float, t, "Process.clock_getres(:#{n})")
+ f = Process.clock_getres(n, :hertz)
+ assert_equal(0, f - f.floor)
+ end
+
+ def test_clock_getres_MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC
+ n = :MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC
+ begin
+ t = Process.clock_getres(n)
+ rescue Errno::EINVAL
+ return
+ end
+ assert_kind_of(Float, t, "Process.clock_getres(:#{n})")
+ end
+
+ def test_deadlock_by_signal_at_forking
+ 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
+ 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
+ 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
+ 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
+ assert_predicate status, :success?
+ rescue Timeout::Error
+ begin
+ 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
+ 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?
+ rescue Timeout::Error
+ begin
+ Process.kill(:KILL, pid)
+ rescue Errno::ESRCH
+ end
+ raise
+ ensure
+ 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
+ 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_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