# 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'

class TestIO < Test::Unit::TestCase
  module Feature
    def have_close_on_exec?
      $stdin.close_on_exec?
      true
    rescue NotImplementedError
      false
    end

    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
    rt = Thread.new do
      begin
        rp.call(r)
      rescue Exception
        r.close
        re = $!
      end
    end
    wt = Thread.new do
      begin
        wp.call(w)
      rescue Exception
        w.close
        we = $!
      end
    end
    flunk("timeout") unless wt.join(10) && rt.join(10)
  ensure
    w&.close
    r&.close
    (wt.kill; wt.join) if wt
    (rt.kill; rt.join) if rt
    raise we if we
    raise re if re
  end

  def with_pipe
    r, w = IO.pipe
    begin
      yield r, w
    ensure
      r.close
      w.close
    end
  end

  def with_read_pipe(content)
    pipe(proc do |w|
      w << content
      w.close
    end, proc do |r|
      yield r
    end)
  end

  def mkcdtmpdir
    Dir.mktmpdir {|d|
      Dir.chdir(d) {
        yield
      }
    }
  end

  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(:USR2, "DEFAULT")
    w&.close
    r&.close
  end

  def test_pipe
    r, w = IO.pipe
    assert_instance_of(IO, r)
    assert_instance_of(IO, w)
    [
      Thread.start{
        w.print "abc"
        w.close
      },
      Thread.start{
        assert_equal("abc", r.read)
        r.close
      }
    ].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|
      x = [r,w]
      assert_instance_of(IO, r)
      assert_instance_of(IO, w)
      [
        Thread.start do
          w.print "abc"
          w.close
        end,
        Thread.start do
          assert_equal("abc", r.read)
        end
      ].each{|thr| thr.join}
      assert_not_predicate(r, :closed?)
      assert_predicate(w, :closed?)
      :foooo
    }
    assert_equal(:foooo, ret)
    assert_predicate(x[0], :closed?)
    assert_predicate(x[1], :closed?)
  end

  def test_pipe_block_close
    4.times {|i|
      x = nil
      IO.pipe {|r, w|
        x = [r,w]
        r.close if (i&1) == 0
        w.close if (i&2) == 0
      }
      assert_predicate(x[0], :closed?)
      assert_predicate(x[1], :closed?)
    }
  end

  def test_gets_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
    end, proc do |r|
      assert_equal "aaa\n", r.gets
      assert_equal "bbb\n", r.gets
      assert_nil r.gets
      r.close
    end)
  end

  def test_gets_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)
      assert_nil r.gets("")
      r.close
    end)
  end

  def test_gets_rs_377
    pipe(proc do |w|
      w.print "\377xyz"
      w.close
    end, proc do |r|
      r.binmode
      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
    end, proc do |r|
      assert_equal "a\n\n", r.gets(""), "[ruby-core:03771]"
      assert_equal "b\n\n", r.gets("")
      assert_nil r.gets("")
      r.close
    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"
      w.close
    end, proc do |r|
      assert_equal("0123456789\n0", r.gets(nil, 12))
      assert_raise(TypeError) { r.gets(3,nil) }
    end)
  end

  # This test cause SEGV.
  def test_ungetc
    pipe(proc do |w|
      w.close
    end, proc do |r|
      s = "a" * 1000
      assert_raise(IOError, "[ruby-dev:31650]") { 200.times { r.ungetc s } }
    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
      t.binmode
      t.ungetbyte(0x41)
      assert_equal(-1, t.pos)
      assert_equal(0x41, t.getbyte)
      t.rewind
      assert_equal(0, t.pos)
      t.ungetbyte("qux")
      assert_equal(-3, t.pos)
      assert_equal("quxfoo\n", t.gets)
      assert_equal(4, t.pos)
      t.set_encoding("utf-8")
      t.ungetbyte(0x89)
      t.ungetbyte(0x8e)
      t.ungetbyte("\xe7")
      t.ungetbyte("\xe7\xb4\x85")
      assert_equal(-2, t.pos)
      assert_equal("\u7d05\u7389bar\n", t.gets)
    }
  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"
      w.close
    end, proc do |r|
      r.each_byte {|byte| break if byte == 32 }
      assert_equal("def", r.read, "[ruby-dev:31659]")
    end)
  end

  def test_each_byte_with_seek
    make_tempfile {|t|
      bug5119 = '[ruby-core:38609]'
      i = 0
      open(t.path) do |f|
        f.each_byte {i = f.pos}
      end
      assert_equal(12, i, bug5119)
    }
  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]'
      a = ""
      File.open(t, 'rt') {|f|
        f.each_codepoint {|c| a << c}
      }
      assert_equal("foo\nbar\nbaz\n", a, bug2959)
    }
  end

  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|
      File.open(t, 'rt') {|f|
        assert_raise(IOError) do
          f.each_codepoint {|c| f.close if c == 10}
        end
      }
    }
  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

  def test_rubydev33072
    t = make_tempfile
    path = t.path
    t.close!
    assert_raise(Errno::ENOENT, "[ruby-dev:33072]") do
      File.read(path, nil, nil, **{})
    end
  end

  def with_srccontent(content = "baz")
    src = "src"
    mkcdtmpdir {
      File.open(src, "w") {|f| f << content }
      yield src, content
    }
  end

  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.
      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))

      ret = IO.copy_stream(src, dst, 2)
      assert_equal(2, ret)
      assert_equal(content[0,2], File.read(dst))

      ret = IO.copy_stream(src, dst, 0)
      assert_equal(0, ret)
      assert_equal("", File.read(dst))

      ret = IO.copy_stream(src, dst, nil, 1)
      assert_equal(content.bytesize-1, ret)
      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")
      }
    }
  end

  def test_copy_stream_pipe
    with_srccontent {|src, content|
      pipe(proc do |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) }
      }
    }
  end

  def with_pipecontent
    mkcdtmpdir {
      yield "abc"
    }
  end

  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)
        assert_equal(pipe_content.bytesize, ret)
        assert_equal(pipe_content, File.read(dst))
      }
    }
  end

  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
          w2 << "def"
          ret = IO.copy_stream(r1, w2)
          assert_equal(2, ret)
          w2.close
        end, proc do |r2|
          assert_equal("defbc", r2.read)
        end)
      }

      with_read_pipe(pipe_content) {|r1|
        assert_equal("a", r1.getc)
        pipe(proc do |w2|
          w2.sync = false
          w2 << "def"
          ret = IO.copy_stream(r1, w2, 1)
          assert_equal(1, ret)
          w2.close
        end, proc do |r2|
          assert_equal("defb", r2.read)
        end)
      }

      with_read_pipe(pipe_content) {|r1|
        assert_equal("a", r1.getc)
        pipe(proc do |w2|
          ret = IO.copy_stream(r1, w2)
          assert_equal(2, ret)
          w2.close
        end, proc do |r2|
          assert_equal("bc", r2.read)
        end)
      }

      with_read_pipe(pipe_content) {|r1|
        assert_equal("a", r1.getc)
        pipe(proc do |w2|
          ret = IO.copy_stream(r1, w2, 1)
          assert_equal(1, ret)
          w2.close
        end, proc do |r2|
          assert_equal("b", r2.read)
        end)
      }

      with_read_pipe(pipe_content) {|r1|
        assert_equal("a", r1.getc)
        pipe(proc do |w2|
          ret = IO.copy_stream(r1, w2, 0)
          assert_equal(0, ret)
          w2.close
        end, proc do |r2|
          assert_equal("", r2.read)
        end)
      }

      pipe(proc do |w1|
        w1 << "abc"
        w1 << "def"
        w1.close
      end, proc do |r1|
        assert_equal("a", r1.getc)
        pipe(proc do |w2|
          ret = IO.copy_stream(r1, w2)
          assert_equal(5, ret)
          w2.close
        end, proc do |r2|
          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)
        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

      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
              omit "nonblocking IO for pipe is not implemented"
            end
            s = w2.syswrite("a" * 100000)
            t = Thread.new { sleep 0.1; r2.read }
            ret = IO.copy_stream(r1, w2)
            w2.close
            assert_equal(2, ret)
            assert_equal("a" * s + "bc", t.value)
          }
        }
      }
    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

  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

  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

  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

  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)
          assert_equal(bigcontent.bytesize-10, ret)
          assert_equal(bigcontent[10..-1], File.read("bigdst"))
          assert_equal(0, f.pos)
          ret = IO.copy_stream(f, "bigdst", 40, 30)
          assert_equal(40, ret)
          assert_equal(bigcontent[30, 40], File.read("bigdst"))
          assert_equal(0, f.pos)
        rescue NotImplementedError
          #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) }
      }
    }
  end

  def with_megacontent
    yield "abc" * 1234567
  end

  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
              omit "nonblocking IO for pipe is not implemented"
            end
            t1 = Thread.new { w1 << megacontent; w1.close }
            t2 = Thread.new { r2.read }
            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

  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 }
          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|
        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

  def test_copy_stream_rbuf
    mkcdtmpdir {
      begin
        pipe(proc do |w|
          File.open("foo", "w") {|f| f << "abcd" }
          File.open("foo") {|f|
            f.read(1)
            assert_equal(3, IO.copy_stream(f, w, 10, 1))
          }
          w.close
        end, proc do |r|
          assert_equal("bcd", r.read)
        end)
      rescue NotImplementedError
        omit "pread(2) is not implemtented."
      end
    }
  end

  def with_socketpair
    s1, s2 = UNIXSocket.pair
    begin
      yield s1, s2
    ensure
      s1.close unless s1.closed?
      s2.close unless s2.closed?
    end
  end

  def test_copy_stream_socket1
    with_srccontent("foobar") {|src, content|
      with_socketpair {|s1, s2|
        ret = IO.copy_stream(src, s1)
        assert_equal(content.bytesize, ret)
        s1.close
        assert_equal(content, s2.read)
      }
    }
  end if defined? UNIXSocket

  def test_copy_stream_socket2
    with_bigsrc {|bigsrc, bigcontent|
      with_socketpair {|s1, s2|
        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
    with_bigsrc {|bigsrc, bigcontent|
      with_socketpair {|s1, s2|
        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
    if RUBY_PLATFORM =~ /mingw|mswin/
      omit "pread(2) is not implemented."
    end

    with_bigsrc {|bigsrc, bigcontent|
      File.open(bigsrc) {|f|
        assert_equal(0, f.pos)
        with_socketpair {|s1, s2|
          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

  def test_copy_stream_socket5
    if RUBY_PLATFORM =~ /mingw|mswin/
      omit "pread(2) is not implemented."
    end

    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|
          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

  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 }

      with_socketpair {|s1, s2|
        begin
          s1.nonblock = true
        rescue Errno::EBADF
          omit "nonblocking IO for pipe is not implemented"
        end
        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

  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 }

      with_socketpair {|s1, s2|
        begin
          s1.nonblock = true
        rescue Errno::EBADF
          omit "nonblocking IO for pipe is not implemented"
        end
        trapping_usr2 do |rd|
          nr = 30
          begin
            pid = fork do
              s1.close
              IO.select([s2])
              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, rd.read(4).unpack1('L'))
          ensure
            s1.close
            _, status = Process.waitpid2(pid) if pid
          end
          assert_predicate(status, :success?)
        end
      }
    }
  end

  def test_copy_stream_strio
    src = StringIO.new("abcd")
    dst = StringIO.new
    ret = IO.copy_stream(src, dst)
    assert_equal(4, ret)
    assert_equal("abcd", dst.string)
    assert_equal(4, src.pos)
  end

  def test_copy_stream_strio_len
    src = StringIO.new("abcd")
    dst = StringIO.new
    ret = IO.copy_stream(src, dst, 3)
    assert_equal(3, ret)
    assert_equal("abc", dst.string)
    assert_equal(3, src.pos)
  end

  def test_copy_stream_strio_off
    src = StringIO.new("abcd")
    with_pipe {|r, w|
      assert_raise(ArgumentError) {
        IO.copy_stream(src, w, 3, 1)
      }
    }
  end

  def test_copy_stream_fname_to_strio
    mkcdtmpdir {
      File.open("foo", "w") {|f| f << "abcd" }
      src = "foo"
      dst = StringIO.new
      ret = IO.copy_stream(src, dst, 3)
      assert_equal(3, ret)
      assert_equal("abc", dst.string)
    }
  end

  def test_copy_stream_strio_to_fname
    mkcdtmpdir {
      # StringIO to filename
      src = StringIO.new("abcd")
      ret = IO.copy_stream(src, "fooo", 3)
      assert_equal(3, ret)
      assert_equal("abc", File.read("fooo"))
      assert_equal(3, src.pos)
    }
  end

  def test_copy_stream_io_to_strio
    mkcdtmpdir {
      # IO to StringIO
      File.open("bar", "w") {|f| f << "abcd" }
      File.open("bar") {|src|
        dst = StringIO.new
        ret = IO.copy_stream(src, dst, 3)
        assert_equal(3, ret)
        assert_equal("abc", dst.string)
        assert_equal(3, src.pos)
      }
    }
  end

  def test_copy_stream_strio_to_io
    mkcdtmpdir {
      # StringIO to IO
      src = StringIO.new("abcd")
      ret = File.open("baz", "w") {|dst|
        IO.copy_stream(src, dst, 3)
      }
      assert_equal(3, ret)
      assert_equal("abc", File.read("baz"))
      assert_equal(3, src.pos)
    }
  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
    end

    def readpartial(*args)
      ret = @io.readpartial(*args)
      ret.tr!('a-zA-Z', 'n-za-mN-ZA-M')
      ret
    end

    def write(str)
      @io.write(str.tr('a-zA-Z', 'n-za-mN-ZA-M'))
    end

    def to_io
      @io
    end
  end

  def test_copy_stream_io_to_rot13
    mkcdtmpdir {
      File.open("bar", "w") {|f| f << "vex" }
      File.open("bar") {|src|
        File.open("baz", "w") {|dst0|
          dst = Rot13IO.new(dst0)
          ret = IO.copy_stream(src, dst, 3)
          assert_equal(3, ret)
        }
        assert_equal("irk", File.read("baz"))
      }
    }
  end

  def test_copy_stream_rot13_to_io
    mkcdtmpdir {
      File.open("bar", "w") {|f| f << "flap" }
      File.open("bar") {|src0|
        src = Rot13IO.new(src0)
        File.open("baz", "w") {|dst|
          ret = IO.copy_stream(src, dst, 4)
          assert_equal(4, ret)
        }
      }
      assert_equal("sync", File.read("baz"))
    }
  end

  def test_copy_stream_rot13_to_rot13
    mkcdtmpdir {
      File.open("bar", "w") {|f| f << "bin" }
      File.open("bar") {|src0|
        src = Rot13IO.new(src0)
        File.open("baz", "w") {|dst0|
          dst = Rot13IO.new(dst0)
          ret = IO.copy_stream(src, dst, 3)
          assert_equal(3, ret)
        }
      }
      assert_equal("bin", File.read("baz"))
    }
  end

  def test_copy_stream_strio_flush
    with_pipe {|r, w|
      w.sync = false
      w.write "zz"
      src = StringIO.new("abcd")
      IO.copy_stream(src, w)
      t1 = Thread.new {
        w.close
      }
      t2 = Thread.new { r.read }
      _, result = assert_join_threads([t1, t2])
      assert_equal("zzabcd", result)
    }
  end

  def test_copy_stream_strio_rbuf
    pipe(proc do |w|
      w << "abcd"
      w.close
    end, proc do |r|
      assert_equal("a", r.read(1))
      sio = StringIO.new
      IO.copy_stream(r, sio)
      assert_equal("bcd", sio.string)
    end)
  end

  def test_copy_stream_src_wbuf
    mkcdtmpdir {
      pipe(proc do |w|
        File.open("foe", "w+") {|f|
          f.write "abcd\n"
          f.rewind
          f.write "xy"
          IO.copy_stream(f, w)
        }
        assert_equal("xycd\n", File.read("foe"))
        w.close
      end, proc do |r|
        assert_equal("cd\n", r.read)
        r.close
      end)
    }
  end

  class Bug5237
    attr_reader :count
    def initialize
      @count = 0
    end

    def read(bytes, buffer)
      @count += 1
      buffer.replace "this is a test"
      nil
    end
  end

  def test_copy_stream_broken_src_read_eof
    src = Bug5237.new
    dst = StringIO.new
    assert_equal 0, src.count
    th = Thread.new { IO.copy_stream(src, dst) }
    flunk("timeout") unless th.join(10)
    assert_equal 1, src.count
  end

  def test_copy_stream_dst_rbuf
    mkcdtmpdir {
      pipe(proc do |w|
        w << "xyz"
        w.close
      end, proc do |r|
        File.open("fom", "w+b") {|f|
          f.write "abcd\n"
          f.rewind
          assert_equal("abc", f.read(3))
          f.ungetc "c"
          IO.copy_stream(r, f)
        }
        assert_equal("abxyz", File.read("fom"))
      end)
    }
  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
    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
    assert_equal(STDOUT, IO.try_convert(STDOUT))
    assert_equal(nil, IO.try_convert("STDOUT"))
  end

  def test_ungetc2
    f = false
    pipe(proc do |w|
      Thread.pass until f
      w.write("1" * 10000)
      w.close
    end, proc do |r|
      r.ungetc("0" * 10000)
      f = true
      assert_equal("0" * 10000 + "1" * 10000, r.read)
    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
        r.write "foobarbaz"
      end
    end
  end

  def test_dup
    ruby do |f|
      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
    opts = {}
    opts[:rlimit_nofile] = 1024 if defined?(Process::RLIMIT_NOFILE)
    assert_separately([], <<-'End', **opts)
      a = []
      assert_raise(Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM) do
        loop {a << IO.pipe}
      end
      assert_raise(Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM) do
        loop {a << [a[-1][0].dup, a[-1][1].dup]}
      end
    End
  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)
      r.freeze
      assert_match(/^#<IO:fd \d+>$/, r.inspect)
    end
  end

  def test_readpartial
    pipe(proc do |w|
      w.write "foobarbaz"
      w.close
    end, proc do |r|
      assert_raise(ArgumentError) { r.readpartial(-1) }
      assert_equal("fooba", r.readpartial(5))
      r.readpartial(5, s = "")
      assert_equal("rbaz", s)
    end)
  end

  def test_readpartial_lock
    with_pipe do |r, w|
      s = ""
      t = Thread.new { r.readpartial(5, s) }
      Thread.pass until t.stop?
      assert_raise(RuntimeError) { s.clear }
      w.write "foobarbaz"
      w.close
      assert_equal("fooba", t.value)
    end
  end

  def test_readpartial_pos
    mkcdtmpdir {
      open("foo", "w") {|f| f << "abc" }
      open("foo") {|f|
        f.seek(0)
        assert_equal("ab", f.readpartial(2))
        assert_equal(2, f.pos)
      }
    }
  end

  def test_readpartial_with_not_empty_buffer
    pipe(proc do |w|
      w.write "foob"
      w.close
    end, proc do |r|
      r.readpartial(5, s = "01234567")
      assert_equal("foob", s)
    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 = ""
      t = Thread.new { r.readpartial(5, s) }
      Thread.pass until t.stop?
      t.kill
      t.value
      assert_equal("", s)
    end
  end if /cygwin/ !~ RUBY_PLATFORM

  def test_read
    pipe(proc do |w|
      w.write "foobarbaz"
      w.close
    end, proc do |r|
      assert_raise(ArgumentError) { r.read(-1) }
      assert_equal("fooba", r.read(5))
      r.read(nil, s = "")
      assert_equal("rbaz", s)
    end)
  end

  def test_read_lock
    with_pipe do |r, w|
      s = ""
      t = Thread.new { r.read(5, s) }
      Thread.pass until t.stop?
      assert_raise(RuntimeError) { s.clear }
      w.write "foobarbaz"
      w.close
      assert_equal("fooba", t.value)
    end
  end

  def test_read_with_not_empty_buffer
    pipe(proc do |w|
      w.write "foob"
      w.close
    end, proc do |r|
      r.read(nil, s = "01234567")
      assert_equal("foob", s)
    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 = ""
      t = Thread.new { r.read(5, s) }
      Thread.pass until t.stop?
      t.kill
      t.value
      assert_equal("", s)
    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
    pipe(proc do |w|
      w.write_nonblock(1)
      w.close
    end, proc do |r|
      assert_equal("1", r.read)
    end)
  end

  def test_read_nonblock_with_not_empty_buffer
    with_pipe {|r, w|
      w.write "foob"
      w.close
      r.read_nonblock(5, s = "01234567")
      assert_equal("foob", s)
    }
  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
    with_pipe {|r, w|
      begin
        r.read_nonblock 4096
      rescue Errno::EWOULDBLOCK
        assert_kind_of(IO::WaitReadable, $!)
      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
    with_pipe {|r, w|
      begin
        loop {
          w.write_nonblock "a"*100000
        }
      rescue Errno::EWOULDBLOCK
        assert_kind_of(IO::WaitWritable, $!)
      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|
      w.write "foobarbaz"
      w.close
    end, proc do |r|
      assert_equal("", r.gets(0))
      assert_equal("foobarbaz", r.gets(9))
    end)
  end

  def test_close_read
    ruby do |f|
      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

  def test_close_read_pipe
    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_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
    with_pipe do |r, w|
      assert_raise(IOError) do
        w.close_read
      end
    end
  end

  def test_close_write
    ruby do |f|
      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_non_readable
    with_pipe do |r, w|
      assert_raise(IOError) do
        r.close_write
      end
    end
  end

  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
    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
    t.puts "foo"
    t.puts "bar"
    t.puts "baz"
    t.close
    if block_given?
      begin
        yield t
      ensure
        t.close(true)
      end
    else
      t
    end
  end

  def test_set_lineno
    make_tempfile {|t|
      assert_separately(["-", t.path], <<-SRC)
        open(ARGV[0]) do |f|
          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
    }
  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

    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_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"
      w.puts "baz"
      w.close
    end, proc do |r|
      r.readline; assert_equal(1, $.)
      r.readline; assert_equal(2, $.)
      r.lineno = 1000; assert_equal(2, $.)
      r.readline; assert_equal(1001, $.)
      assert_raise(EOFError) { r.readline }
    end)
  end

  def test_each_char
    pipe(proc do |w|
      w.puts "foo"
      w.puts "bar"
      w.puts "baz"
      w.close
    end, proc do |r|
      a = []
      r.each_char {|c| a << c }
      assert_equal(%w(f o o) + ["\n"] + %w(b a r) + ["\n"] + %w(b a z) + ["\n"], a)
    end)
  end

  def test_each_line
    pipe(proc do |w|
      w.puts "foo"
      w.puts "bar"
      w.puts "baz"
      w.close
    end, proc do |r|
      e = nil
      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)

    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_each_byte2
    pipe(proc do |w|
      w.binmode
      w.puts "foo"
      w.puts "bar"
      w.puts "baz"
      w.close
    end, proc do |r|
      e = nil
      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)
  end

  def test_each_char2
    pipe(proc do |w|
      w.puts "foo"
      w.puts "bar"
      w.puts "baz"
      w.close
    end, proc do |r|
      e = nil
      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)
  end

  def test_readbyte
    pipe(proc do |w|
      w.binmode
      w.puts "foo"
      w.puts "bar"
      w.puts "baz"
      w.close
    end, proc do |r|
      r.binmode
      (%w(f o o) + ["\n"] + %w(b a r) + ["\n"] + %w(b a z) + ["\n"]).each do |c|
        assert_equal(c.ord, r.readbyte)
      end
      assert_raise(EOFError) { r.readbyte }
    end)
  end

  def test_readchar
    pipe(proc do |w|
      w.puts "foo"
      w.puts "bar"
      w.puts "baz"
      w.close
    end, proc do |r|
      (%w(f o o) + ["\n"] + %w(b a r) + ["\n"] + %w(b a z) + ["\n"]).each do |c|
        assert_equal(c, r.readchar)
      end
      assert_raise(EOFError) { r.readchar }
    end)
  end

  def test_close_on_exec
    ruby do |f|
      assert_equal(true, f.close_on_exec?)
      f.close_on_exec = false
      assert_equal(false, f.close_on_exec?)
      f.close_on_exec = true
      assert_equal(true, f.close_on_exec?)
      f.close_on_exec = false
      assert_equal(false, f.close_on_exec?)
    end

    with_pipe do |r, w|
      assert_equal(true, r.close_on_exec?)
      r.close_on_exec = false
      assert_equal(false, r.close_on_exec?)
      r.close_on_exec = true
      assert_equal(true, r.close_on_exec?)
      r.close_on_exec = false
      assert_equal(false, r.close_on_exec?)

      assert_equal(true, w.close_on_exec?)
      w.close_on_exec = false
      assert_equal(false, w.close_on_exec?)
      w.close_on_exec = true
      assert_equal(true, w.close_on_exec?)
      w.close_on_exec = false
      assert_equal(false, w.close_on_exec?)
    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)
      end
      open(t.path, IO::RDWR|IO::CREAT|IO::TRUNC, 0600) do |f|
        f.sync = true
        f.read
        f.write "Hello"
        assert_equal(5, f.pos)
      end
    }
  end

  def test_pos_with_getc
    _bug6179 = '[ruby-core:43497]'
    make_tempfile {|t|
      ["", "t", "b"].each do |mode|
        open(t.path, "w#{mode}") do |f|
          f.write "0123456789\n"
        end

        open(t.path, "r#{mode}") do |f|
          assert_equal 0, f.pos, "mode=r#{mode}"
          assert_equal '0', f.getc, "mode=r#{mode}"
          assert_equal 1, f.pos, "mode=r#{mode}"
          assert_equal '1', f.getc, "mode=r#{mode}"
          assert_equal 2, f.pos, "mode=r#{mode}"
          assert_equal '2', f.getc, "mode=r#{mode}"
          assert_equal 3, f.pos, "mode=r#{mode}"
          assert_equal '3', f.getc, "mode=r#{mode}"
          assert_equal 4, f.pos, "mode=r#{mode}"
          assert_equal '4', f.getc, "mode=r#{mode}"
        end
      end
    }
  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|
      open(t.path) { |f|
        f.seek(9)
        assert_equal("az\n", f.read)
      }

      open(t.path) { |f|
        f.seek(9, IO::SEEK_SET)
        assert_equal("az\n", f.read)
      }

      open(t.path) { |f|
        f.seek(-4, IO::SEEK_END)
        assert_equal("baz\n", f.read)
      }

      open(t.path) { |f|
        assert_equal("foo\n", f.gets)
        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

  def test_seek_symwhence
    make_tempfile {|t|
      open(t.path) { |f|
        f.seek(9, :SET)
        assert_equal("az\n", f.read)
      }

      open(t.path) { |f|
        f.seek(-4, :END)
        assert_equal("baz\n", f.read)
      }

      open(t.path) { |f|
        assert_equal("foo\n", f.gets)
        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

  def test_sysseek
    make_tempfile {|t|
      open(t.path) do |f|
        f.sysseek(-4, IO::SEEK_END)
        assert_equal("baz\n", f.read)
      end

      open(t.path) do |f|
        a = [f.getc, f.getc, f.getc]
        a.reverse_each {|c| f.ungetc c }
        assert_raise(IOError) { f.sysseek(1) }
      end
    }
  end

  def test_syswrite
    make_tempfile {|t|
      open(t.path, "w") do |f|
        o = Object.new
        def o.to_s; "FOO\n"; end
        f.syswrite(o)
      end
      assert_equal("FOO\n", File.read(t.path))
    }
  end

  def test_sysread
    make_tempfile {|t|
      open(t.path) do |f|
        a = [f.getc, f.getc, f.getc]
        a.reverse_each {|c| f.ungetc c }
        assert_raise(IOError) { f.sysread(1) }
      end
    }
  end

  def test_sysread_with_not_empty_buffer
    pipe(proc do |w|
      w.write "foob"
      w.close
    end, proc do |r|
      r.sysread( 5, s = "01234567" )
      assert_equal( "foob", s )
    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
        open(t.path, "z") { }
      end

      assert_raise(ArgumentError) do
        open(t.path, "rr") { }
      end

      assert_raise(ArgumentError) do
        open(t.path, "rbt") { }
      end
    }
  end

  def test_sysopen
    make_tempfile {|t|
      fd = IO.sysopen(t.path)
      assert_kind_of(Integer, fd)
      f = IO.for_fd(fd)
      assert_equal("foo\nbar\nbaz\n", f.read)
      f.close

      fd = IO.sysopen(t.path, "w", 0666)
      assert_kind_of(Integer, fd)
      if defined?(Fcntl::F_GETFL)
        f = IO.for_fd(fd)
      else
        f = IO.for_fd(fd, 0666)
      end
      f.write("FOO\n")
      f.close

      fd = IO.sysopen(t.path, "r")
      assert_kind_of(Integer, fd)
      f = IO.for_fd(fd)
      assert_equal("FOO\n", f.read)
      f.close
    }
  end

  def try_fdopen(fd, autoclose = true, level = 50)
    if level > 0
      begin
        1.times {return try_fdopen(fd, autoclose, level - 1)}
      ensure
        GC.start
      end
    else
      WeakRef.new(IO.for_fd(fd, autoclose: autoclose))
    end
  end

  def test_autoclose
    feature2250 = '[ruby-core:26222]'
    pre = 'ft2250'

    Dir.mktmpdir {|d|
      t = open("#{d}/#{pre}", "w")
      f = IO.for_fd(t.fileno)
      assert_equal(true, f.autoclose?)
      f.autoclose = false
      assert_equal(false, f.autoclose?)
      f.close
      assert_nothing_raised(Errno::EBADF, feature2250) {t.close}

      t = open("#{d}/#{pre}", "w")
      f = IO.for_fd(t.fileno, autoclose: false)
      assert_equal(false, f.autoclose?)
      f.autoclose = true
      assert_equal(true, f.autoclose?)
      f.close
      assert_raise(Errno::EBADF, feature2250) {t.close}
    }
  end

  def test_autoclose_true_closed_by_finalizer
    feature2250 = '[ruby-core:26222]'
    pre = 'ft2250'
    t = Tempfile.new(pre)
    w = try_fdopen(t.fileno)
    begin
      w.close
      begin
        t.close
      rescue Errno::EBADF
      end
      omit "expect IO object was GC'ed but not recycled yet"
    rescue WeakRef::RefError
      assert_raise(Errno::EBADF, feature2250) {t.close}
    end
  ensure
    t&.close!
  end

  def test_autoclose_false_closed_by_finalizer
    feature2250 = '[ruby-core:26222]'
    pre = 'ft2250'
    t = Tempfile.new(pre)
    w = try_fdopen(t.fileno, false)
    begin
      w.close
      t.close
      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.close!
  end

  def test_open_redirect
    o = Object.new
    def o.to_open; self; end
    assert_equal(o, open(o))
    o2 = nil
    open(o) do |f|
      o2 = f
    end
    assert_equal(o, o2)
  end

  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|
      open(__FILE__) do |f|
        f.gets
        assert_nothing_raised {
          f.reopen(t.path)
          assert_equal("foo\n", f.gets)
        }
      end

      open(__FILE__) do |f|
        f.gets
        f2 = open(t.path)
        begin
          f2.gets
          assert_nothing_raised {
            f.reopen(f2)
            assert_equal("bar\n", f.gets, '[ruby-core:24240]')
          }
        ensure
          f2.close
        end
      end

      open(__FILE__) do |f|
        f2 = open(t.path)
        begin
          f.reopen(f2)
          assert_equal("foo\n", f.gets)
          assert_equal("bar\n", f.gets)
          f.reopen(f2)
          assert_equal("baz\n", f.gets, '[ruby-dev:39479]')
        ensure
          f2.close
        end
      end
    }
  end

  def test_reopen_inherit
    mkcdtmpdir {
      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
      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|
      open(__FILE__) do |f|
        assert_nothing_raised {
          f.reopen(t.path, "r")
          assert_equal("foo\n", f.gets)
        }
      end

      open(__FILE__) do |f|
        assert_nothing_raised(feature7067) {
          f.reopen(t.path, File::RDONLY)
          assert_equal("foo\n", f.gets)
        }
      end
    }
  end

  def test_reopen_opt
    feature7103 = '[ruby-core:47806]'
    make_tempfile {|t|
      open(__FILE__) do |f|
        assert_nothing_raised(feature7103) {
          f.reopen(t.path, "r", binmode: true)
        }
        assert_equal("foo\n", f.gets)
      end

      open(__FILE__) do |f|
        assert_nothing_raised(feature7103) {
          f.reopen(t.path, autoclose: false)
        }
        assert_equal("foo\n", f.gets)
      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"}
    if block_given?
      yield t
    else
      t
    end
  ensure
    t&.close(true) if block_given?
  end

  def test_reopen_encoding
    make_tempfile_for_encoding {|t|
      open(__FILE__) {|f|
        f.reopen(t.path, "r:utf-8")
        s = f.gets
        assert_equal(Encoding::UTF_8, s.encoding)
        assert_equal("\u7d05\u7389bar\n", s)
      }

      open(__FILE__) {|f|
        f.reopen(t.path, "r:UTF-8:EUC-JP")
        s = f.gets
        assert_equal(Encoding::EUC_JP, s.encoding)
        assert_equal("\xB9\xC8\xB6\xCCbar\n".force_encoding(Encoding::EUC_JP), s)
      }
    }
  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|
      open(__FILE__) {|f|
        assert_nothing_raised(feature7103) {f.reopen(t.path, encoding: "ASCII-8BIT")}
        s = f.gets
        assert_equal(Encoding::ASCII_8BIT, s.encoding)
        assert_equal("\xe7\xb4\x85\xe7\x8e\x89bar\n", s)
      }

      open(__FILE__) {|f|
        assert_nothing_raised(feature7103) {f.reopen(t.path, encoding: "UTF-8:EUC-JP")}
        s = f.gets
        assert_equal(Encoding::EUC_JP, s.encoding)
        assert_equal("\xB9\xC8\xB6\xCCbar\n".force_encoding(Encoding::EUC_JP), s)
      }
    }
  end

  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 }
      assert_equal(["foo\n", "bar\n", "baz\n"], a)

      a = []
      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 }
      assert_equal(["foo\n", "bar\n", "baz\n"], a)

      a = []
      IO.foreach(t.path, "b") {|x| a << x }
      assert_equal(["foo\nb", "ar\nb", "az\n"], a)

      a = []
      IO.foreach(t.path, 3) {|x| a << x }
      assert_equal(["foo", "\n", "bar", "\n", "baz", "\n"], a)

      a = []
      IO.foreach(t.path, "b", 3) {|x| a << x }
      assert_equal(["foo", "\nb", "ar\n", "b", "az\n"], a)

      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]'
      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

  def test_s_readlines
    make_tempfile {|t|
      assert_equal(["foo\n", "bar\n", "baz\n"], IO.readlines(t.path))
      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

  def test_printf
    pipe(proc do |w|
      printf(w, "foo %s baz\n", "bar")
      w.close_write
    end, proc do |r|
      assert_equal("foo bar baz\n", r.read)
    end)
  end

  def test_print
    make_tempfile {|t|
      assert_in_out_err(["-", t.path],
                        "print while $<.gets",
                        %w(foo bar baz), [])
    }
  end

  def test_print_separators
    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')
      EnvUtil.suppress_warning {w.print('a','b','c')}
      w.close
    end, proc do |r|
      assert_equal("a\n", r.gets)
      assert_equal("a:b:c\n", r.gets)
      assert_nil r.gets
      r.close
    end)
  ensure
    $, = nil
    $\ = nil
  end

  def test_putc
    pipe(proc do |w|
      w.putc "A"
      w.putc "BC"
      w.putc 68
      w.close_write
    end, proc do |r|
      assert_equal("ABD", r.read)
    end)

    assert_in_out_err([], "putc 65", %w(A), [])
  end

  def test_puts_recursive_array
    a = ["foo"]
    a << a
    pipe(proc do |w|
      w.puts a
      w.close
    end, proc do |r|
      assert_equal("foo\n[...]\n", r.read)
    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)
      w.close
    end, proc do |r|
      assert_equal("foo", r.read)
    end)

    assert_in_out_err([], "'foo'.display", %w(foo), [])
  end

  def test_set_stdout
    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|
        assert_raise(Errno::EINVAL, "#{mode} [ruby-dev:38571]") {IO.new(fd, mode)}
      end
      f = IO.new(fd, "w")
      f.write("FOO\n")
      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

  def test_reinitialize
    make_tempfile {|t|
      f = open(t.path)
      begin
        assert_raise(RuntimeError) do
          f.instance_eval { initialize }
        end
      ensure
        f.close
      end
    }
  end

  def test_new_with_block
    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
    assert_in_out_err(["-e", <<-SRC], "foo\nbar\nbaz\n", %w(foo bar baz end), [])
      puts readline
      puts readline
      puts readline
      begin
        puts readline
      rescue EOFError
        puts "end"
      end
    SRC
  end

  def test_readlines
    assert_in_out_err(["-e", "p readlines"], "foo\nbar\nbaz\n",
                      ["[\"foo\\n\", \"bar\\n\", \"baz\\n\"]"], [])
  end

  def test_s_read
    make_tempfile {|t|
      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

  def test_uninitialized
    assert_raise(IOError) { IO.allocate.print "" }
  end

  def test_nofollow
    # O_NOFOLLOW is not standard.
    mkcdtmpdir {
      open("file", "w") {|f| f << "content" }
      begin
        File.symlink("file", "slnk")
      rescue NotImplementedError
        return
      end
      assert_raise(Errno::EMLINK, Errno::ELOOP) {
        open("slnk", File::RDONLY|File::NOFOLLOW) {}
      }
      assert_raise(Errno::EMLINK, Errno::ELOOP) {
        File.foreach("slnk", :open_args=>[File::RDONLY|File::NOFOLLOW]) {}
      }
    }
  end if /freebsd|linux/ =~ RUBY_PLATFORM and defined? File::NOFOLLOW

  def test_binmode_after_closed
    make_tempfile {|t|
      assert_raise(IOError) {t.binmode}
    }
  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 = "#{<<~"begin;"}\n#{<<~'end;'}"
    begin;
      t = Thread.new { sleep 3 }
      Thread.new {sleep 1; t.kill; p 'hi!'}
      t.join
    end;
    10.times.map do
      Thread.start do
        assert_in_out_err([], src, timeout: 20) {|stdout, stderr|
          assert_no_match(/hi.*hi/, stderr.join, bug3585)
        }
      end
    end.each {|th| th.join}
  end

  def test_flush_in_finalizer1
    bug3910 = '[ruby-dev:42341]'
    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
    }
  ensure
    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
    bug3910 = '[ruby-dev:42341]'
    Tempfile.create("bug3910") {|t|
      path = t.path
      t.close
      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
    }
  end

  def test_readlines_limit_0
    bug4024 = '[ruby-dev:42538]'
    make_tempfile {|t|
      open(t.path, "r") do |io|
        assert_raise(ArgumentError, bug4024) do
          io.readlines(0)
        end
      end
    }
  end

  def test_each_line_limit_0
    bug4024 = '[ruby-dev:42538]'
    make_tempfile {|t|
      open(t.path, "r") do |io|
        assert_raise(ArgumentError, bug4024) do
          io.each_line(0).next
        end
      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|
            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
            assert_raise(TypeError, "wrong type for first argument") do
              t.advise(adv.to_s, offset, len)
            end
            assert_raise(TypeError, "wrong type for last argument") do
              t.advise(adv, offset, Array(len))
            end
            assert_raise(RangeError, "last argument too big") do
              t.advise(adv, offset, 9999e99)
            end
          end
          assert_raise(IOError, "closed file") do
            make_tempfile {|tf2|
              tf2.advise(adv.to_sym, offset, len)
            }
          end
        end
      end
    }
  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|
        [[0,0], [0, 20], [400, 2]].each do |offset, len|
          open(tf.path) do |t|
            assert_raise_with_message(NotImplementedError, /#{Regexp.quote(adv.inspect)}/, feature4204) { t.advise(adv, offset, len) }
          end
        end
      end
    }
  end

  def test_fcntl_lock_linux
    pad = 0
    Tempfile.create(self.class.name) do |f|
      r, w = IO.pipe
      pid = fork do
        r.close
        lock = [Fcntl::F_WRLCK, IO::SEEK_SET, pad, 12, 34, 0].pack("s!s!i!L!L!i!")
        f.fcntl Fcntl::F_SETLKW, lock
        w.syswrite "."
        sleep
      end
      w.close
      assert_equal ".", r.read(1)
      r.close
      pad = 0
      getlock = [Fcntl::F_WRLCK, 0, pad, 0, 0, 0].pack("s!s!i!L!L!i!")
      f.fcntl Fcntl::F_GETLK, getlock

      ptype, whence, pad, start, len, lockpid = getlock.unpack("s!s!i!L!L!i!")

      assert_equal(ptype, Fcntl::F_WRLCK)
      assert_equal(whence, IO::SEEK_SET)
      assert_equal(start, 12)
      assert_equal(len, 34)
      assert_equal(pid, lockpid)

      Process.kill :TERM, pid
      Process.waitpid2(pid)
    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
    start = 12
    len = 34
    sysid = 0
    Tempfile.create(self.class.name) do |f|
      r, w = IO.pipe
      pid = fork do
        r.close
        lock = [start, len, 0, Fcntl::F_WRLCK, IO::SEEK_SET, sysid].pack("qqis!s!i!")
        f.fcntl Fcntl::F_SETLKW, lock
        w.syswrite "."
        sleep
      end
      w.close
      assert_equal ".", r.read(1)
      r.close

      getlock = [0, 0, 0, Fcntl::F_WRLCK, 0, 0].pack("qqis!s!i!")
      f.fcntl Fcntl::F_GETLK, getlock

      start, len, lockpid, ptype, whence, sysid = getlock.unpack("qqis!s!i!")

      assert_equal(ptype, Fcntl::F_WRLCK)
      assert_equal(whence, IO::SEEK_SET)
      assert_equal(start, 12)
      assert_equal(len, 34)
      assert_equal(pid, lockpid)

      Process.kill :TERM, pid
      Process.waitpid2(pid)
    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|
      fd = f.fcntl(Fcntl::F_DUPFD, 63)
      begin
        assert_operator(fd, :>=, 63)
      ensure
        IO.for_fd(fd).close
      end
    end
  end

  def test_cross_thread_close_fd
    with_pipe do |r,w|
      read_thread = Thread.new do
        begin
          r.read(1)
        rescue => e
          e
        end
      end

      sleep(0.1) until read_thread.stop?
      r.close
      read_thread.join
      assert_kind_of(IOError, read_thread.value)
    end
  end

  def test_cross_thread_close_stdio
    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 IOError => e
            e
          end
        end
        sleep(0.1) until read_thread.stop?
        $stdin.close
        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
    end
    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
    feature4742 = "[ruby-core:36338]"
    bug6055 = '[ruby-dev:45268]'

    mkcdtmpdir do
      assert_not_nil(f = File.open('symbolic', 'w'))
      f.close
      assert_not_nil(f = File.open('numeric',  File::WRONLY|File::TRUNC|File::CREAT))
      f.close
      assert_not_nil(f = File.open('hash-symbolic', :mode => 'w'))
      f.close
      assert_not_nil(f = File.open('hash-numeric', :mode => File::WRONLY|File::TRUNC|File::CREAT), feature4742)
      f.close
      assert_nothing_raised(bug6055) {f = File.open('hash-symbolic', binmode: true)}
      f.close
    end
  end

  def test_s_write
    mkcdtmpdir do
      path = "test_s_write"
      File.write(path, "foo\nbar\nbaz")
      assert_equal("foo\nbar\nbaz", File.read(path))
      File.write(path, "FOO", 0)
      assert_equal("FOO\nbar\nbaz", File.read(path))
      File.write(path, "BAR")
      assert_equal("BAR", File.read(path))
      File.write(path, "\u{3042}", mode: "w", encoding: "EUC-JP")
      assert_equal("\u{3042}".encode("EUC-JP"), File.read(path, encoding: "EUC-JP"))
      File.delete path
      assert_equal(6, File.write(path, 'string', 2))
      File.delete path
      assert_raise(Errno::EINVAL) { File.write('nonexisting','string', -2) }
      assert_equal(6, File.write(path, 'string'))
      assert_equal(3, File.write(path, 'sub', 1))
      assert_equal("ssubng", File.read(path))
      File.delete path
      assert_equal(3, File.write(path, "foo", encoding: "UTF-8"))
      File.delete path
      assert_equal(3, File.write(path, "foo", 0, encoding: "UTF-8"))
      assert_equal("foo", File.read(path))
      assert_equal(1, File.write(path, "f", 1, encoding: "UTF-8"))
      assert_equal("ffo", File.read(path))
      File.delete path
      assert_equal(1, File.write(path, "f", 1, encoding: "UTF-8"))
      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"
      File.binwrite(path, "foo\nbar\nbaz")
      assert_equal("foo\nbar\nbaz", File.read(path))
      File.binwrite(path, "FOO", 0)
      assert_equal("FOO\nbar\nbaz", File.read(path))
      File.binwrite(path, "BAR")
      assert_equal("BAR", File.read(path))
      File.binwrite(path, "\u{3042}")
      assert_equal("\u{3042}".force_encoding("ASCII-8BIT"), File.binread(path))
      File.delete path
      assert_equal(6, File.binwrite(path, 'string', 2))
      File.delete path
      assert_equal(6, File.binwrite(path, 'string'))
      assert_equal(3, File.binwrite(path, 'sub', 1))
      assert_equal("ssubng", File.binread(path))
      assert_equal(6, File.size(path))
      assert_raise(Errno::EINVAL) { File.binwrite('nonexisting', 'string', -2) }
      assert_nothing_raised(TypeError) { File.binwrite(path, "string", mode: "w", encoding: "EUC-JP") }
    end
  end

  def test_race_between_read
    Tempfile.create("test") {|file|
      begin
        path = file.path
        file.close
        write_file = File.open(path, "wt")
        read_file = File.open(path, "rt")

        threads = []
        10.times do |i|
          threads << Thread.new {write_file.print(i)}
          threads << Thread.new {read_file.read}
        end
        assert_join_threads(threads)
        assert(true, "[ruby-core:37197]")
      ensure
        read_file.close
        write_file.close
      end
    }
  end

  def test_warn
    assert_warning "warning\n" do
      warn "warning"
    end

    assert_warning '' do
      warn
    end

    assert_warning "[Feature #5029]\n[ruby-core:38070]\n" do
      warn "[Feature #5029]", "[ruby-core:38070]"
    end
  end

  def test_cloexec
    return unless defined? Fcntl::FD_CLOEXEC
    open(__FILE__) {|f|
      assert_predicate(f, :close_on_exec?)
      g = f.dup
      begin
        assert_predicate(g, :close_on_exec?)
        f.reopen(g)
        assert_predicate(f, :close_on_exec?)
      ensure
        g.close
      end
      g = IO.new(f.fcntl(Fcntl::F_DUPFD))
      begin
        assert_predicate(g, :close_on_exec?)
      ensure
        g.close
      end
    }
    IO.pipe {|r,w|
      assert_predicate(r, :close_on_exec?)
      assert_predicate(w, :close_on_exec?)
    }
  end

  def test_ioctl_linux
    # Alpha, mips, sparc and ppc have an another ioctl request number scheme.
    # So, hardcoded 0x80045200 may fail.
    assert_nothing_raised do
      File.open('/dev/urandom'){|f1|
        entropy_count = ""
        # RNDGETENTCNT(0x80045200) mean "get entropy count".
        f1.ioctl(0x80045200, entropy_count)
      }
    end

    buf = ''
    assert_nothing_raised do
      fionread = 0x541B
      File.open(__FILE__){|f1|
        f1.ioctl(fionread, buf)
      }
    end
    assert_equal(File.size(__FILE__), buf.unpack('i!')[0])
  end if /^(?:i.?86|x86_64)-linux/ =~ RUBY_PLATFORM

  def test_ioctl_linux2
    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)
      }
    ensure
      f&.close
    end
  end if /^(?:i.?86|x86_64)-linux/ =~ RUBY_PLATFORM

  def test_setpos
    mkcdtmpdir {
      File.open("tmp.txt", "wb") {|f|
        f.puts "a"
        f.puts "bc"
        f.puts "def"
      }
      pos1 = pos2 = pos3 = nil
      File.open("tmp.txt", "rb") {|f|
        assert_equal("a\n", f.gets)
        pos1 = f.pos
        assert_equal("bc\n", f.gets)
        pos2 = f.pos
        assert_equal("def\n", f.gets)
        pos3 = f.pos
        assert_equal(nil, f.gets)
      }
      File.open("tmp.txt", "rb") {|f|
        f.pos = pos1
        assert_equal("bc\n", f.gets)
        assert_equal("def\n", f.gets)
        assert_equal(nil, f.gets)
      }
      File.open("tmp.txt", "rb") {|f|
        f.pos = pos2
        assert_equal("def\n", f.gets)
        assert_equal(nil, f.gets)
      }
      File.open("tmp.txt", "rb") {|f|
        f.pos = pos3
        assert_equal(nil, f.gets)
      }
      File.open("tmp.txt", "rb") {|f|
        f.pos = File.size("tmp.txt")
        s = "not empty string        "
        assert_equal("", f.read(0,s))
      }
    }
  end

  def test_std_fileno
    assert_equal(0, STDIN.fileno)
    assert_equal(1, STDOUT.fileno)
    assert_equal(2, STDERR.fileno)
    assert_equal(0, $stdin.fileno)
    assert_equal(1, $stdout.fileno)
    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?

      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)
      th.join
    end
    assert_equal(data, buf, bug6099)
  end

  def test_readpartial_locktmp
    bug6099 = '[ruby-dev:45297]'
    buf = " " * 100
    data = "a" * 100
    th = nil
    with_pipe do |r,w|
      r.nonblock = true
      th = Thread.new {r.readpartial(100, buf)}

      Thread.pass until th.stop?

      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)
      th.join
    end
    assert_equal(data, buf, bug6099)
  end

  def test_advise_pipe
    # we don't know if other platforms have a real posix_fadvise()
    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) 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 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 = []
    assert_nothing_raised(RuntimeError, bug6764) do
      buf = ''
      File.open(t.path, "r") do |r|
        while yield(r, size, buf)
          w << buf.dup
        end
      end
    end
    assert_equal(data, w.join(""), bug9847)
  ensure
    t.close!
  end

  def test_read_buffer_not_raise_shared_string_error
    assert_buffer_not_raise_shared_string_error do |r, size, buf|
      r.read(size, buf)
    end
  end

  def test_sysread_buffer_not_raise_shared_string_error
    assert_buffer_not_raise_shared_string_error do |r, size, buf|
      begin
        r.sysread(size, buf)
      rescue EOFError
        nil
      end
    end
  end

  def test_readpartial_buffer_not_raise_shared_string_error
    assert_buffer_not_raise_shared_string_error do |r, size, buf|
      begin
        r.readpartial(size, buf)
      rescue EOFError
        nil
      end
    end
  end

  def test_puts_recursive_ary
    bug5986 = '[ruby-core:42444]'
    c = Class.new {
      def to_ary
        [self]
      end
    }
    s = StringIO.new
    s.puts(c.new)
    assert_equal("[...]\n", s.string, bug5986)
  end

  def test_io_select_with_many_files
    bug8080 = '[ruby-core:53349]'

    assert_normal_exit %q{
      require "tempfile"

      # 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+20)
      rescue Errno::EPERM
        exit 0
      end

      tempfiles = []
      (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

      assert_predicate(status, :success?)
    RUBY
  end
end
