diff options
Diffstat (limited to 'test/ruby/test_io.rb')
| -rw-r--r-- | test/ruby/test_io.rb | 2557 |
1 files changed, 2155 insertions, 402 deletions
diff --git a/test/ruby/test_io.rb b/test/ruby/test_io.rb index 58cae7e291..1adf47ac51 100644 --- a/test/ruby/test_io.rb +++ b/test/ruby/test_io.rb @@ -1,29 +1,33 @@ # coding: US-ASCII +# frozen_string_literal: false require 'test/unit' require 'tmpdir' require "fcntl" require 'io/nonblock' +require 'pathname' require 'socket' require 'stringio' require 'timeout' require 'tempfile' require 'weakref' -require_relative 'envutil' class TestIO < Test::Unit::TestCase - def have_close_on_exec? - begin + module Feature + def have_close_on_exec? $stdin.close_on_exec? true rescue NotImplementedError false end - end - def have_nonblock? - IO.method_defined?("nonblock=") + def have_nonblock? + IO.method_defined?("nonblock=") + end end + include Feature + extend Feature + def pipe(wp, rp) re, we = nil, nil r, w = IO.pipe @@ -45,8 +49,8 @@ class TestIO < Test::Unit::TestCase end flunk("timeout") unless wt.join(10) && rt.join(10) ensure - w.close unless !w || w.closed? - r.close unless !r || r.closed? + w&.close + r&.close (wt.kill; wt.join) if wt (rt.kill; rt.join) if rt raise we if we @@ -58,8 +62,8 @@ class TestIO < Test::Unit::TestCase begin yield r, w ensure - r.close unless r.closed? - w.close unless w.closed? + r.close + w.close end end @@ -80,12 +84,17 @@ class TestIO < Test::Unit::TestCase } end - def trapping_usr1 - @usr1_rcvd = 0 - trap(:USR1) { @usr1_rcvd += 1 } - yield + def trapping_usr2 + @usr2_rcvd = 0 + r, w = IO.pipe + trap(:USR2) do + w.write([@usr2_rcvd += 1].pack('L')) + end + yield r ensure - trap(:USR1, "DEFAULT") + trap(:USR2, "DEFAULT") + w&.close + r&.close end def test_pipe @@ -104,6 +113,65 @@ class TestIO < Test::Unit::TestCase ].each{|thr| thr.join} end + def test_binmode_pipe + EnvUtil.with_default_internal(Encoding::UTF_8) do + EnvUtil.with_default_external(Encoding::UTF_8) do + begin + reader0, writer0 = IO.pipe + reader0.binmode + writer0.binmode + + reader1, writer1 = IO.pipe + + reader2, writer2 = IO.pipe(binmode: true) + assert_predicate writer0, :binmode? + assert_predicate writer2, :binmode? + assert_equal writer0.binmode?, writer2.binmode? + assert_equal writer0.external_encoding, writer2.external_encoding + assert_equal writer0.internal_encoding, writer2.internal_encoding + assert_predicate reader0, :binmode? + assert_predicate reader2, :binmode? + assert_equal reader0.binmode?, reader2.binmode? + assert_equal reader0.external_encoding, reader2.external_encoding + assert_equal reader0.internal_encoding, reader2.internal_encoding + + reader3, writer3 = IO.pipe("UTF-8:UTF-8", binmode: true) + assert_predicate writer3, :binmode? + assert_equal writer1.external_encoding, writer3.external_encoding + assert_equal writer1.internal_encoding, writer3.internal_encoding + assert_predicate reader3, :binmode? + assert_equal reader1.external_encoding, reader3.external_encoding + assert_equal reader1.internal_encoding, reader3.internal_encoding + + reader4, writer4 = IO.pipe("UTF-8:UTF-8", binmode: true) + assert_predicate writer4, :binmode? + assert_equal writer1.external_encoding, writer4.external_encoding + assert_equal writer1.internal_encoding, writer4.internal_encoding + assert_predicate reader4, :binmode? + assert_equal reader1.external_encoding, reader4.external_encoding + assert_equal reader1.internal_encoding, reader4.internal_encoding + + reader5, writer5 = IO.pipe("UTF-8", "UTF-8", binmode: true) + assert_predicate writer5, :binmode? + assert_equal writer1.external_encoding, writer5.external_encoding + assert_equal writer1.internal_encoding, writer5.internal_encoding + assert_predicate reader5, :binmode? + assert_equal reader1.external_encoding, reader5.external_encoding + assert_equal reader1.internal_encoding, reader5.internal_encoding + ensure + [ + reader0, writer0, + reader1, writer1, + reader2, writer2, + reader3, writer3, + reader4, writer4, + reader5, writer5, + ].compact.map(&:close) + end + end + end + end + def test_pipe_block x = nil ret = IO.pipe {|r, w| @@ -119,13 +187,13 @@ class TestIO < Test::Unit::TestCase assert_equal("abc", r.read) end ].each{|thr| thr.join} - assert(!r.closed?) - assert(w.closed?) + assert_not_predicate(r, :closed?) + assert_predicate(w, :closed?) :foooo } assert_equal(:foooo, ret) - assert(x[0].closed?) - assert(x[1].closed?) + assert_predicate(x[0], :closed?) + assert_predicate(x[1], :closed?) end def test_pipe_block_close @@ -136,13 +204,25 @@ class TestIO < Test::Unit::TestCase r.close if (i&1) == 0 w.close if (i&2) == 0 } - assert(x[0].closed?) - assert(x[1].closed?) + assert_predicate(x[0], :closed?) + assert_predicate(x[1], :closed?) } end def test_gets_rs - # default_rs + rs = ":" + pipe(proc do |w| + w.print "aaa:bbb" + w.close + end, proc do |r| + assert_equal "aaa:", r.gets(rs) + assert_equal "bbb", r.gets(rs) + assert_nil r.gets(rs) + r.close + end) + end + + def test_gets_default_rs pipe(proc do |w| w.print "aaa\nbbb\n" w.close @@ -152,8 +232,9 @@ class TestIO < Test::Unit::TestCase assert_nil r.gets r.close end) + end - # nil + def test_gets_rs_nil pipe(proc do |w| w.print "a\n\nb\n\n" w.close @@ -162,8 +243,9 @@ class TestIO < Test::Unit::TestCase assert_nil r.gets("") r.close end) + end - # "\377" + def test_gets_rs_377 pipe(proc do |w| w.print "\377xyz" w.close @@ -172,8 +254,9 @@ class TestIO < Test::Unit::TestCase assert_equal("\377", r.gets("\377"), "[ruby-dev:24460]") r.close end) + end - # "" + def test_gets_paragraph pipe(proc do |w| w.print "a\n\nb\n\n" w.close @@ -185,6 +268,68 @@ class TestIO < Test::Unit::TestCase end) end + def test_gets_chomp_rs + rs = ":" + pipe(proc do |w| + w.print "aaa:bbb" + w.close + end, proc do |r| + assert_equal "aaa", r.gets(rs, chomp: true) + assert_equal "bbb", r.gets(rs, chomp: true) + assert_nil r.gets(rs, chomp: true) + r.close + end) + end + + def test_gets_chomp_default_rs + pipe(proc do |w| + w.print "aaa\r\nbbb\nccc" + w.close + end, proc do |r| + assert_equal "aaa", r.gets(chomp: true) + assert_equal "bbb", r.gets(chomp: true) + assert_equal "ccc", r.gets(chomp: true) + assert_nil r.gets + r.close + end) + + (0..3).each do |i| + pipe(proc do |w| + w.write("a" * ((4096 << i) - 4), "\r\n" "a\r\n") + w.close + end, + proc do |r| + r.gets + assert_equal "a", r.gets(chomp: true) + assert_nil r.gets + r.close + end) + end + end + + def test_gets_chomp_rs_nil + pipe(proc do |w| + w.print "a\n\nb\n\n" + w.close + end, proc do |r| + assert_equal("a\n\nb\n\n", r.gets(nil, chomp: true), "[Bug #18770]") + assert_nil r.gets("") + r.close + end) + end + + def test_gets_chomp_paragraph + pipe(proc do |w| + w.print "a\n\nb\n\n" + w.close + end, proc do |r| + assert_equal "a", r.gets("", chomp: true) + assert_equal "b", r.gets("", chomp: true) + assert_nil r.gets("", chomp: true) + r.close + end) + end + def test_gets_limit_extra_arg pipe(proc do |w| w << "0123456789\n0123456789" @@ -205,6 +350,19 @@ class TestIO < Test::Unit::TestCase end) end + def test_ungetc_with_seek + make_tempfile {|t| + t.open + t.write('0123456789') + t.rewind + + t.ungetc('a') + t.seek(2, :SET) + + assert_equal('2', t.getc) + } + end + def test_ungetbyte make_tempfile {|t| t.open @@ -228,6 +386,19 @@ class TestIO < Test::Unit::TestCase } end + def test_ungetbyte_with_seek + make_tempfile {|t| + t.open + t.write('0123456789') + t.rewind + + t.ungetbyte('a'.ord) + t.seek(2, :SET) + + assert_equal('2'.ord, t.getbyte) + } + end + def test_each_byte pipe(proc do |w| w << "abc def" @@ -249,6 +420,24 @@ class TestIO < Test::Unit::TestCase } end + def test_each_byte_closed + pipe(proc do |w| + w << "abc def" + w.close + end, proc do |r| + assert_raise(IOError) do + r.each_byte {|byte| r.close if byte == 32 } + end + end) + make_tempfile {|t| + File.open(t, 'rt') {|f| + assert_raise(IOError) do + f.each_byte {|c| f.close if c == 10} + end + } + } + end + def test_each_codepoint make_tempfile {|t| bug2959 = '[ruby-core:28650]' @@ -260,16 +449,39 @@ class TestIO < Test::Unit::TestCase } end - def test_codepoints + def test_each_codepoint_closed + pipe(proc do |w| + w.print("abc def") + w.close + end, proc do |r| + assert_raise(IOError) do + r.each_codepoint {|c| r.close if c == 32} + end + end) make_tempfile {|t| - bug2959 = '[ruby-core:28650]' - a = "" File.open(t, 'rt') {|f| - assert_warn(/deprecated/) { - f.codepoints {|c| a << c} - } + assert_raise(IOError) do + f.each_codepoint {|c| f.close if c == 10} + end } - assert_equal("foo\nbar\nbaz\n", a, bug2959) + } + end + + def test_each_codepoint_with_ungetc + bug21562 = '[ruby-core:123176] [Bug #21562]' + with_read_pipe("") {|p| + p.binmode + p.ungetc("aa") + a = "" + p.each_codepoint { |c| a << c } + assert_equal("aa", a, bug21562) + } + with_read_pipe("") {|p| + p.set_encoding("ascii-8bit", universal_newline: true) + p.ungetc("aa") + a = "" + p.each_codepoint { |c| a << c } + assert_equal("aa", a, bug21562) } end @@ -278,67 +490,126 @@ class TestIO < Test::Unit::TestCase path = t.path t.close! assert_raise(Errno::ENOENT, "[ruby-dev:33072]") do - File.read(path, nil, nil, {}) + File.read(path, nil, nil, **{}) end end - def test_copy_stream + def with_srccontent(content = "baz") + src = "src" mkcdtmpdir { + File.open(src, "w") {|f| f << content } + yield src, content + } + end - content = "foobar" - File.open("src", "w") {|f| f << content } - ret = IO.copy_stream("src", "dst") + def test_copy_stream_small + with_srccontent("foobar") {|src, content| + ret = IO.copy_stream(src, "dst") assert_equal(content.bytesize, ret) assert_equal(content, File.read("dst")) + } + end + + def test_copy_stream_append + with_srccontent("foobar") {|src, content| + File.open('dst', 'ab') do |dst| + ret = IO.copy_stream(src, dst) + assert_equal(content.bytesize, ret) + assert_equal(content, File.read("dst")) + end + } + end + + def test_copy_stream_append_to_nonempty + with_srccontent("foobar") {|src, content| + preface = 'preface' + File.write('dst', preface) + File.open('dst', 'ab') do |dst| + ret = IO.copy_stream(src, dst) + assert_equal(content.bytesize, ret) + assert_equal(preface + content, File.read("dst")) + end + } + end + + def test_copy_stream_smaller + with_srccontent {|src, content| # overwrite by smaller file. - content = "baz" - File.open("src", "w") {|f| f << content } - ret = IO.copy_stream("src", "dst") + dst = "dst" + File.open(dst, "w") {|f| f << "foobar"} + + ret = IO.copy_stream(src, dst) assert_equal(content.bytesize, ret) - assert_equal(content, File.read("dst")) + assert_equal(content, File.read(dst)) - ret = IO.copy_stream("src", "dst", 2) + ret = IO.copy_stream(src, dst, 2) assert_equal(2, ret) - assert_equal(content[0,2], File.read("dst")) + assert_equal(content[0,2], File.read(dst)) - ret = IO.copy_stream("src", "dst", 0) + ret = IO.copy_stream(src, dst, 0) assert_equal(0, ret) - assert_equal("", File.read("dst")) + assert_equal("", File.read(dst)) - ret = IO.copy_stream("src", "dst", nil, 1) + ret = IO.copy_stream(src, dst, nil, 1) assert_equal(content.bytesize-1, ret) - assert_equal(content[1..-1], File.read("dst")) + assert_equal(content[1..-1], File.read(dst)) + } + end + def test_copy_stream_noent + with_srccontent {|src, content| assert_raise(Errno::ENOENT) { IO.copy_stream("nodir/foo", "dst") } assert_raise(Errno::ENOENT) { - IO.copy_stream("src", "nodir/bar") + IO.copy_stream(src, "nodir/bar") } + } + end + def test_copy_stream_pipe + with_srccontent {|src, content| pipe(proc do |w| - ret = IO.copy_stream("src", w) + ret = IO.copy_stream(src, w) assert_equal(content.bytesize, ret) w.close end, proc do |r| assert_equal(content, r.read) end) + } + end + def test_copy_stream_write_pipe + with_srccontent {|src, content| with_pipe {|r, w| w.close - assert_raise(IOError) { IO.copy_stream("src", w) } + assert_raise(IOError) { IO.copy_stream(src, w) } } + } + end + + def with_pipecontent + mkcdtmpdir { + yield "abc" + } + end - pipe_content = "abc" + def test_copy_stream_pipe_to_file + with_pipecontent {|pipe_content| + dst = "dst" with_read_pipe(pipe_content) {|r| - ret = IO.copy_stream(r, "dst") + ret = IO.copy_stream(r, dst) assert_equal(pipe_content.bytesize, ret) - assert_equal(pipe_content, File.read("dst")) + assert_equal(pipe_content, File.read(dst)) } + } + end - with_read_pipe("abc") {|r1| + def test_copy_stream_read_pipe + with_pipecontent {|pipe_content| + with_read_pipe(pipe_content) {|r1| assert_equal("a", r1.getc) pipe(proc do |w2| w2.sync = false @@ -351,7 +622,7 @@ class TestIO < Test::Unit::TestCase end) } - with_read_pipe("abc") {|r1| + with_read_pipe(pipe_content) {|r1| assert_equal("a", r1.getc) pipe(proc do |w2| w2.sync = false @@ -364,7 +635,7 @@ class TestIO < Test::Unit::TestCase end) } - with_read_pipe("abc") {|r1| + with_read_pipe(pipe_content) {|r1| assert_equal("a", r1.getc) pipe(proc do |w2| ret = IO.copy_stream(r1, w2) @@ -375,7 +646,7 @@ class TestIO < Test::Unit::TestCase end) } - with_read_pipe("abc") {|r1| + with_read_pipe(pipe_content) {|r1| assert_equal("a", r1.getc) pipe(proc do |w2| ret = IO.copy_stream(r1, w2, 1) @@ -386,7 +657,7 @@ class TestIO < Test::Unit::TestCase end) } - with_read_pipe("abc") {|r1| + with_read_pipe(pipe_content) {|r1| assert_equal("a", r1.getc) pipe(proc do |w2| ret = IO.copy_stream(r1, w2, 0) @@ -411,24 +682,43 @@ class TestIO < Test::Unit::TestCase assert_equal("bcdef", r2.read) end) end) + } + end + def test_copy_stream_file_to_pipe + with_srccontent {|src, content| pipe(proc do |w| - ret = IO.copy_stream("src", w, 1, 1) + ret = IO.copy_stream(src, w, 1, 1) assert_equal(1, ret) w.close end, proc do |r| assert_equal(content[1,1], r.read) end) + } + end + + if have_nonblock? + def test_copy_stream_no_busy_wait + omit "multiple threads already active" if Thread.list.size > 1 - if have_nonblock? + msg = 'r58534 [ruby-core:80969] [Backport #13533]' + IO.pipe do |r,w| + r.nonblock = true + assert_cpu_usage_low(msg, stop: ->{w.close}) do + IO.copy_stream(r, IO::NULL) + end + end + end + + def test_copy_stream_pipe_nonblock + mkcdtmpdir { with_read_pipe("abc") {|r1| assert_equal("a", r1.getc) with_pipe {|r2, w2| begin w2.nonblock = true rescue Errno::EBADF - skip "nonblocking IO for pipe is not implemented" - break + omit "nonblocking IO for pipe is not implemented" end s = w2.syswrite("a" * 100000) t = Thread.new { sleep 0.1; r2.read } @@ -438,25 +728,51 @@ class TestIO < Test::Unit::TestCase assert_equal("a" * s + "bc", t.value) } } - end + } + end + end + + def with_bigcontent + yield "abc" * 123456 + end + + def with_bigsrc + mkcdtmpdir { + with_bigcontent {|bigcontent| + bigsrc = "bigsrc" + File.open("bigsrc", "w") {|f| f << bigcontent } + yield bigsrc, bigcontent + } + } + end - bigcontent = "abc" * 123456 - File.open("bigsrc", "w") {|f| f << bigcontent } - ret = IO.copy_stream("bigsrc", "bigdst") + def test_copy_stream_bigcontent + with_bigsrc {|bigsrc, bigcontent| + ret = IO.copy_stream(bigsrc, "bigdst") assert_equal(bigcontent.bytesize, ret) assert_equal(bigcontent, File.read("bigdst")) + } + end - File.unlink("bigdst") - ret = IO.copy_stream("bigsrc", "bigdst", nil, 100) + def test_copy_stream_bigcontent_chop + with_bigsrc {|bigsrc, bigcontent| + ret = IO.copy_stream(bigsrc, "bigdst", nil, 100) assert_equal(bigcontent.bytesize-100, ret) assert_equal(bigcontent[100..-1], File.read("bigdst")) + } + end - File.unlink("bigdst") - ret = IO.copy_stream("bigsrc", "bigdst", 30000, 100) + def test_copy_stream_bigcontent_mid + with_bigsrc {|bigsrc, bigcontent| + ret = IO.copy_stream(bigsrc, "bigdst", 30000, 100) assert_equal(30000, ret) assert_equal(bigcontent[100, 30000], File.read("bigdst")) + } + end - File.open("bigsrc") {|f| + def test_copy_stream_bigcontent_fpos + with_bigsrc {|bigsrc, bigcontent| + File.open(bigsrc) {|f| begin assert_equal(0, f.pos) ret = IO.copy_stream(f, "bigdst", nil, 10) @@ -468,56 +784,90 @@ class TestIO < Test::Unit::TestCase assert_equal(bigcontent[30, 40], File.read("bigdst")) assert_equal(0, f.pos) rescue NotImplementedError - #skip "pread(2) is not implemtented." + #skip "pread(2) is not implemented." end } + } + end + def test_copy_stream_closed_pipe + with_srccontent {|src,| with_pipe {|r, w| w.close - assert_raise(IOError) { IO.copy_stream("src", w) } + assert_raise(IOError) { IO.copy_stream(src, w) } } + } + end - megacontent = "abc" * 1234567 - File.open("megasrc", "w") {|f| f << megacontent } + def with_megacontent + yield "abc" * 1234567 + end - if have_nonblock? + def with_megasrc + mkcdtmpdir { + with_megacontent {|megacontent| + megasrc = "megasrc" + File.open(megasrc, "w") {|f| f << megacontent } + yield megasrc, megacontent + } + } + end + + if have_nonblock? + def test_copy_stream_megacontent_nonblock + with_megacontent {|megacontent| with_pipe {|r1, w1| with_pipe {|r2, w2| begin r1.nonblock = true w2.nonblock = true rescue Errno::EBADF - skip "nonblocking IO for pipe is not implemented" + omit "nonblocking IO for pipe is not implemented" end t1 = Thread.new { w1 << megacontent; w1.close } t2 = Thread.new { r2.read } - ret = IO.copy_stream(r1, w2) - assert_equal(megacontent.bytesize, ret) - w2.close - t1.join - assert_equal(megacontent, t2.value) + t3 = Thread.new { + ret = IO.copy_stream(r1, w2) + assert_equal(megacontent.bytesize, ret) + w2.close + } + _, t2_value, _ = assert_join_threads([t1, t2, t3]) + assert_equal(megacontent, t2_value) } } - end + } + end + end + def test_copy_stream_megacontent_pipe_to_file + with_megasrc {|megasrc, megacontent| with_pipe {|r1, w1| with_pipe {|r2, w2| t1 = Thread.new { w1 << megacontent; w1.close } t2 = Thread.new { r2.read } - ret = IO.copy_stream(r1, w2) - assert_equal(megacontent.bytesize, ret) - w2.close - t1.join - assert_equal(megacontent, t2.value) + t3 = Thread.new { + ret = IO.copy_stream(r1, w2) + assert_equal(megacontent.bytesize, ret) + w2.close + } + _, t2_value, _ = assert_join_threads([t1, t2, t3]) + assert_equal(megacontent, t2_value) } } + } + end + def test_copy_stream_megacontent_file_to_pipe + with_megasrc {|megasrc, megacontent| with_pipe {|r, w| - t = Thread.new { r.read } - ret = IO.copy_stream("megasrc", w) - assert_equal(megacontent.bytesize, ret) - w.close - assert_equal(megacontent, t.value) + t1 = Thread.new { r.read } + t2 = Thread.new { + ret = IO.copy_stream(megasrc, w) + assert_equal(megacontent.bytesize, ret) + w.close + } + t1_value, _ = assert_join_threads([t1, t2]) + assert_equal(megacontent, t1_value) } } end @@ -536,7 +886,7 @@ class TestIO < Test::Unit::TestCase assert_equal("bcd", r.read) end) rescue NotImplementedError - skip "pread(2) is not implemtented." + omit "pread(2) is not implemtented." end } end @@ -552,12 +902,9 @@ class TestIO < Test::Unit::TestCase end def test_copy_stream_socket1 - mkcdtmpdir { - content = "foobar" - File.open("src", "w") {|f| f << content } - + with_srccontent("foobar") {|src, content| with_socketpair {|s1, s2| - ret = IO.copy_stream("src", s1) + ret = IO.copy_stream(src, s1) assert_equal(content.bytesize, ret) s1.close assert_equal(content, s2.read) @@ -566,79 +913,87 @@ class TestIO < Test::Unit::TestCase end if defined? UNIXSocket def test_copy_stream_socket2 - mkcdtmpdir { - bigcontent = "abc" * 123456 - File.open("bigsrc", "w") {|f| f << bigcontent } - + with_bigsrc {|bigsrc, bigcontent| with_socketpair {|s1, s2| - t = Thread.new { s2.read } - ret = IO.copy_stream("bigsrc", s1) - assert_equal(bigcontent.bytesize, ret) - s1.close - result = t.value + t1 = Thread.new { s2.read } + t2 = Thread.new { + ret = IO.copy_stream(bigsrc, s1) + assert_equal(bigcontent.bytesize, ret) + s1.close + } + result, _ = assert_join_threads([t1, t2]) assert_equal(bigcontent, result) } } end if defined? UNIXSocket def test_copy_stream_socket3 - mkcdtmpdir { - bigcontent = "abc" * 123456 - File.open("bigsrc", "w") {|f| f << bigcontent } - + with_bigsrc {|bigsrc, bigcontent| with_socketpair {|s1, s2| - t = Thread.new { s2.read } - ret = IO.copy_stream("bigsrc", s1, 10000) - assert_equal(10000, ret) - s1.close - result = t.value + t1 = Thread.new { s2.read } + t2 = Thread.new { + ret = IO.copy_stream(bigsrc, s1, 10000) + assert_equal(10000, ret) + s1.close + } + result, _ = assert_join_threads([t1, t2]) assert_equal(bigcontent[0,10000], result) } } end if defined? UNIXSocket def test_copy_stream_socket4 - mkcdtmpdir { - bigcontent = "abc" * 123456 - File.open("bigsrc", "w") {|f| f << bigcontent } + if RUBY_PLATFORM =~ /mingw|mswin/ + omit "pread(2) is not implemented." + end - File.open("bigsrc") {|f| + with_bigsrc {|bigsrc, bigcontent| + File.open(bigsrc) {|f| assert_equal(0, f.pos) with_socketpair {|s1, s2| - t = Thread.new { s2.read } - ret = IO.copy_stream(f, s1, nil, 100) - assert_equal(bigcontent.bytesize-100, ret) - assert_equal(0, f.pos) - s1.close - result = t.value + t1 = Thread.new { s2.read } + t2 = Thread.new { + ret = IO.copy_stream(f, s1, nil, 100) + assert_equal(bigcontent.bytesize-100, ret) + assert_equal(0, f.pos) + s1.close + } + result, _ = assert_join_threads([t1, t2]) assert_equal(bigcontent[100..-1], result) } } } - end if defined? UNIXSocket + end def test_copy_stream_socket5 - mkcdtmpdir { - bigcontent = "abc" * 123456 - File.open("bigsrc", "w") {|f| f << bigcontent } + if RUBY_PLATFORM =~ /mingw|mswin/ + omit "pread(2) is not implemented." + end - File.open("bigsrc") {|f| + with_bigsrc {|bigsrc, bigcontent| + File.open(bigsrc) {|f| assert_equal(bigcontent[0,100], f.read(100)) assert_equal(100, f.pos) with_socketpair {|s1, s2| - t = Thread.new { s2.read } - ret = IO.copy_stream(f, s1) - assert_equal(bigcontent.bytesize-100, ret) - assert_equal(bigcontent.length, f.pos) - s1.close - result = t.value + t1 = Thread.new { s2.read } + t2 = Thread.new { + ret = IO.copy_stream(f, s1) + assert_equal(bigcontent.bytesize-100, ret) + assert_equal(bigcontent.length, f.pos) + s1.close + } + result, _ = assert_join_threads([t1, t2]) assert_equal(bigcontent[100..-1], result) } } } - end if defined? UNIXSocket + end def test_copy_stream_socket6 + if RUBY_PLATFORM =~ /mingw|mswin/ + omit "pread(2) is not implemented." + end + mkcdtmpdir { megacontent = "abc" * 1234567 File.open("megasrc", "w") {|f| f << megacontent } @@ -647,19 +1002,26 @@ class TestIO < Test::Unit::TestCase begin s1.nonblock = true rescue Errno::EBADF - skip "nonblocking IO for pipe is not implemented" + omit "nonblocking IO for pipe is not implemented" end - t = Thread.new { s2.read } - ret = IO.copy_stream("megasrc", s1) - assert_equal(megacontent.bytesize, ret) - s1.close - result = t.value + t1 = Thread.new { s2.read } + t2 = Thread.new { + ret = IO.copy_stream("megasrc", s1) + assert_equal(megacontent.bytesize, ret) + s1.close + } + result, _ = assert_join_threads([t1, t2]) assert_equal(megacontent, result) } } - end if defined? UNIXSocket + end def test_copy_stream_socket7 + if RUBY_PLATFORM =~ /mingw|mswin/ + omit "pread(2) is not implemented." + end + + GC.start mkcdtmpdir { megacontent = "abc" * 1234567 File.open("megasrc", "w") {|f| f << megacontent } @@ -668,31 +1030,32 @@ class TestIO < Test::Unit::TestCase begin s1.nonblock = true rescue Errno::EBADF - skip "nonblocking IO for pipe is not implemented" + omit "nonblocking IO for pipe is not implemented" end - trapping_usr1 do + trapping_usr2 do |rd| nr = 30 begin pid = fork do s1.close IO.select([s2]) - Process.kill(:USR1, Process.ppid) - s2.read + Process.kill(:USR2, Process.ppid) + buf = String.new(capacity: 16384) + nil while s2.read(16384, buf) end s2.close nr.times do assert_equal megacontent.bytesize, IO.copy_stream("megasrc", s1) end - assert_equal(1, @usr1_rcvd) + assert_equal(1, rd.read(4).unpack1('L')) ensure s1.close _, status = Process.waitpid2(pid) if pid end - assert status.success?, status.inspect + assert_predicate(status, :success?) end } } - end if defined? UNIXSocket and IO.method_defined?("nonblock=") + end def test_copy_stream_strio src = StringIO.new("abcd") @@ -770,6 +1133,101 @@ class TestIO < Test::Unit::TestCase } end + def test_copy_stream_strio_to_tempfile + bug11015 = '[ruby-core:68676] [Bug #11015]' + # StringIO to Tempfile + src = StringIO.new("abcd") + dst = Tempfile.new("baz") + ret = IO.copy_stream(src, dst) + assert_equal(4, ret) + pos = dst.pos + dst.rewind + assert_equal("abcd", dst.read) + assert_equal(4, pos, bug11015) + ensure + dst.close! + end + + def test_copy_stream_pathname_to_pathname + bug11199 = '[ruby-dev:49008] [Bug #11199]' + mkcdtmpdir { + File.open("src", "w") {|f| f << "ok" } + src = Pathname.new("src") + dst = Pathname.new("dst") + IO.copy_stream(src, dst) + assert_equal("ok", IO.read("dst"), bug11199) + } + end + + def test_copy_stream_dup_buffer + bug21131 = '[ruby-core:120961] [Bug #21131]' + mkcdtmpdir do + dst_class = Class.new do + def initialize(&block) + @block = block + end + + def write(data) + @block.call(data.dup) + data.bytesize + end + end + + rng = Random.new(42) + body = Tempfile.new("ruby-bug", binmode: true) + body.write(rng.bytes(16_385)) + body.rewind + + payload = [] + IO.copy_stream(body, dst_class.new{payload << it}) + body.rewind + assert_equal(body.read, payload.join, bug21131) + ensure + body&.close + end + end + + def test_copy_stream_write_in_binmode + bug8767 = '[ruby-core:56518] [Bug #8767]' + mkcdtmpdir { + EnvUtil.with_default_internal(Encoding::UTF_8) do + # StringIO to object with to_path + bytes = "\xDE\xAD\xBE\xEF".force_encoding(Encoding::ASCII_8BIT) + src = StringIO.new(bytes) + dst = Object.new + def dst.to_path + "qux" + end + assert_nothing_raised(bug8767) { + IO.copy_stream(src, dst) + } + assert_equal(bytes, File.binread("qux"), bug8767) + assert_equal(4, src.pos, bug8767) + end + } + end + + def test_copy_stream_read_in_binmode + bug8767 = '[ruby-core:56518] [Bug #8767]' + mkcdtmpdir { + EnvUtil.with_default_internal(Encoding::UTF_8) do + # StringIO to object with to_path + bytes = "\xDE\xAD\xBE\xEF".force_encoding(Encoding::ASCII_8BIT) + File.binwrite("qux", bytes) + dst = StringIO.new + src = Object.new + def src.to_path + "qux" + end + assert_nothing_raised(bug8767) { + IO.copy_stream(src, dst) + } + assert_equal(bytes, dst.string.b, bug8767) + assert_equal(4, dst.pos, bug8767) + end + } + end + class Rot13IO def initialize(io) @io = io @@ -839,11 +1297,12 @@ class TestIO < Test::Unit::TestCase w.write "zz" src = StringIO.new("abcd") IO.copy_stream(src, w) - t = Thread.new { + t1 = Thread.new { w.close } - assert_equal("zzabcd", r.read) - t.join + t2 = Thread.new { r.read } + _, result = assert_join_threads([t1, t2]) + assert_equal("zzabcd", result) } end @@ -917,24 +1376,36 @@ class TestIO < Test::Unit::TestCase } end - def safe_4 - t = Thread.new do - $SAFE = 4 - yield - end - unless t.join(10) - t.kill - flunk("timeout in safe_4") - end + def test_copy_stream_to_duplex_io + result = IO.pipe {|a,w| + th = Thread.start {w.puts "yes"; w.close} + IO.popen([EnvUtil.rubybin, '-pe$_="#$.:#$_"'], "r+") {|b| + IO.copy_stream(a, b) + b.close_write + assert_join_threads([th]) + b.read + } + } + assert_equal("1:yes\n", result) end def ruby(*args) args = ['-e', '$>.write($<.read)'] if args.empty? ruby = EnvUtil.rubybin - f = IO.popen([ruby] + args, 'r+') + opts = {} + if defined?(Process::RLIMIT_NPROC) + lim = Process.getrlimit(Process::RLIMIT_NPROC)[1] + opts[:rlimit_nproc] = [lim, 2048].min + end + f = IO.popen([ruby] + args, 'r+', opts) + pid = f.pid yield(f) ensure f.close unless !f || f.closed? + begin + Process.wait(pid) + rescue Errno::ECHILD, Errno::ESRCH + end end def test_try_convert @@ -955,6 +1426,70 @@ class TestIO < Test::Unit::TestCase end) end + def test_write_with_multiple_arguments + pipe(proc do |w| + w.write("foo", "bar") + w.close + end, proc do |r| + assert_equal("foobar", r.read) + end) + end + + def test_write_with_multiple_arguments_and_buffer + mkcdtmpdir do + line = "x"*9+"\n" + file = "test.out" + open(file, "wb") do |w| + w.write(line) + assert_equal(11, w.write(line, "\n")) + end + open(file, "rb") do |r| + assert_equal([line, line, "\n"], r.readlines) + end + + line = "x"*99+"\n" + open(file, "wb") do |w| + w.write(line*81) # 8100 bytes + assert_equal(100, w.write("a"*99, "\n")) + end + open(file, "rb") do |r| + 81.times {assert_equal(line, r.gets)} + assert_equal("a"*99+"\n", r.gets) + end + end + end + + def test_write_with_many_arguments + [1023, 1024].each do |n| + pipe(proc do |w| + w.write(*(["a"] * n)) + w.close + end, proc do |r| + assert_equal("a" * n, r.read) + end) + end + end + + def test_write_with_multiple_nonstring_arguments + assert_in_out_err([], "STDOUT.write(:foo, :bar)", ["foobar"]) + end + + def test_write_buffered_with_multiple_arguments + out, err, (_, status) = EnvUtil.invoke_ruby(["-e", "sleep 0.1;puts 'foo'"], "", true, true) do |_, o, e, i| + [o.read, e.read, Process.waitpid2(i)] + end + assert_predicate(status, :success?) + assert_equal("foo\n", out) + assert_empty(err) + end + + def test_write_no_args + IO.pipe do |r, w| + assert_equal 0, w.write, '[ruby-core:86285] [Bug #14338]' + assert_equal :wait_readable, r.read_nonblock(1, exception: false) + end + end + def test_write_non_writable with_pipe do |r, w| assert_raise(IOError) do @@ -965,44 +1500,49 @@ class TestIO < Test::Unit::TestCase def test_dup ruby do |f| - f2 = f.dup - f.puts "foo" - f2.puts "bar" - f.close_write - f2.close_write - assert_equal("foo\nbar\n", f.read) - assert_equal("", f2.read) + begin + f2 = f.dup + f.puts "foo" + f2.puts "bar" + f.close_write + f2.close_write + assert_equal("foo\nbar\n", f.read) + assert_equal("", f2.read) + ensure + f2.close + end end end def test_dup_many - ruby('-e', <<-'End') {|f| - ok = 0 + opts = {} + opts[:rlimit_nofile] = 1024 if defined?(Process::RLIMIT_NOFILE) + assert_separately([], <<-'End', **opts) a = [] - begin + assert_raise(Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM) do loop {a << IO.pipe} - rescue Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM - ok += 1 end - print "no" if ok != 1 - begin + assert_raise(Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM) do loop {a << [a[-1][0].dup, a[-1][1].dup]} - rescue Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM - ok += 1 end - print "no" if ok != 2 - print "ok" End - assert_equal("ok", f.read) - } + end + + def test_dup_timeout + with_pipe do |r, w| + r.timeout = 0.1 + r2 = r.dup + assert_equal(0.1, r2.timeout) + ensure + r2&.close + end end def test_inspect with_pipe do |r, w| assert_match(/^#<IO:fd \d+>$/, r.inspect) - assert_raise(SecurityError) do - safe_4 { r.inspect } - end + r.freeze + assert_match(/^#<IO:fd \d+>$/, r.inspect) end end @@ -1051,6 +1591,13 @@ class TestIO < Test::Unit::TestCase end) end + def test_readpartial_zero_size + File.open(IO::NULL) do |r| + assert_empty(r.readpartial(0, s = "01234567")) + assert_empty(s) + end + end + def test_readpartial_buffer_error with_pipe do |r, w| s = "" @@ -1060,7 +1607,7 @@ class TestIO < Test::Unit::TestCase t.value assert_equal("", s) end - end + end if /cygwin/ !~ RUBY_PLATFORM def test_read pipe(proc do |w| @@ -1096,6 +1643,13 @@ class TestIO < Test::Unit::TestCase end) end + def test_read_zero_size + File.open(IO::NULL) do |r| + assert_empty(r.read(0, s = "01234567")) + assert_empty(s) + end + end + def test_read_buffer_error with_pipe do |r, w| s = "" @@ -1105,10 +1659,17 @@ class TestIO < Test::Unit::TestCase t.value assert_equal("", s) end - end + with_pipe do |r, w| + s = "xxx" + t = Thread.new {r.read(2, s)} + Thread.pass until t.stop? + t.kill + t.value + assert_equal("xxx", s) + end + end if /cygwin/ !~ RUBY_PLATFORM def test_write_nonblock - skip "IO#write_nonblock is not supported on file/pipe." if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM pipe(proc do |w| w.write_nonblock(1) w.close @@ -1118,7 +1679,6 @@ class TestIO < Test::Unit::TestCase end def test_read_nonblock_with_not_empty_buffer - skip "IO#read_nonblock is not supported on file/pipe." if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM with_pipe {|r, w| w.write "foob" w.close @@ -1127,9 +1687,45 @@ class TestIO < Test::Unit::TestCase } end + def test_read_nonblock_zero_size + File.open(IO::NULL) do |r| + assert_empty(r.read_nonblock(0, s = "01234567")) + assert_empty(s) + end + end + + def test_read_nonblock_file + make_tempfile do |path| + File.open(path, 'r') do |file| + file.read_nonblock(4) + end + end + end + + def test_write_nonblock_file + make_tempfile do |path| + File.open(path, 'w') do |file| + file.write_nonblock("Ruby") + end + end + end + + def test_explicit_path + io = IO.for_fd(0, path: "Fake Path", autoclose: false) + assert_match %r"Fake Path", io.inspect + assert_equal "Fake Path", io.path + end + + def test_write_nonblock_simple_no_exceptions + pipe(proc do |w| + w.write_nonblock('1', exception: false) + w.close + end, proc do |r| + assert_equal("1", r.read) + end) + end + def test_read_nonblock_error - return if !have_nonblock? - skip "IO#read_nonblock is not supported on file/pipe." if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM with_pipe {|r, w| begin r.read_nonblock 4096 @@ -1137,11 +1733,46 @@ class TestIO < Test::Unit::TestCase assert_kind_of(IO::WaitReadable, $!) end } - end + + with_pipe {|r, w| + begin + r.read_nonblock 4096, "" + rescue Errno::EWOULDBLOCK + assert_kind_of(IO::WaitReadable, $!) + end + } + end if have_nonblock? + + def test_read_nonblock_invalid_exception + with_pipe {|r, w| + assert_raise(ArgumentError) {r.read_nonblock(4096, exception: 1)} + } + end if have_nonblock? + + def test_read_nonblock_no_exceptions + with_pipe {|r, w| + assert_equal :wait_readable, r.read_nonblock(4096, exception: false) + w.puts "HI!" + assert_equal "HI!\n", r.read_nonblock(4096, exception: false) + w.close + assert_equal nil, r.read_nonblock(4096, exception: false) + } + end if have_nonblock? + + def test_read_nonblock_with_buffer_no_exceptions + with_pipe {|r, w| + assert_equal :wait_readable, r.read_nonblock(4096, "", exception: false) + w.puts "HI!" + buf = "buf" + value = r.read_nonblock(4096, buf, exception: false) + assert_equal value, "HI!\n" + assert_same(buf, value) + w.close + assert_equal nil, r.read_nonblock(4096, "", exception: false) + } + end if have_nonblock? def test_write_nonblock_error - return if !have_nonblock? - skip "IO#write_nonblock is not supported on file/pipe." if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM with_pipe {|r, w| begin loop { @@ -1151,7 +1782,25 @@ class TestIO < Test::Unit::TestCase assert_kind_of(IO::WaitWritable, $!) end } - end + end if have_nonblock? + + def test_write_nonblock_invalid_exception + with_pipe {|r, w| + assert_raise(ArgumentError) {w.write_nonblock(4096, exception: 1)} + } + end if have_nonblock? + + def test_write_nonblock_no_exceptions + with_pipe {|r, w| + loop { + ret = w.write_nonblock("a"*100000, exception: false) + if ret.is_a?(Symbol) + assert_equal :wait_writable, ret + break + end + } + } + end if have_nonblock? def test_gets pipe(proc do |w| @@ -1168,6 +1817,9 @@ class TestIO < Test::Unit::TestCase f.close_read f.write "foobarbaz" assert_raise(IOError) { f.read } + assert_nothing_raised(IOError) {f.close_read} + assert_nothing_raised(IOError) {f.close} + assert_nothing_raised(IOError) {f.close_read} end end @@ -1175,15 +1827,21 @@ class TestIO < Test::Unit::TestCase with_pipe do |r, w| r.close_read assert_raise(Errno::EPIPE) { w.write "foobarbaz" } + assert_nothing_raised(IOError) {r.close_read} + assert_nothing_raised(IOError) {r.close} + assert_nothing_raised(IOError) {r.close_read} end end - def test_close_read_security_error - with_pipe do |r, w| - assert_raise(SecurityError) do - safe_4 { r.close_read } - end - end + def test_write_epipe_nosync + assert_separately([], <<-"end;") + r, w = IO.pipe + r.close + w.sync = false + assert_raise(Errno::EPIPE) { + loop { w.write "a" } + } + end; end def test_close_read_non_readable @@ -1199,41 +1857,64 @@ class TestIO < Test::Unit::TestCase f.write "foobarbaz" f.close_write assert_equal("foobarbaz", f.read) + assert_nothing_raised(IOError) {f.close_write} + assert_nothing_raised(IOError) {f.close} + assert_nothing_raised(IOError) {f.close_write} end end - def test_close_write_security_error + def test_close_write_non_readable with_pipe do |r, w| - assert_raise(SecurityError) do - safe_4 { r.close_write } + assert_raise(IOError) do + r.close_write end end end - def test_close_write_non_readable - with_pipe do |r, w| - assert_raise(IOError) do - r.close_write + def test_close_read_write_separately + bug = '[ruby-list:49598]' + (1..10).each do |i| + assert_nothing_raised(IOError, "#{bug} trying ##{i}") do + IO.popen(EnvUtil.rubybin, "r+") {|f| + th = Thread.new {f.close_write} + f.close_read + th.join + } end end end def test_pid - r, w = IO.pipe - assert_equal(nil, r.pid) - assert_equal(nil, w.pid) - - pipe = IO.popen(EnvUtil.rubybin, "r+") - pid1 = pipe.pid - pipe.puts "p $$" - pipe.close_write - pid2 = pipe.read.chomp.to_i - assert_equal(pid2, pid1) - assert_equal(pid2, pipe.pid) - pipe.close + IO.pipe {|r, w| + assert_equal(nil, r.pid) + assert_equal(nil, w.pid) + } + + begin + pipe = IO.popen(EnvUtil.rubybin, "r+") + pid1 = pipe.pid + pipe.puts "p $$" + pipe.close_write + pid2 = pipe.read.chomp.to_i + assert_equal(pid2, pid1) + assert_equal(pid2, pipe.pid) + ensure + pipe.close + end assert_raise(IOError) { pipe.pid } end + def test_pid_after_close_read + pid1 = pid2 = nil + IO.popen("exit ;", "r+") do |io| + pid1 = io.pid + io.close_read + pid2 = io.pid + end + assert_not_nil(pid1) + assert_equal(pid1, pid2) + end + def make_tempfile t = Tempfile.new("test_io") t.binmode @@ -1254,40 +1935,182 @@ class TestIO < Test::Unit::TestCase def test_set_lineno make_tempfile {|t| - ruby("-e", <<-SRC, t.path) do |f| + assert_separately(["-", t.path], <<-SRC) open(ARGV[0]) do |f| - p $. - f.gets; p $. - f.gets; p $. - f.lineno = 1000; p $. - f.gets; p $. - f.gets; p $. - f.rewind; p $. - f.gets; p $. - f.gets; p $. - f.gets; p $. - f.gets; p $. + assert_equal(0, $.) + f.gets; assert_equal(1, $.) + f.gets; assert_equal(2, $.) + f.lineno = 1000; assert_equal(2, $.) + f.gets; assert_equal(1001, $.) + f.gets; assert_equal(1001, $.) + f.rewind; assert_equal(1001, $.) + f.gets; assert_equal(1, $.) + f.gets; assert_equal(2, $.) + f.gets; assert_equal(3, $.) + f.gets; assert_equal(3, $.) end SRC - assert_equal("0,1,2,2,1001,1001,1001,1,2,3,3", f.read.chomp.gsub("\n", ",")) + } + end + + def test_set_lineno_gets + pipe(proc do |w| + w.puts "foo" + w.puts "bar" + w.puts "baz" + w.close + end, proc do |r| + r.gets; assert_equal(1, $.) + r.gets; assert_equal(2, $.) + r.lineno = 1000; assert_equal(2, $.) + r.gets; assert_equal(1001, $.) + r.gets; assert_equal(1001, $.) + end) + end + + def test_readline_bad_param_raises + File.open(__FILE__) do |f| + assert_raise(TypeError) do + f.readline Object.new end + end - pipe(proc do |w| - w.puts "foo" - w.puts "bar" - w.puts "baz" - w.close - end, proc do |r| - r.gets; assert_equal(1, $.) - r.gets; assert_equal(2, $.) - r.lineno = 1000; assert_equal(2, $.) - r.gets; assert_equal(1001, $.) - r.gets; assert_equal(1001, $.) - end) + File.open(__FILE__) do |f| + assert_raise(TypeError) do + f.readline 1, 2 + end + end + end + + def test_readline_raises + File.open(__FILE__) do |f| + assert_equal File.read(__FILE__), f.readline(nil) + assert_raise(EOFError) do + f.readline + end + end + end + + def test_readline_separators + File.open(__FILE__) do |f| + line = f.readline("def") + assert_equal File.read(__FILE__)[/\A.*?def/m], line + end + + File.open(__FILE__) do |f| + line = f.readline("def", chomp: true) + assert_equal File.read(__FILE__)[/\A.*?(?=def)/m], line + end + end + + def test_readline_separators_limits + t = Tempfile.open("readline_limit") + str = "#" * 50 + sep = "def" + + t.write str + t.write sep + t.write str + t.flush + + # over limit + File.open(t.path) do |f| + line = f.readline sep, str.bytesize + assert_equal(str, line) + end + + # under limit + File.open(t.path) do |f| + line = f.readline(sep, str.bytesize + 5) + assert_equal(str + sep, line) + end + + # under limit + chomp + File.open(t.path) do |f| + line = f.readline(sep, str.bytesize + 5, chomp: true) + assert_equal(str, line) + end + ensure + t&.close! + end + + def test_readline_limit_without_separator + t = Tempfile.open("readline_limit") + str = "#" * 50 + sep = "\n" + + t.write str + t.write sep + t.write str + t.flush + + # over limit + File.open(t.path) do |f| + line = f.readline str.bytesize + assert_equal(str, line) + end + + # under limit + File.open(t.path) do |f| + line = f.readline(str.bytesize + 5) + assert_equal(str + sep, line) + end + + # under limit + chomp + File.open(t.path) do |f| + line = f.readline(str.bytesize + 5, chomp: true) + assert_equal(str, line) + end + ensure + t&.close! + end + + def test_readline_chomp_true + File.open(__FILE__) do |f| + line = f.readline(chomp: true) + assert_equal File.readlines(__FILE__).first.chomp, line + end + end + + def test_readline_incompatible_rs + first_line = File.open(__FILE__, &:gets).encode("utf-32le") + File.open(__FILE__, encoding: "utf-8:utf-32le") {|f| + assert_equal first_line, f.readline + assert_raise(ArgumentError) {f.readline("\0")} } end - def test_readline + def test_readline_limit_nonascii + mkcdtmpdir do + i = 0 + + File.open("text#{i+=1}", "w+:utf-8") do |f| + f.write("Test\nok\u{bf}ok\n") + f.rewind + + assert_equal("Test\nok\u{bf}", f.readline("\u{bf}")) + assert_equal("ok\n", f.readline("\u{bf}")) + end + + File.open("text#{i+=1}", "w+b:utf-32le") do |f| + f.write("0123456789") + f.rewind + + assert_equal(4, f.readline(4).bytesize) + assert_equal(4, f.readline(3).bytesize) + end + + File.open("text#{i+=1}", "w+:utf-8:utf-32le") do |f| + f.write("0123456789") + f.rewind + + assert_equal(4, f.readline(4).bytesize) + assert_equal(4, f.readline(3).bytesize) + end + end + end + + def test_set_lineno_readline pipe(proc do |w| w.puts "foo" w.puts "bar" @@ -1315,8 +2138,7 @@ class TestIO < Test::Unit::TestCase end) end - def test_lines - verbose, $VERBOSE = $VERBOSE, nil + def test_each_line pipe(proc do |w| w.puts "foo" w.puts "bar" @@ -1324,20 +2146,31 @@ class TestIO < Test::Unit::TestCase w.close end, proc do |r| e = nil - assert_warn(/deprecated/) { - e = r.lines + assert_warn('') { + e = r.each_line } assert_equal("foo\n", e.next) assert_equal("bar\n", e.next) assert_equal("baz\n", e.next) assert_raise(StopIteration) { e.next } end) - ensure - $VERBOSE = verbose + + pipe(proc do |w| + w.write "foo\n" + w.close + end, proc do |r| + assert_equal(["foo\n"], r.each_line(nil, chomp: true).to_a, "[Bug #18770]") + end) + + pipe(proc do |w| + w.write "foo\n" + w.close + end, proc do |r| + assert_equal(["fo", "o\n"], r.each_line(nil, 2, chomp: true).to_a, "[Bug #18770]") + end) end - def test_bytes - verbose, $VERBOSE = $VERBOSE, nil + def test_each_byte2 pipe(proc do |w| w.binmode w.puts "foo" @@ -1346,20 +2179,17 @@ class TestIO < Test::Unit::TestCase w.close end, proc do |r| e = nil - assert_warn(/deprecated/) { - e = r.bytes + assert_warn('') { + e = r.each_byte } (%w(f o o) + ["\n"] + %w(b a r) + ["\n"] + %w(b a z) + ["\n"]).each do |c| assert_equal(c.ord, e.next) end assert_raise(StopIteration) { e.next } end) - ensure - $VERBOSE = verbose end - def test_chars - verbose, $VERBOSE = $VERBOSE, nil + def test_each_char2 pipe(proc do |w| w.puts "foo" w.puts "bar" @@ -1367,16 +2197,14 @@ class TestIO < Test::Unit::TestCase w.close end, proc do |r| e = nil - assert_warn(/deprecated/) { - e = r.chars + assert_warn('') { + e = r.each_char } (%w(f o o) + ["\n"] + %w(b a r) + ["\n"] + %w(b a z) + ["\n"]).each do |c| assert_equal(c, e.next) end assert_raise(StopIteration) { e.next } end) - ensure - $VERBOSE = verbose end def test_readbyte @@ -1410,7 +2238,6 @@ class TestIO < Test::Unit::TestCase end def test_close_on_exec - skip "IO\#close_on_exec is not implemented." unless have_close_on_exec? ruby do |f| assert_equal(true, f.close_on_exec?) f.close_on_exec = false @@ -1438,19 +2265,10 @@ class TestIO < Test::Unit::TestCase w.close_on_exec = false assert_equal(false, w.close_on_exec?) end - end - - def test_close_security_error - with_pipe do |r, w| - assert_raise(SecurityError) do - safe_4 { r.close } - end - end - end + end if have_close_on_exec? def test_pos make_tempfile {|t| - open(t.path, IO::RDWR|IO::CREAT|IO::TRUNC, 0600) do |f| f.write "Hello" assert_equal(5, f.pos) @@ -1465,7 +2283,7 @@ class TestIO < Test::Unit::TestCase end def test_pos_with_getc - bug6179 = '[ruby-core:43497]' + _bug6179 = '[ruby-core:43497]' make_tempfile {|t| ["", "t", "b"].each do |mode| open(t.path, "w#{mode}") do |f| @@ -1488,6 +2306,25 @@ class TestIO < Test::Unit::TestCase } end + def can_seek_data(f) + if /linux/ =~ RUBY_PLATFORM + require "-test-/file" + # lseek(2) + case Bug::File::Fs.fsname(f.path) + when "btrfs" + return true if (Etc.uname[:release].split('.').map(&:to_i) <=> [3,1]) >= 0 + when "ocfs" + return true if (Etc.uname[:release].split('.').map(&:to_i) <=> [3,2]) >= 0 + when "xfs" + return true if (Etc.uname[:release].split('.').map(&:to_i) <=> [3,5]) >= 0 + when "ext4" + return true if (Etc.uname[:release].split('.').map(&:to_i) <=> [3,8]) >= 0 + when "tmpfs" + return true if (Etc.uname[:release].split('.').map(&:to_i) <=> [3,8]) >= 0 + end + end + false + end def test_seek make_tempfile {|t| @@ -1511,6 +2348,34 @@ class TestIO < Test::Unit::TestCase f.seek(2, IO::SEEK_CUR) assert_equal("r\nbaz\n", f.read) } + + if defined?(IO::SEEK_DATA) + open(t.path) { |f| + break unless can_seek_data(f) + assert_equal("foo\n", f.gets) + f.seek(0, IO::SEEK_DATA) + assert_equal("foo\nbar\nbaz\n", f.read) + } + open(t.path, 'r+') { |f| + break unless can_seek_data(f) + f.seek(100*1024, IO::SEEK_SET) + f.print("zot\n") + f.seek(50*1024, IO::SEEK_DATA) + assert_operator(f.pos, :>=, 50*1024) + assert_match(/\A\0*zot\n\z/, f.read) + } + end + + if defined?(IO::SEEK_HOLE) + open(t.path) { |f| + break unless can_seek_data(f) + assert_equal("foo\n", f.gets) + f.seek(0, IO::SEEK_HOLE) + assert_operator(f.pos, :>, 20) + f.seek(100*1024, IO::SEEK_HOLE) + assert_equal("", f.read) + } + end } end @@ -1531,6 +2396,34 @@ class TestIO < Test::Unit::TestCase f.seek(2, :CUR) assert_equal("r\nbaz\n", f.read) } + + if defined?(IO::SEEK_DATA) + open(t.path) { |f| + break unless can_seek_data(f) + assert_equal("foo\n", f.gets) + f.seek(0, :DATA) + assert_equal("foo\nbar\nbaz\n", f.read) + } + open(t.path, 'r+') { |f| + break unless can_seek_data(f) + f.seek(100*1024, :SET) + f.print("zot\n") + f.seek(50*1024, :DATA) + assert_operator(f.pos, :>=, 50*1024) + assert_match(/\A\0*zot\n\z/, f.read) + } + end + + if defined?(IO::SEEK_HOLE) + open(t.path) { |f| + break unless can_seek_data(f) + assert_equal("foo\n", f.gets) + f.seek(0, :HOLE) + assert_operator(f.pos, :>, 20) + f.seek(100*1024, :HOLE) + assert_equal("", f.read) + } + end } end @@ -1580,6 +2473,14 @@ class TestIO < Test::Unit::TestCase end) end + def test_sysread_with_negative_length + make_tempfile {|t| + open(t.path) do |f| + assert_raise(ArgumentError) { f.sysread(-1) } + end + } + end + def test_flag make_tempfile {|t| assert_raise(ArgumentError) do @@ -1589,6 +2490,10 @@ class TestIO < Test::Unit::TestCase assert_raise(ArgumentError) do open(t.path, "rr") { } end + + assert_raise(ArgumentError) do + open(t.path, "rbt") { } + end } end @@ -1664,12 +2569,12 @@ class TestIO < Test::Unit::TestCase t.close rescue Errno::EBADF end - skip "expect IO object was GC'ed but not recycled yet" + omit "expect IO object was GC'ed but not recycled yet" rescue WeakRef::RefError assert_raise(Errno::EBADF, feature2250) {t.close} end ensure - t.unlink + t&.close! end def test_autoclose_false_closed_by_finalizer @@ -1680,12 +2585,12 @@ class TestIO < Test::Unit::TestCase begin w.close t.close - skip "expect IO object was GC'ed but not recycled yet" + omit "expect IO object was GC'ed but not recycled yet" rescue WeakRef::RefError assert_nothing_raised(Errno::EBADF, feature2250) {t.close} end ensure - t.unlink + t.close! end def test_open_redirect @@ -1699,22 +2604,35 @@ class TestIO < Test::Unit::TestCase assert_equal(o, o2) end - def test_open_pipe - open("|" + EnvUtil.rubybin, "r+") do |f| - f.puts "puts 'foo'" - f.close_write - assert_equal("foo\n", f.read) + def test_open_redirect_keyword + o = Object.new + def o.to_open(**kw); kw; end + assert_equal({:a=>1}, open(o, a: 1)) + + assert_raise(ArgumentError) { open(o, {a: 1}) } + + class << o + remove_method(:to_open) + end + def o.to_open(kw); kw; end + assert_equal({:a=>1}, open(o, a: 1)) + assert_equal({:a=>1}, open(o, {a: 1})) + end + + def test_path_with_pipe + mkcdtmpdir do + cmd = "|echo foo" + assert_file.not_exist?(cmd) + + pipe_errors = [Errno::ENOENT, Errno::EINVAL, Errno::EACCES, Errno::EPERM] + assert_raise(*pipe_errors) { open(cmd, "r+") } + assert_raise(*pipe_errors) { IO.read(cmd) } + assert_raise(*pipe_errors) { IO.foreach(cmd) {|x| assert false } } end end def test_reopen make_tempfile {|t| - with_pipe do |r, w| - assert_raise(SecurityError) do - safe_4 { r.reopen(t.path) } - end - end - open(__FILE__) do |f| f.gets assert_nothing_raised { @@ -1754,17 +2672,26 @@ class TestIO < Test::Unit::TestCase def test_reopen_inherit mkcdtmpdir { - system(EnvUtil.rubybin, '-e', <<"End") + system(EnvUtil.rubybin, '-e', <<-"End") f = open("out", "w") STDOUT.reopen(f) STDERR.reopen(f) system(#{EnvUtil.rubybin.dump}, '-e', 'STDOUT.print "out"') system(#{EnvUtil.rubybin.dump}, '-e', 'STDERR.print "err"') -End + End assert_equal("outerr", File.read("out")) } end + def test_reopen_stdio + mkcdtmpdir { + fname = 'bug11319' + File.write(fname, 'hello') + system(EnvUtil.rubybin, '-e', "STDOUT.reopen('#{fname}', 'w+')") + assert_equal('', File.read(fname)) + } + end + def test_reopen_mode feature7067 = '[ruby-core:47694]' make_tempfile {|t| @@ -1803,6 +2730,17 @@ End } end + def test_reopen_binmode + f1 = File.open(__FILE__) + f2 = File.open(__FILE__) + f1.binmode + f1.reopen(f2) + assert_not_operator(f1, :binmode?) + ensure + f2.close + f1.close + end + def make_tempfile_for_encoding t = make_tempfile open(t.path, "rb+:utf-8") {|f| f.puts "\u7d05\u7389bar\n"} @@ -1812,7 +2750,7 @@ End t end ensure - t.close(true) if t and block_given? + t&.close(true) if block_given? end def test_reopen_encoding @@ -1833,6 +2771,16 @@ End } end + def test_reopen_encoding_from_io + f1 = File.open(__FILE__, "rb:UTF-16LE") + f2 = File.open(__FILE__, "r:UTF-8") + f1.reopen(f2) + assert_equal(Encoding::UTF_8, f1.external_encoding) + ensure + f2.close + f1.close + end + def test_reopen_opt_encoding feature7103 = '[ruby-core:47806]' make_tempfile_for_encoding {|t| @@ -1852,26 +2800,53 @@ End } end - def test_foreach - a = [] - IO.foreach("|" + EnvUtil.rubybin + " -e 'puts :foo; puts :bar; puts :baz'") {|x| a << x } - assert_equal(["foo\n", "bar\n", "baz\n"], a) + bug11320 = '[ruby-core:69780] [Bug #11320]' + ["UTF-8", "EUC-JP", "Shift_JIS"].each do |enc| + define_method("test_reopen_nonascii(#{enc})") do + mkcdtmpdir do + fname = "\u{30eb 30d3 30fc}".encode(enc) + File.write(fname, '') + assert_file.exist?(fname) + stdin = $stdin.dup + begin + assert_nothing_raised(Errno::ENOENT, "#{bug11320}: #{enc}") { + $stdin.reopen(fname, 'r') + } + ensure + $stdin.reopen(stdin) + stdin.close + end + end + end + end + + def test_reopen_ivar + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + f = File.open(IO::NULL) + f.instance_variable_set(:@foo, 42) + f.reopen(STDIN) + f.instance_variable_defined?(:@foo) + f.instance_variable_get(:@foo) + end; + end + def test_foreach make_tempfile {|t| a = [] IO.foreach(t.path) {|x| a << x } assert_equal(["foo\n", "bar\n", "baz\n"], a) a = [] - IO.foreach(t.path, {:mode => "r" }) {|x| a << x } + IO.foreach(t.path, :mode => "r") {|x| a << x } assert_equal(["foo\n", "bar\n", "baz\n"], a) a = [] - IO.foreach(t.path, {:open_args => [] }) {|x| a << x } + IO.foreach(t.path, :open_args => []) {|x| a << x } assert_equal(["foo\n", "bar\n", "baz\n"], a) a = [] - IO.foreach(t.path, {:open_args => ["r"] }) {|x| a << x } + IO.foreach(t.path, :open_args => ["r"]) {|x| a << x } assert_equal(["foo\n", "bar\n", "baz\n"], a) a = [] @@ -1889,13 +2864,18 @@ End bug = '[ruby-dev:31525]' assert_raise(ArgumentError, bug) {IO.foreach} + assert_raise(ArgumentError, "[Bug #18767] [ruby-core:108499]") {IO.foreach(__FILE__, 0){}} + a = nil assert_nothing_raised(ArgumentError, bug) {a = IO.foreach(t.path).to_a} assert_equal(["foo\n", "bar\n", "baz\n"], a, bug) bug6054 = '[ruby-dev:45267]' - e = assert_raise(IOError, bug6054) {IO.foreach(t.path, mode:"w").next} - assert_match(/not opened for reading/, e.message, bug6054) + assert_raise_with_message(IOError, /not opened for reading/, bug6054) do + IO.foreach(t.path, mode:"w").next + end + + assert_raise(ArgumentError, "[Bug #18771] [ruby-core:108503]") {IO.foreach(t, "\n", 10, true){}} } end @@ -1905,6 +2885,7 @@ End assert_equal(["foo\nb", "ar\nb", "az\n"], IO.readlines(t.path, "b")) assert_equal(["fo", "o\n", "ba", "r\n", "ba", "z\n"], IO.readlines(t.path, 2)) assert_equal(["fo", "o\n", "b", "ar", "\nb", "az", "\n"], IO.readlines(t.path, "b", 2)) + assert_raise(ArgumentError, "[Bug #18771] [ruby-core:108503]") {IO.readlines(t, "\n", 10, true){}} } end @@ -1926,11 +2907,13 @@ End end def test_print_separators - $, = ':' - $\ = "\n" + assert_deprecated_warning(/non-nil '\$,'/) {$, = ":"} + assert_raise(TypeError) {$, = 1} + assert_deprecated_warning(/non-nil '\$\\'/) {$\ = "\n"} + assert_raise(TypeError) {$/ = 1} pipe(proc do |w| w.print('a') - w.print('a','b','c') + EnvUtil.suppress_warning {w.print('a','b','c')} w.close end, proc do |r| assert_equal("a\n", r.gets) @@ -1967,6 +2950,36 @@ End end) end + def test_puts_parallel + omit "not portable" + pipe(proc do |w| + threads = [] + 100.times do + threads << Thread.new { w.puts "hey" } + end + threads.each(&:join) + w.close + end, proc do |r| + assert_equal("hey\n" * 100, r.read) + end) + end + + def test_puts_old_write + capture = String.new + def capture.write(str) + self << str + end + + capture.clear + assert_deprecated_warning(/[.#]write is outdated/) do + stdout, $stdout = $stdout, capture + puts "hey" + ensure + $stdout = stdout + end + assert_equal("hey\n", capture) + end + def test_display pipe(proc do |w| "foo".display(w) @@ -1982,13 +2995,21 @@ End assert_raise(TypeError) { $> = Object.new } assert_in_out_err([], "$> = $stderr\nputs 'foo'", [], %w(foo)) + + assert_separately(%w[-Eutf-8], "#{<<~"begin;"}\n#{<<~"end;"}") + begin; + alias $\u{6a19 6e96 51fa 529b} $stdout + x = eval("class X\u{307b 3052}; self; end".encode("euc-jp")) + assert_raise_with_message(TypeError, /\\$\u{6a19 6e96 51fa 529b} must.*, X\u{307b 3052} given/) do + $\u{6a19 6e96 51fa 529b} = x.new + end + end; end def test_initialize return unless defined?(Fcntl::F_GETFL) make_tempfile {|t| - fd = IO.sysopen(t.path, "w") assert_kind_of(Integer, fd) %w[r r+ w+ a+].each do |mode| @@ -1999,6 +3020,15 @@ End f.close assert_equal("FOO\n", File.read(t.path)) + + fd = IO.sysopen(t.path) + %w[w r+ w+ a+].each do |mode| + assert_raise(Errno::EINVAL, "#{mode} [ruby-dev:38571]") {IO.new(fd, mode)} + end + f = IO.new(fd, "r") + data = f.read + f.close + assert_equal("FOO\n", data) } end @@ -2016,7 +3046,16 @@ End end def test_new_with_block - assert_in_out_err([], "r, w = IO.pipe; IO.new(r) {}", [], /^.+$/) + assert_in_out_err([], "r, w = IO.pipe; r.autoclose=false; IO.new(r.fileno) {}.close", [], /^.+$/) + n = "IO\u{5165 51fa 529b}" + c = eval("class #{n} < IO; self; end") + IO.pipe do |r, w| + assert_warning(/#{n}/) { + r.autoclose=false + io = c.new(r.fileno) {} + io.close + } + end end def test_readline2 @@ -2042,6 +3081,9 @@ End assert_equal("foo\nbar\nbaz\n", File.read(t.path)) assert_equal("foo\nba", File.read(t.path, 6)) assert_equal("bar\n", File.read(t.path, 4, 4)) + + assert_raise(ArgumentError) { File.read(t.path, -1) } + assert_raise(ArgumentError) { File.read(t.path, 1, -1) } } end @@ -2051,8 +3093,6 @@ End def test_nofollow # O_NOFOLLOW is not standard. - return if /freebsd|linux/ !~ RUBY_PLATFORM - return unless defined? File::NOFOLLOW mkcdtmpdir { open("file", "w") {|f| f << "content" } begin @@ -2067,14 +3107,7 @@ End File.foreach("slnk", :open_args=>[File::RDONLY|File::NOFOLLOW]) {} } } - end - - def test_tainted - make_tempfile {|t| - assert(File.read(t.path, 4).tainted?, '[ruby-dev:38826]') - assert(File.open(t.path) {|f| f.read(4)}.tainted?, '[ruby-dev:38826]') - } - end + end if /freebsd|linux/ =~ RUBY_PLATFORM and defined? File::NOFOLLOW def test_binmode_after_closed make_tempfile {|t| @@ -2082,16 +3115,24 @@ End } end + def test_DATA_binmode + assert_separately([], <<-SRC) +assert_not_predicate(DATA, :binmode?) +__END__ + SRC + end + def test_threaded_flush bug3585 = '[ruby-core:31348]' - src = %q{\ + src = "#{<<~"begin;"}\n#{<<~'end;'}" + begin; t = Thread.new { sleep 3 } Thread.new {sleep 1; t.kill; p 'hi!'} t.join - }.gsub(/^\s+/, '') + end; 10.times.map do Thread.start do - assert_in_out_err([], src) {|stdout, stderr| + assert_in_out_err([], src, timeout: 20) {|stdout, stderr| assert_no_match(/hi.*hi/, stderr.join, bug3585) } end @@ -2099,39 +3140,51 @@ End end def test_flush_in_finalizer1 - require 'tempfile' bug3910 = '[ruby-dev:42341]' - Tempfile.open("bug3910") {|t| + tmp = Tempfile.open("bug3910") {|t| path = t.path t.close fds = [] assert_nothing_raised(TypeError, bug3910) do 500.times { f = File.open(path, "w") + f.instance_variable_set(:@test_flush_in_finalizer1, true) fds << f.fileno f.print "hoge" } end - t.unlink + t } ensure - GC.start + ObjectSpace.each_object(File) {|f| + if f.instance_variables.include?(:@test_flush_in_finalizer1) + f.close + end + } + tmp.close! end def test_flush_in_finalizer2 - require 'tempfile' bug3910 = '[ruby-dev:42341]' - Tempfile.open("bug3910") {|t| + Tempfile.create("bug3910") {|t| path = t.path t.close - 1.times do - io = open(path,"w") - io.print "hoge" - end - assert_nothing_raised(TypeError, bug3910) do - GC.start + begin + 1.times do + io = open(path,"w") + io.instance_variable_set(:@test_flush_in_finalizer2, true) + io.print "hoge" + end + assert_nothing_raised(TypeError, bug3910) do + GC.start + end + ensure + ObjectSpace.each_object(File) {|f| + if f.instance_variables.include?(:@test_flush_in_finalizer2) + f.close + end + } end - t.unlink } end @@ -2157,13 +3210,56 @@ End } end + def os_and_fs(path) + uname = Etc.uname + os = "#{uname[:sysname]} #{uname[:release]}" + + fs = nil + if uname[:sysname] == 'Linux' + # [ruby-dev:45703] Old Linux's fadvise() doesn't work on tmpfs. + mount = `mount` + mountpoints = [] + mount.scan(/ on (\S+) type (\S+) /) { + mountpoints << [$1, $2] + } + mountpoints.sort_by {|mountpoint, fstype| mountpoint.length }.reverse_each {|mountpoint, fstype| + if path == mountpoint + fs = fstype + break + end + mountpoint += "/" if %r{/\z} !~ mountpoint + if path.start_with?(mountpoint) + fs = fstype + break + end + } + end + + if fs + "#{fs} on #{os}" + else + os + end + end + def test_advise make_tempfile {|tf| assert_raise(ArgumentError, "no arguments") { tf.advise } %w{normal random sequential willneed dontneed noreuse}.map(&:to_sym).each do |adv| [[0,0], [0, 20], [400, 2]].each do |offset, len| open(tf.path) do |t| - assert_equal(t.advise(adv, offset, len), nil) + ret = assert_nothing_raised(lambda { os_and_fs(tf.path) }) { + begin + t.advise(adv, offset, len) + rescue Errno::EINVAL => e + if /linux/ =~ RUBY_PLATFORM && (Etc.uname[:release].split('.').map(&:to_i) <=> [3,6]) < 0 + next # [ruby-core:65355] tmpfs is not supported + else + raise e + end + end + } + assert_nil(ret) assert_raise(ArgumentError, "superfluous arguments") do t.advise(adv, offset, len, offset) end @@ -2190,10 +3286,10 @@ End def test_invalid_advise feature4204 = '[ruby-dev:42887]' make_tempfile {|tf| - %w{Normal rand glark will_need zzzzzzzzzzzz \u2609}.map(&:to_sym).each do |adv| + %W{Normal rand glark will_need zzzzzzzzzzzz \u2609}.map(&:to_sym).each do |adv| [[0,0], [0, 20], [400, 2]].each do |offset, len| open(tf.path) do |t| - assert_raise(NotImplementedError, feature4204) { t.advise(adv, offset, len) } + assert_raise_with_message(NotImplementedError, /#{Regexp.quote(adv.inspect)}/, feature4204) { t.advise(adv, offset, len) } end end end @@ -2201,9 +3297,7 @@ End end def test_fcntl_lock_linux - return if /x86_64-linux/ !~ RUBY_PLATFORM # A binary form of struct flock depend on platform - - pad=0 + pad = 0 Tempfile.create(self.class.name) do |f| r, w = IO.pipe pid = fork do @@ -2231,11 +3325,10 @@ End Process.kill :TERM, pid Process.waitpid2(pid) end - end + end if /x86_64-linux/ =~ RUBY_PLATFORM and # A binary form of struct flock depend on platform + [nil].pack("p").bytesize == 8 # unless x32 platform. def test_fcntl_lock_freebsd - return if /freebsd/ !~ RUBY_PLATFORM # A binary form of struct flock depend on platform - start = 12 len = 34 sysid = 0 @@ -2266,7 +3359,7 @@ End Process.kill :TERM, pid Process.waitpid2(pid) end - end + end if /freebsd/ =~ RUBY_PLATFORM # A binary form of struct flock depend on platform def test_fcntl_dupfd Tempfile.create(self.class.name) do |f| @@ -2280,7 +3373,6 @@ End end def test_cross_thread_close_fd - skip "cross thread close causes hung-up if pipe." if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM with_pipe do |r,w| read_thread = Thread.new do begin @@ -2298,25 +3390,46 @@ End end def test_cross_thread_close_stdio - with_pipe do |r,w| - pid = fork do + omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM + + assert_separately([], <<-'end;') + IO.pipe do |r,w| $stdin.reopen(r) r.close read_thread = Thread.new do begin $stdin.read(1) - rescue => e + rescue IOError => e e end end sleep(0.1) until read_thread.stop? $stdin.close - read_thread.join - exit(IOError === read_thread.value) + assert_kind_of(IOError, read_thread.value) + end + end; + end + + def test_single_exception_on_close + a = [] + t = [] + 10.times do + r, w = IO.pipe + a << [r, w] + t << Thread.new do + while r.gets + end rescue IOError + Thread.current.pending_interrupt? end - assert Process.waitpid2(pid)[1].success? end - rescue NotImplementedError + a.each do |r, w| + w.write(-"\n") + w.close + r.close + end + t.each do |th| + assert_equal false, th.value, '[ruby-core:81581] [Bug #13632]' + end end def test_open_mode @@ -2367,9 +3480,15 @@ End assert_equal("\00f", File.read(path)) assert_equal(1, File.write(path, "f", 0, encoding: "UTF-8")) assert_equal("ff", File.read(path)) + File.write(path, "foo", Object.new => Object.new) + assert_equal("foo", File.read(path)) end end + def test_s_binread_does_not_leak_with_invalid_offset + assert_raise(Errno::EINVAL) { IO.binread(__FILE__, 0, -1) } + end + def test_s_binwrite mkcdtmpdir do path = "test_s_binwrite" @@ -2406,7 +3525,7 @@ End threads << Thread.new {write_file.print(i)} threads << Thread.new {read_file.read} end - threads.each {|t| t.join} + assert_join_threads(threads) assert(true, "[ruby-core:37197]") ensure read_file.close @@ -2432,34 +3551,31 @@ End def test_cloexec return unless defined? Fcntl::FD_CLOEXEC open(__FILE__) {|f| - assert(f.close_on_exec?) + assert_predicate(f, :close_on_exec?) g = f.dup begin - assert(g.close_on_exec?) + assert_predicate(g, :close_on_exec?) f.reopen(g) - assert(f.close_on_exec?) + assert_predicate(f, :close_on_exec?) ensure g.close end g = IO.new(f.fcntl(Fcntl::F_DUPFD)) begin - assert(g.close_on_exec?) + assert_predicate(g, :close_on_exec?) ensure g.close end } IO.pipe {|r,w| - assert(r.close_on_exec?) - assert(w.close_on_exec?) + assert_predicate(r, :close_on_exec?) + assert_predicate(w, :close_on_exec?) } end def test_ioctl_linux - return if /linux/ !~ RUBY_PLATFORM # Alpha, mips, sparc and ppc have an another ioctl request number scheme. # So, hardcoded 0x80045200 may fail. - return if /^i.?86|^x86_64/ !~ RUBY_PLATFORM - assert_nothing_raised do File.open('/dev/urandom'){|f1| entropy_count = "" @@ -2476,21 +3592,24 @@ End } end assert_equal(File.size(__FILE__), buf.unpack('i!')[0]) - end + end if /^(?:i.?86|x86_64)-linux/ =~ RUBY_PLATFORM def test_ioctl_linux2 - return if /linux/ !~ RUBY_PLATFORM - return if /^i.?86|^x86_64/ !~ RUBY_PLATFORM - - return unless system('tty', '-s') # stdin is not a terminal - File.open('/dev/tty') { |f| + return unless STDIN.tty? # stdin is not a terminal + begin + f = File.open('/dev/tty') + rescue Errno::ENOENT, Errno::ENXIO => e + omit e.message + else tiocgwinsz=0x5413 winsize="" assert_nothing_raised { f.ioctl(tiocgwinsz, winsize) } - } - end + ensure + f&.close + end + end if /^(?:i.?86|x86_64)-linux/ =~ RUBY_PLATFORM def test_setpos mkcdtmpdir { @@ -2541,72 +3660,99 @@ End assert_equal(2, $stderr.fileno) end + def test_frozen_fileno + bug9865 = '[ruby-dev:48241] [Bug #9865]' + with_pipe do |r,w| + fd = r.fileno + assert_equal(fd, r.freeze.fileno, bug9865) + end + end + + def test_frozen_autoclose + with_pipe do |r,w| + assert_equal(true, r.freeze.autoclose?) + end + end + def test_sysread_locktmp bug6099 = '[ruby-dev:45297]' buf = " " * 100 data = "a" * 100 with_pipe do |r,w| th = Thread.new {r.sysread(100, buf)} + Thread.pass until th.stop? - buf.replace("") - assert_empty(buf, bug6099) + + assert_equal 100, buf.bytesize + + msg = /can't modify string; temporarily locked/ + assert_raise_with_message(RuntimeError, msg) do + buf.replace("") + end + assert_predicate(th, :alive?) w.write(data) - Thread.pass while th.alive? th.join end assert_equal(data, buf, bug6099) end def test_readpartial_locktmp - skip "nonblocking mode is not supported for pipe on this platform" if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM bug6099 = '[ruby-dev:45297]' buf = " " * 100 data = "a" * 100 + th = nil with_pipe do |r,w| - r.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK) + r.nonblock = true th = Thread.new {r.readpartial(100, buf)} + Thread.pass until th.stop? - buf.replace("") - assert_empty(buf, bug6099) + + assert_equal 100, buf.bytesize + + msg = /can't modify string; temporarily locked/ + assert_raise_with_message(RuntimeError, msg) do + buf.replace("") + end + assert_predicate(th, :alive?) w.write(data) - Thread.pass while th.alive? th.join end assert_equal(data, buf, bug6099) - rescue RuntimeError # can't modify string; temporarily locked end def test_advise_pipe # we don't know if other platforms have a real posix_fadvise() - return if /linux/ !~ RUBY_PLATFORM with_pipe do |r,w| # Linux 2.6.15 and earlier returned EINVAL instead of ESPIPE - assert_raise(Errno::ESPIPE, Errno::EINVAL) { r.advise(:willneed) } - assert_raise(Errno::ESPIPE, Errno::EINVAL) { w.advise(:willneed) } + assert_raise(Errno::ESPIPE, Errno::EINVAL) { + r.advise(:willneed) or omit "fadvise(2) is not implemented" + } + assert_raise(Errno::ESPIPE, Errno::EINVAL) { + w.advise(:willneed) or omit "fadvise(2) is not implemented" + } end - end + end if /linux/ =~ RUBY_PLATFORM def assert_buffer_not_raise_shared_string_error bug6764 = '[ruby-core:46586]' + bug9847 = '[ruby-core:62643] [Bug #9847]' size = 28 data = [*"a".."z", *"A".."Z"].shuffle.join("") t = Tempfile.new("test_io") t.write(data) t.close - w = Tempfile.new("test_io") + w = [] assert_nothing_raised(RuntimeError, bug6764) do + buf = '' File.open(t.path, "r") do |r| - buf = '' while yield(r, size, buf) - w << buf + w << buf.dup end end end - w.close - assert_equal(data, w.open.read, bug6764) + assert_equal(data, w.join(""), bug9847) ensure t.close! - w.close! end def test_read_buffer_not_raise_shared_string_error @@ -2653,23 +3799,630 @@ End assert_normal_exit %q{ require "tempfile" - # try to raise RLIM_NOFILE to >FD_SETSIZE - # Unfortunately, ruby export FD_SETSIZE. then we assume it's 1024. + # Unfortunately, ruby doesn't export FD_SETSIZE. then we assume it's 1024. fd_setsize = 1024 + # try to raise RLIM_NOFILE to >FD_SETSIZE begin - Process.setrlimit(Process::RLIMIT_NOFILE, fd_setsize+10) - rescue =>e - # Process::RLIMIT_NOFILE couldn't be raised. skip the test + Process.setrlimit(Process::RLIMIT_NOFILE, fd_setsize+20) + rescue Errno::EPERM exit 0 end tempfiles = [] - (0..fd_setsize+1).map {|i| - tempfiles << Tempfile.open("test_io_select_with_many_files") + (0...fd_setsize).map {|i| + tempfiles << Tempfile.create("test_io_select_with_many_files") + } + + begin + IO.select(tempfiles) + ensure + tempfiles.each { |t| + t.close + File.unlink(t.path) + } + end + }, bug8080, timeout: 100 + end if defined?(Process::RLIMIT_NOFILE) + + def test_read_32bit_boundary + bug8431 = '[ruby-core:55098] [Bug #8431]' + make_tempfile {|t| + assert_separately(["-", bug8431, t.path], <<-"end;") + msg = ARGV.shift + f = open(ARGV[0], "rb") + f.seek(0xffff_ffff) + assert_nil(f.read(1), msg) + end; + } + end if /mswin|mingw/ =~ RUBY_PLATFORM + + def test_write_32bit_boundary + bug8431 = '[ruby-core:55098] [Bug #8431]' + make_tempfile {|t| + def t.close(unlink_now = false) + # TODO: Tempfile should deal with this delay on Windows? + # NOTE: re-opening with O_TEMPORARY does not work. + path = self.path + ret = super + if unlink_now + begin + File.unlink(path) + rescue Errno::ENOENT + rescue Errno::EACCES + sleep(2) + retry + end + end + ret + end + + begin + assert_separately(["-", bug8431, t.path], <<-"end;", timeout: 30) + msg = ARGV.shift + f = open(ARGV[0], "wb") + f.seek(0xffff_ffff) + begin + # this will consume very long time or fail by ENOSPC on a + # filesystem which sparse file is not supported + f.write('1') + pos = f.tell + rescue Errno::ENOSPC + omit "non-sparse file system" + rescue SystemCallError + else + assert_equal(0x1_0000_0000, pos, msg) + end + end; + rescue Timeout::Error + omit "Timeout because of slow file writing" + end + } + end if /mswin|mingw/ =~ RUBY_PLATFORM + + def test_read_unlocktmp_ensure + bug8669 = '[ruby-core:56121] [Bug #8669]' + + str = "" + IO.pipe {|r,| + t = Thread.new { + assert_raise(RuntimeError) { + r.read(nil, str) + } + } + sleep 0.1 until t.stop? + t.raise + sleep 0.1 while t.alive? + assert_nothing_raised(RuntimeError, bug8669) { str.clear } + t.join + } + end if /cygwin/ !~ RUBY_PLATFORM + + def test_readpartial_unlocktmp_ensure + bug8669 = '[ruby-core:56121] [Bug #8669]' + + str = "" + IO.pipe {|r, w| + t = Thread.new { + assert_raise(RuntimeError) { + r.readpartial(4096, str) + } + } + sleep 0.1 until t.stop? + t.raise + sleep 0.1 while t.alive? + assert_nothing_raised(RuntimeError, bug8669) { str.clear } + t.join + } + end if /cygwin/ !~ RUBY_PLATFORM + + def test_readpartial_bad_args + IO.pipe do |r, w| + w.write '.' + buf = String.new + assert_raise(ArgumentError) { r.readpartial(1, buf, exception: false) } + assert_raise(TypeError) { r.readpartial(1, exception: false) } + assert_equal [[r],[],[]], IO.select([r], nil, nil, 1) + assert_equal '.', r.readpartial(1) + end + end + + def test_sysread_unlocktmp_ensure + bug8669 = '[ruby-core:56121] [Bug #8669]' + + str = "" + IO.pipe {|r, w| + t = Thread.new { + assert_raise(RuntimeError) { + r.sysread(4096, str) + } } + sleep 0.1 until t.stop? + t.raise + sleep 0.1 while t.alive? + assert_nothing_raised(RuntimeError, bug8669) { str.clear } + t.join + } + end if /cygwin/ !~ RUBY_PLATFORM + + def test_exception_at_close + bug10153 = '[ruby-core:64463] [Bug #10153] exception in close at the end of block' + assert_raise(Errno::EBADF, bug10153) do + IO.pipe do |r, w| + assert_nothing_raised {IO.open(w.fileno) {}} + end + end + end + + def test_close_twice + open(__FILE__) {|f| + assert_equal(nil, f.close) + assert_equal(nil, f.close) + } + end + + def test_close_uninitialized + io = IO.allocate + assert_raise(IOError) { io.close } + end + + def test_open_fifo_does_not_block_other_threads + mkcdtmpdir do + File.mkfifo("fifo") + rescue NotImplementedError + else + assert_separately([], <<-'EOS') + t1 = Thread.new { + open("fifo", "r") {|r| + r.read + } + } + t2 = Thread.new { + open("fifo", "w") {|w| + w.write "foo" + } + } + t1_value, _ = assert_join_threads([t1, t2]) + assert_equal("foo", t1_value) + EOS + end + end + + def test_open_fifo_restart_at_signal_intterupt + mkcdtmpdir do + File.mkfifo("fifo") + rescue NotImplementedError + else + wait = EnvUtil.apply_timeout_scale(0.1) + data = "writing to fifo" + + # Do not use assert_separately, because reading from stdin + # prevents to reproduce [Bug #20708] + assert_in_out_err(["-e", "#{<<~"begin;"}\n#{<<~'end;'}"], [], [data]) + wait, data = #{wait}, #{data.dump} + ; + begin; + trap(:USR1) {} + Thread.new do + sleep wait; Process.kill(:USR1, $$) + sleep wait; File.write("fifo", data) + end + puts File.read("fifo") + end; + end + end if Signal.list[:USR1] # Pointless on platforms without signal + + def test_open_flag + make_tempfile do |t| + assert_raise(Errno::EEXIST){ open(t.path, File::WRONLY|File::CREAT, flags: File::EXCL){} } + assert_raise(Errno::EEXIST){ open(t.path, 'w', flags: File::EXCL){} } + assert_raise(Errno::EEXIST){ open(t.path, mode: 'w', flags: File::EXCL){} } + end + end + + def test_open_flag_binary + binary_enc = Encoding.find("BINARY") + make_tempfile do |t| + open(t.path, File::RDONLY, flags: File::BINARY) do |f| + assert_equal true, f.binmode? + assert_equal binary_enc, f.external_encoding + end + open(t.path, 'r', flags: File::BINARY) do |f| + assert_equal true, f.binmode? + assert_equal binary_enc, f.external_encoding + end + open(t.path, mode: 'r', flags: File::BINARY) do |f| + assert_equal true, f.binmode? + assert_equal binary_enc, f.external_encoding + end + open(t.path, File::RDONLY|File::BINARY) do |f| + assert_equal true, f.binmode? + assert_equal binary_enc, f.external_encoding + end + open(t.path, File::RDONLY|File::BINARY, autoclose: true) do |f| + assert_equal true, f.binmode? + assert_equal binary_enc, f.external_encoding + end + end + end if File::BINARY != 0 + + def test_exclusive_mode + make_tempfile do |t| + assert_raise(Errno::EEXIST){ open(t.path, 'wx'){} } + assert_raise(ArgumentError){ open(t.path, 'rx'){} } + assert_raise(ArgumentError){ open(t.path, 'ax'){} } + end + end + + def test_race_gets_and_close + opt = { signal: :ABRT, timeout: 10 } + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}", **opt) + bug13076 = '[ruby-core:78845] [Bug #13076]' + begin; + 10.times do |i| + a = [] + t = [] + 10.times do + r,w = IO.pipe + a << [r,w] + t << Thread.new do + begin + while r.gets + end + rescue IOError + end + end + end + a.each do |r,w| + w.puts "hoge" + w.close + r.close + end + t.each do |th| + assert_same(th, th.join(2), bug13076) + end + end + end; + end + + def test_race_closed_stream + omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM + + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") + begin; + bug13158 = '[ruby-core:79262] [Bug #13158]' + closed = nil + q = Thread::Queue.new + IO.pipe do |r, w| + thread = Thread.new do + begin + q << true + assert_raise_with_message(IOError, /stream closed/) do + while r.gets + end + end + ensure + closed = r.closed? + end + end + q.pop + sleep 0.01 until thread.stop? + r.close + thread.join + assert_equal(true, closed, bug13158 + ': stream should be closed') + end + end; + end + + if RUBY_ENGINE == "ruby" # implementation details + def test_foreach_rs_conversion + make_tempfile {|t| + a = [] + rs = Struct.new(:count).new(0) + def rs.to_str; self.count += 1; "\n"; end + IO.foreach(t.path, rs) {|x| a << x } + assert_equal(["foo\n", "bar\n", "baz\n"], a) + assert_equal(1, rs.count) + } + end + + def test_foreach_rs_invalid + make_tempfile {|t| + rs = Object.new + def rs.to_str; raise "invalid rs"; end + assert_raise(RuntimeError) do + IO.foreach(t.path, rs, mode:"w") {} + end + assert_equal(["foo\n", "bar\n", "baz\n"], IO.foreach(t.path).to_a) + } + end + + def test_foreach_limit_conversion + make_tempfile {|t| + a = [] + lim = Struct.new(:count).new(0) + def lim.to_int; self.count += 1; -1; end + IO.foreach(t.path, lim) {|x| a << x } + assert_equal(["foo\n", "bar\n", "baz\n"], a) + assert_equal(1, lim.count) + } + end + + def test_foreach_limit_invalid + make_tempfile {|t| + lim = Object.new + def lim.to_int; raise "invalid limit"; end + assert_raise(RuntimeError) do + IO.foreach(t.path, lim, mode:"w") {} + end + assert_equal(["foo\n", "bar\n", "baz\n"], IO.foreach(t.path).to_a) + } + end + + def test_readlines_rs_invalid + make_tempfile {|t| + rs = Object.new + def rs.to_str; raise "invalid rs"; end + assert_raise(RuntimeError) do + IO.readlines(t.path, rs, mode:"w") + end + assert_equal(["foo\n", "bar\n", "baz\n"], IO.readlines(t.path)) + } + end + + def test_readlines_limit_invalid + make_tempfile {|t| + lim = Object.new + def lim.to_int; raise "invalid limit"; end + assert_raise(RuntimeError) do + IO.readlines(t.path, lim, mode:"w") + end + assert_equal(["foo\n", "bar\n", "baz\n"], IO.readlines(t.path)) + } + end + + def test_closed_stream_in_rescue + omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM + + assert_separately([], "#{<<-"begin;"}\n#{<<~"end;"}") + begin; + 10.times do + assert_nothing_raised(RuntimeError, /frozen IOError/) do + IO.pipe do |r, w| + th = Thread.start {r.close} + r.gets + rescue IOError + # swallow pending exceptions + begin + sleep 0.001 + rescue IOError + retry + end + ensure + th.kill.join + end + end + end + end; + end + end + + def test_pread + make_tempfile { |t| + open(t.path) do |f| + assert_equal("bar", f.pread(3, 4)) + buf = "asdf" + assert_equal("bar", f.pread(3, 4, buf)) + assert_equal("bar", buf) + assert_raise(EOFError) { f.pread(1, f.size) } + end + } + end + + def test_pwrite + make_tempfile { |t| + open(t.path, IO::RDWR) do |f| + assert_equal(3, f.pwrite("ooo", 4)) + assert_equal("ooo", f.pread(3, 4)) + end + } + end + + def test_select_exceptfds + if Etc.uname[:sysname] == 'SunOS' + str = 'h'.freeze #(???) Only 1 byte with MSG_OOB on Solaris + else + str = 'hello'.freeze + end + + TCPServer.open('localhost', 0) do |svr| + con = TCPSocket.new('localhost', svr.addr[1]) + acc = svr.accept + assert_equal str.length, con.send(str, Socket::MSG_OOB) + set = IO.select(nil, nil, [acc], 30) + assert_equal([[], [], [acc]], set, 'IO#select exceptions array OK') + acc.close + con.close + end + end if Socket.const_defined?(:MSG_OOB) + + def test_select_timeout + assert_equal(nil, IO.select(nil,nil,nil,0)) + assert_equal(nil, IO.select(nil,nil,nil,0.0)) + assert_raise(TypeError) { IO.select(nil,nil,nil,"invalid-timeout") } + assert_raise(ArgumentError) { IO.select(nil,nil,nil,-1) } + assert_raise(ArgumentError) { IO.select(nil,nil,nil,-0.1) } + assert_raise(ArgumentError) { IO.select(nil,nil,nil,-Float::INFINITY) } + assert_raise(RangeError) { IO.select(nil,nil,nil,Float::NAN) } + IO.pipe {|r, w| + w << "x" + ret = [[r], [], []] + assert_equal(ret, IO.select([r],nil,nil,0.1)) + assert_equal(ret, IO.select([r],nil,nil,1)) + assert_equal(ret, IO.select([r],nil,nil,Float::INFINITY)) + } + end + + def test_recycled_fd_close + dot = -'.' + IO.pipe do |sig_rd, sig_wr| + noex = Thread.new do # everything right and never see exceptions :) + until sig_rd.wait_readable(0) + IO.pipe do |r, w| + assert_nil r.timeout + assert_nil w.timeout + + th = Thread.new { r.read(1) } + w.write(dot) + + assert_same th, th.join(15), '"good" reader timeout' + assert_equal(dot, th.value) + end + end + sig_rd.read(4) + end + 1000.times do |i| # stupid things and make exceptions: + IO.pipe do |r,w| + th = Thread.new do + begin + while r.gets + end + rescue IOError => e + e + end + end + Thread.pass until th.stop? + + r.close + assert_same th, th.join(30), '"bad" reader timeout' + assert_match(/stream closed/, th.value.message) + end + end + sig_wr.write 'done' + assert_same noex, noex.join(20), '"good" writer timeout' + assert_equal 'done', noex.value ,'r63216' + end + end + + def test_select_memory_leak + # avoid malloc arena explosion from glibc and jemalloc: + env = { + 'MALLOC_ARENA_MAX' => '1', + 'MALLOC_ARENA_TEST' => '1', + 'MALLOC_CONF' => 'narenas:1', + } + assert_no_memory_leak([env], "#{<<~"begin;"}\n#{<<~'else;'}", "#{<<~'end;'}", rss: true, timeout: 60) + begin; + r, w = IO.pipe + rset = [r] + wset = [w] + exc = StandardError.new(-"select used to leak on exception") + exc.set_backtrace([]) + Thread.new { IO.select(rset, wset, nil, 0) }.join + else; + th = Thread.new do + Thread.handle_interrupt(StandardError => :on_blocking) do + begin + IO.select(rset, wset) + rescue + retry + end while true + end + end + 50_000.times do + Thread.pass until th.stop? + th.raise(exc) + end + th.kill + th.join + end; + end + + def test_external_encoding_index + IO.pipe {|r, w| + assert_raise(TypeError) {Marshal.dump(r)} + assert_raise(TypeError) {Marshal.dump(w)} + } + end + + def test_marshal_closed_io + bug18077 = '[ruby-core:104927] [Bug #18077]' + r, w = IO.pipe + r.close; w.close + assert_raise(TypeError, bug18077) {Marshal.dump(r)} + + class << r + undef_method :closed? + end + assert_raise(TypeError, bug18077) {Marshal.dump(r)} + end + + def test_stdout_to_closed_pipe + EnvUtil.invoke_ruby(["-e", "loop {puts :ok}"], "", true, true) do + |in_p, out_p, err_p, pid| + out = out_p.gets + out_p.close + err = err_p.read + ensure + status = Process.wait2(pid)[1] + assert_equal("ok\n", out) + assert_empty(err) + assert_not_predicate(status, :success?) + if Signal.list["PIPE"] + assert_predicate(status, :signaled?) + assert_equal("PIPE", Signal.signame(status.termsig) || status.termsig) + end + end + end + + def test_blocking_timeout + assert_separately([], <<~'RUBY') + IO.pipe do |r, w| + trap(:INT) do + w.puts "INT" + end + + main = Thread.current + thread = Thread.new do + # Wait until the main thread has entered `$stdin.gets`: + Thread.pass until main.status == 'sleep' + + # Cause an interrupt while handling `$stdin.gets`: + Process.kill :INT, $$ + end + + r.timeout = 1 + assert_equal("INT", r.gets.chomp) + rescue IO::TimeoutError + # Ignore - some platforms don't support interrupting `gets`. + ensure + thread&.join + end + RUBY + end + + def test_fork_close + omit "fork is not supported" unless Process.respond_to?(:fork) + + assert_separately([], <<~'RUBY') + r, w = IO.pipe + + thread = Thread.new do + r.read + end + + Thread.pass until thread.status == "sleep" + + pid = fork do + r.close + end + + w.close + + status = Process.wait2(pid).last + thread.join - IO.select(tempfiles) - }, bug8080 + assert_predicate(status, :success?) + RUBY end end |
