require "../spec_helper"
require "../../support/channel"
require "../../support/string"
require "spec/helpers/iterate"

require "socket"
require "big"
require "base64"

# This is a non-optimized version of IO::Memory so we can test
# raw IO. Optimizations for specific IOs are tested separately
# (for example in buffered_io_spec)
private class SimpleIOMemory < IO
  getter buffer : UInt8*
  getter bytesize : Int32
  @capacity : Int32
  @pos : Int32
  @max_read : Int32?

  def initialize(capacity = 64, @max_read = nil)
    @buffer = GC.malloc_atomic(capacity.to_u32).as(UInt8*)
    @bytesize = 0
    @capacity = capacity
    @pos = 0
  end

  def self.new(string : String, max_read = nil)
    io = new(string.bytesize, max_read: max_read)
    io << string
    io
  end

  def self.new(bytes : Bytes, max_read = nil)
    io = new(bytes.size, max_read: max_read)
    io.write(bytes)
    io
  end

  def read(slice : Bytes)
    count = slice.size
    count = Math.min(count, @bytesize - @pos)
    if max_read = @max_read
      count = Math.min(count, max_read)
    end
    slice.copy_from(@buffer + @pos, count)
    @pos += count
    count
  end

  def write(slice : Bytes) : Nil
    count = slice.size
    new_bytesize = bytesize + count
    if new_bytesize > @capacity
      resize_to_capacity(Math.pw2ceil(new_bytesize))
    end

    slice.copy_to(@buffer + @bytesize, count)
    @bytesize += count
  end

  def to_slice
    Slice.new(@buffer, @bytesize)
  end

  def to_s
    String.new @buffer, @bytesize
  end

  def rewind
    @pos = 0
    self
  end

  private def resize_to_capacity(capacity)
    @capacity = capacity
    @buffer = GC.realloc(@buffer, @capacity)
  end
end

private class OneByOneIO < IO
  @bytes : Bytes

  def initialize(string)
    @bytes = string.to_slice
    @pos = 0
  end

  def read(slice : Bytes)
    return 0 if slice.empty?
    return 0 if @pos >= @bytes.size

    slice[0] = @bytes[@pos]
    @pos += 1
    1
  end

  def write(slice : Bytes) : Nil
  end
end

describe IO do
  describe "partial read" do
    it "doesn't block on first read.  blocks on 2nd read" do
      IO.pipe do |read, write|
        write.puts "hello"
        slice = Bytes.new 1024

        read.read_timeout = 1.second
        read.read(slice).should eq(6)

        expect_raises(IO::TimeoutError) do
          read.read_timeout = 0.1.microseconds
          read.read(slice)
        end
      end
    end
  end

  it_iterates "#each_line", ["hello", "bye"], SimpleIOMemory.new("hello\nbye\n").each_line
  it_iterates "#each_line(chomp: false)", ["hello\n", "bye\n"], SimpleIOMemory.new("hello\nbye\n").each_line(chomp: false)

  it_iterates "#char", ['a', 'b', 'あ', 'ぼ'], SimpleIOMemory.new("abあぼ").each_char
  it_iterates "#char", ['a'.ord.to_u8, 'b'.ord.to_u8], SimpleIOMemory.new("ab").each_byte

  it "copies" do
    string = "abあぼ"
    src = SimpleIOMemory.new(string)
    dst = SimpleIOMemory.new
    IO.copy(src, dst).should eq(string.bytesize)
    dst.to_s.should eq(string)
  end

  it "copies with limit" do
    string = "abcあぼ"
    src = SimpleIOMemory.new(string)
    dst = SimpleIOMemory.new
    IO.copy(src, dst, 3).should eq(3)
    dst.to_s.should eq("abc")
  end

  it "raises on copy with negative limit" do
    string = "abcあぼ"
    src = SimpleIOMemory.new(string)
    dst = SimpleIOMemory.new
    expect_raises(ArgumentError, "Negative limit") do
      IO.copy(src, dst, -10)
    end
  end

  describe "read operations" do
    it "does gets" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.gets.should eq("hello")
      io.gets.should eq("world")
      io.gets.should be_nil
    end

    it "does gets with \\r\\n" do
      io = SimpleIOMemory.new("hello\r\nworld\r\nfoo\rbar\n")
      io.gets.should eq("hello")
      io.gets.should eq("world")
      io.gets.should eq("foo\rbar")
      io.gets.should be_nil
    end

    it "does gets with \\r\\n, chomp true goes past \\r" do
      io = SimpleIOMemory.new("hello\rworld\r\nfoo\rbar\n")
      io.gets(chomp: true, limit: 8).should eq("hello\rwo")
    end

    it "does gets with chomp false" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.gets(chomp: false).should eq("hello\n")
      io.gets(chomp: false).should eq("world\n")
      io.gets(chomp: false).should be_nil
    end

    it "does gets with empty string (no peek)" do
      io = SimpleIOMemory.new("")
      io.gets(chomp: true).should be_nil
    end

    it "does gets with empty string (with peek)" do
      io = IO::Memory.new("")
      io.gets(chomp: true).should be_nil
    end

    it "does gets with \\n (no peek)" do
      io = SimpleIOMemory.new("\n")
      io.gets(chomp: true).should eq("")
      io.gets(chomp: true).should be_nil
    end

    it "does gets with \\n (with peek)" do
      io = IO::Memory.new("\n")
      io.gets(chomp: true).should eq("")
      io.gets(chomp: true).should be_nil
    end

    it "does gets with \\r\\n (no peek)" do
      io = SimpleIOMemory.new("\r\n")
      io.gets(chomp: true).should eq("")
      io.gets(chomp: true).should be_nil
    end

    it "does gets with \\r\\n (with peek)" do
      io = IO::Memory.new("\r\n")
      io.gets(chomp: true).should eq("")
      io.gets(chomp: true).should be_nil
    end

    it "does gets with big line" do
      big_line = "a" * 20_000
      io = SimpleIOMemory.new("#{big_line}\nworld\n")
      io.gets.should eq(big_line)
    end

    it "does gets with char delimiter" do
      io = SimpleIOMemory.new("hello world")
      io.gets('w').should eq("hello w")
      io.gets('r').should eq("or")
      io.gets('r').should eq("ld")
      io.gets('r').should be_nil
    end

    it "does gets with unicode char delimiter" do
      io = SimpleIOMemory.new("こんにちは")
      io.gets('ち').should eq("こんにち")
      io.gets('ち').should eq("は")
      io.gets('ち').should be_nil
    end

    it "gets with string as delimiter" do
      io = SimpleIOMemory.new("hello world")
      io.gets("lo").should eq("hello")
      io.gets("rl").should eq(" worl")
      io.gets("foo").should eq("d")
    end

    it "gets with string as delimiter and chomp = true" do
      io = SimpleIOMemory.new("hello world")
      io.gets("lo", chomp: true).should eq("hel")
      io.gets("rl", chomp: true).should eq(" wo")
      io.gets("foo", chomp: true).should eq("d")
    end

    it "gets with empty string as delimiter" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.gets("").should eq("hello\nworld\n")
    end

    it "gets with single byte string as delimiter" do
      io = SimpleIOMemory.new("hello\nworld\nbye")
      io.gets('\n').should eq("hello\n")
      io.gets('\n').should eq("world\n")
      io.gets('\n').should eq("bye")
    end

    it "does gets with limit" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.gets(3).should eq("hel")
      io.gets(10_000).should eq("lo\n")
      io.gets(10_000).should eq("world\n")
      io.gets(3).should be_nil
    end

    it "does gets with char and limit" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.gets('o', 2).should eq("he")
      io.gets('w', 10_000).should eq("llo\nw")
      io.gets('z', 10_000).should eq("orld\n")
      io.gets('a', 3).should be_nil
    end

    it "doesn't underflow when limit is unsigned" do
      io = IO::Memory.new("aїa")
      io.gets('є', 2u32).should eq("aї")
    end

    it "raises if invoking gets with negative limit" do
      io = SimpleIOMemory.new("hello\nworld\n")
      expect_raises ArgumentError, "Negative limit" do
        io.gets(-1)
      end
    end

    it "does read_line with limit" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.read_line(3).should eq("hel")
      io.read_line(10_000).should eq("lo\n")
      io.read_line(10_000).should eq("world\n")
      expect_raises(IO::EOFError) { io.read_line(3) }
    end

    it "does read_line with char and limit" do
      io = SimpleIOMemory.new("hello\nworld\n")
      io.read_line('o', 2).should eq("he")
      io.read_line('w', 10_000).should eq("llo\nw")
      io.read_line('z', 10_000).should eq("orld\n")
      expect_raises(IO::EOFError) { io.read_line('a', 3) }
    end

    it "reads all remaining content" do
      io = SimpleIOMemory.new("foo\nbar\nbaz\n")
      io.gets.should eq("foo")
      io.gets_to_end.should eq("bar\nbaz\n")
      io.gets_to_end.should eq("")
    end

    it "reads all remaining content as bytes" do
      io = SimpleIOMemory.new(Bytes[0, 1, 3, 6, 10, 15])
      io.getb_to_end.should eq(Bytes[0, 1, 3, 6, 10, 15])
      io.getb_to_end.should eq(Bytes[])
      io.rewind
      bytes = io.getb_to_end
      bytes.should eq(Bytes[0, 1, 3, 6, 10, 15])
      bytes.read_only?.should be_false

      io.rewind
      io.write(Bytes[2, 4, 5])
      bytes.should eq(Bytes[0, 1, 3, 6, 10, 15])
    end

    it "reads char" do
      io = SimpleIOMemory.new("hi 世界")
      io.read_char.should eq('h')
      io.read_char.should eq('i')
      io.read_char.should eq(' ')
      io.read_char.should eq('世')
      io.read_char.should eq('界')
      io.read_char.should be_nil

      {% for bytes, char in VALID_UTF8_BYTE_SEQUENCES %}
        SimpleIOMemory.new(Bytes{{ bytes }}).read_char.should eq({{ char }})
      {% end %}

      {% for bytes in INVALID_UTF8_BYTE_SEQUENCES %}
        expect_raises(InvalidByteSequenceError) { SimpleIOMemory.new(Bytes{{ bytes }}).read_char }
      {% end %}
    end

    it "reads byte" do
      io = SimpleIOMemory.new("hello")
      io.read_byte.should eq('h'.ord)
      io.read_byte.should eq('e'.ord)
      io.read_byte.should eq('l'.ord)
      io.read_byte.should eq('l'.ord)
      io.read_byte.should eq('o'.ord)
      io.read_char.should be_nil
    end

    it "reads string" do
      io = SimpleIOMemory.new("hello world")
      io.read_string(5).should eq("hello")
      io.read_string(1).should eq(" ")
      io.read_string(0).should eq("")
      expect_raises(IO::EOFError) do
        io.read_string(6)
      end
    end

    it "does each_line" do
      lines = [] of String
      io = SimpleIOMemory.new("a\nbb\ncc")
      io.each_line do |line|
        lines << line
      end
      lines.should eq ["a", "bb", "cc"]
    end

    it "does each_char" do
      chars = [] of Char
      io = SimpleIOMemory.new("あいう")
      io.each_char do |c|
        chars << c
      end
      chars.should eq ['あ', 'い', 'う']
    end

    it "does each_byte" do
      bytes = [] of UInt8
      io = SimpleIOMemory.new("abc")
      io.each_byte do |b|
        bytes << b
      end
      bytes.should eq ['a'.ord.to_u8, 'b'.ord.to_u8, 'c'.ord.to_u8]
    end

    it "raises on EOF with read_line" do
      str = SimpleIOMemory.new("hello")
      str.read_line.should eq("hello")

      expect_raises IO::EOFError, "End of file reached" do
        str.read_line
      end
    end

    it "raises on EOF with readline and delimiter" do
      str = SimpleIOMemory.new("hello")
      str.read_line('e').should eq("he")
      str.read_line('e').should eq("llo")

      expect_raises IO::EOFError, "End of file reached" do
        str.read_line
      end
    end

    it "does read_fully" do
      str = SimpleIOMemory.new("hello")
      slice = Bytes.new(4)
      str.read_fully(slice).should eq(4)
      String.new(slice).should eq("hell")

      expect_raises(IO::EOFError) do
        str.read_fully(slice)
      end
    end

    it "does read_fully?" do
      str = SimpleIOMemory.new("hello")
      slice = Bytes.new(4)
      str.read_fully?(slice).should eq(4)
      String.new(slice).should eq("hell")

      str.read_fully?(slice).should be_nil
    end

    # pipe(2) returns bidirectional file descriptors on some platforms,
    # gate this test behind the platform flag.
    {% unless flag?(:freebsd) || flag?(:solaris) || flag?(:openbsd) || flag?(:dragonfly) %}
      it "raises if trying to read to an IO not opened for reading" do
        IO.pipe do |r, w|
          expect_raises(IO::Error, "File not open for reading") do
            w.gets
          end
        end
      end
    {% end %}

    describe ".same_content?" do
      it "compares two ios, one way (true)" do
        io1 = OneByOneIO.new("hello")
        io2 = IO::Memory.new("hello")
        IO.same_content?(io1, io2).should be_true
      end

      it "compares two ios, second way (true)" do
        io1 = OneByOneIO.new("hello")
        io2 = IO::Memory.new("hello")
        IO.same_content?(io2, io1).should be_true
      end

      it "compares two ios, one way (false)" do
        io1 = OneByOneIO.new("hello")
        io2 = IO::Memory.new("hella")
        IO.same_content?(io1, io2).should be_false
      end

      it "compares two ios, second way (false)" do
        io1 = OneByOneIO.new("hello")
        io2 = IO::Memory.new("hella")
        IO.same_content?(io2, io1).should be_false
      end

      it "refutes prefix match, one way" do
        io1 = OneByOneIO.new("hello")
        io2 = IO::Memory.new("hello again")
        IO.same_content?(io1, io2).should be_false
      end

      it "refutes prefix match, second way" do
        io1 = IO::Memory.new("hello")
        io2 = OneByOneIO.new("hello again")
        IO.same_content?(io1, io2).should be_false
      end

      it "refutes prefix match, one way" do
        io1 = OneByOneIO.new("hello again")
        io2 = IO::Memory.new("hello")
        IO.same_content?(io1, io2).should be_false
      end

      it "refutes prefix match, second way" do
        io1 = IO::Memory.new("hello again")
        io2 = OneByOneIO.new("hello")
        IO.same_content?(io1, io2).should be_false
      end
    end

    it "combines multiple reads using #read_greedy" do
      bytes = Bytes.new 7

      io = SimpleIOMemory.new("Hello World", max_read: 2)
      io.read_greedy(bytes).should eq(7)
      bytes.should eq("Hello W".to_slice)
      io.read_greedy(bytes).should eq(4)
      bytes[0, 4].should eq("orld".to_slice)
      io.read_greedy(bytes).should eq(0)
    end
  end

  describe "write operations" do
    it "does puts" do
      io = SimpleIOMemory.new
      io.puts "Hello"
      io.gets_to_end.should eq("Hello\n")
    end

    it "does puts with big string" do
      io = SimpleIOMemory.new
      s = "*" * 20_000
      io << "hello"
      io << s
      io.gets_to_end.should eq("hello#{s}")
    end

    it "does puts many times" do
      io = SimpleIOMemory.new
      10_000.times { io << "hello" }
      io.gets_to_end.should eq("hello" * 10_000)
    end

    it "puts several arguments" do
      io = SimpleIOMemory.new
      io.puts(1, "aaa", "\n")
      io.gets_to_end.should eq("1\naaa\n\n")
    end

    it "prints" do
      io = SimpleIOMemory.new
      io.print "foo"
      io.gets_to_end.should eq("foo")
    end

    it "prints several arguments" do
      io = SimpleIOMemory.new
      io.print "foo", "bar", "baz"
      io.gets_to_end.should eq("foobarbaz")
    end

    it "writes bytes" do
      io = SimpleIOMemory.new
      10_000.times { io.write_byte 'a'.ord.to_u8 }
      io.gets_to_end.should eq("a" * 10_000)
    end

    it "writes with printf" do
      io = SimpleIOMemory.new
      io.printf "Hello %d", 123
      io.gets_to_end.should eq("Hello 123")
    end

    it "writes with printf as an array" do
      io = SimpleIOMemory.new
      io.printf "Hello %d", [123]
      io.gets_to_end.should eq("Hello 123")
    end

    it "skips a few bytes" do
      io = SimpleIOMemory.new
      io << "hello world"
      io.skip(6)
      io.gets_to_end.should eq("world")
    end

    it "skips but raises if not enough bytes" do
      io = SimpleIOMemory.new
      io << "hello"
      expect_raises(IO::EOFError) do
        io.skip(6)
      end
    end

    it "skips more than 4096 bytes" do
      io = SimpleIOMemory.new
      io << "a" * 4100
      io.skip(4099)
      io.gets_to_end.should eq("a")
    end

    it "skips to end" do
      io = SimpleIOMemory.new
      io << "hello"
      io.skip_to_end
      io.read_byte.should be_nil
    end

    # pipe(2) returns bidirectional file descriptors on some platforms,
    # gate this test behind the platform flag.
    {% unless flag?(:freebsd) || flag?(:solaris) || flag?(:openbsd) || flag?(:dragonfly) %}
      it "raises if trying to write to an IO not opened for writing" do
        IO.pipe do |r, w|
          # unless sync is used the flush on close triggers the exception again
          r.sync = true

          expect_raises(IO::Error, "File not open for writing") do
            r << "hello"
          end
        end
      end
    {% end %}
  end

  {% unless flag?(:without_iconv) %}
    describe "encoding" do
      describe "decode" do
        it "gets_to_end" do
          str = "Hello world" * 200
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets_to_end.should eq(str)
        end

        it "gets" do
          str = "Hello world\r\nFoo\nBar"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets.should eq("Hello world")
          io.gets.should eq("Foo")
          io.gets.should eq("Bar")
          io.gets.should be_nil
        end

        it "gets with chomp = false" do
          str = "Hello world\r\nFoo\nBar"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets(chomp: false).should eq("Hello world\r\n")
          io.gets(chomp: false).should eq("Foo\n")
          io.gets(chomp: false).should eq("Bar")
          io.gets(chomp: false).should be_nil
        end

        it "gets big string" do
          str = "Hello\nWorld\n" * 10_000
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          10_000.times do |i|
            io.gets.should eq("Hello")
            io.gets.should eq("World")
          end
        end

        it "gets big EUC-JP string" do
          2.times do
            str = ("好我是人\n" * 1000).encode("EUC-JP")
            io = SimpleIOMemory.new(str)
            io.set_encoding("EUC-JP")
            1000.times do
              io.gets.should eq("好我是人")
            end
          end
        end

        it "does gets on unicode with char and limit without off-by-one" do
          io = SimpleIOMemory.new("test\nabc".encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets('a', 5).should eq("test\n")
          io = SimpleIOMemory.new("test\nabc".encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets('a', 6).should eq("test\na")
        end

        it "gets with limit" do
          str = "Hello\nWorld\n"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets(3).should eq("Hel")
        end

        it "gets with limit (small, no newline)" do
          str = "Hello world" * 10_000
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets(3).should eq("Hel")
        end

        it "gets with non-ascii" do
          str = "你好我是人"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets('人').should eq("你好我是人")
        end

        it "gets with non-ascii and chomp: false" do
          str = "你好我是人"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets('人', chomp: true).should eq("你好我是")
        end

        it "gets with limit (big)" do
          str = "Hello world" * 10_000
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets(20_000).should eq(str[0, 20_000])
        end

        it "gets with string delimiter" do
          str = "Hello world\nFoo\nBar"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.gets("wo").should eq("Hello wo")
          io.gets("oo").should eq("rld\nFoo")
          io.gets("xx").should eq("\nBar")
          io.gets("zz").should be_nil
        end

        it "reads char" do
          str = "Hello world"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          str.each_char do |char|
            io.read_char.should eq(char)
          end
          io.read_char.should be_nil
        end

        it "reads utf8 byte" do
          str = "Hello world"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          str.each_byte do |byte|
            io.read_utf8_byte.should eq(byte)
          end
          io.read_utf8_byte.should be_nil
        end

        it "reads utf8" do
          io = IO::Memory.new("好".encode("EUC-JP"))
          io.set_encoding("EUC-JP")

          buffer = uninitialized UInt8[1024]
          bytes_read = io.read_utf8(buffer.to_slice) # => 3
          bytes_read.should eq(3)
          buffer.to_slice[0, bytes_read].to_a.should eq("好".bytes)
        end

        it "raises on incomplete byte sequence" do
          io = SimpleIOMemory.new("好".byte_slice(0, 1))
          io.set_encoding("EUC-JP")
          expect_raises ArgumentError, "Incomplete multibyte sequence" do
            io.read_char
          end
        end

        it "says invalid byte sequence" do
          io = SimpleIOMemory.new(Slice.new(1, 255_u8))
          io.set_encoding("EUC-JP")
          message =
            {% if flag?(:musl) || flag?(:freebsd) || flag?(:netbsd) || flag?(:dragonfly) %}
              "Incomplete multibyte sequence"
            {% else %}
              "Invalid multibyte sequence"
            {% end %}
          expect_raises(ArgumentError, message) { io.read_char }
        end

        it "skips invalid byte sequences" do
          string = String.build do |str|
            str.write "好".encode("EUC-JP")
            str.write_byte 255_u8
            str.write "是".encode("EUC-JP")
          end
          io = SimpleIOMemory.new(string)
          io.set_encoding("EUC-JP", invalid: :skip)
          io.read_char.should eq('好')
          io.read_char.should eq('是')
          io.read_char.should be_nil
        end

        it "says invalid 'invalid' option" do
          io = SimpleIOMemory.new
          expect_raises ArgumentError, "Valid values for `invalid` option are `nil` and `:skip`, not :foo" do
            io.set_encoding("EUC-JP", invalid: :foo)
          end
        end

        it "says invalid encoding" do
          io = SimpleIOMemory.new("foo")
          io.set_encoding("FOO")
          expect_raises ArgumentError, "Invalid encoding: FOO" do
            io.gets_to_end
          end
        end

        it "sets encoding to utf-8 and stays as UTF-8" do
          io = SimpleIOMemory.new(Base64.decode_string("ey8qx+Tl8fwg7+Dw4Ozl8vD7IOLo5+jy4CovfQ=="))
          io.set_encoding("utf-8")
          io.encoding.should eq("UTF-8")
        end

        it "sets encoding to utf8 and stays as UTF-8" do
          io = SimpleIOMemory.new(Base64.decode_string("ey8qx+Tl8fwg7+Dw4Ozl8vD7IOLo5+jy4CovfQ=="))
          io.set_encoding("utf8")
          io.encoding.should eq("UTF-8")
        end

        it "handles long lines correctly with invalid: :skip" do
          # Using both ASCII characters and a 26-byte Unicode characters to
          # ensure we hit as many byte boundaries inside the Unicode characters
          # as we can to get sufficient confidence in this test.
          text = "test string 👩🏾‍🤝‍👨🏻" * 10240
          io = IO::Memory.new
          io.set_encoding "UTF-8", invalid: :skip
          io << text

          io.bytesize.should eq text.bytesize
          io.to_slice.should eq text.to_slice
        end

        it "does skips when converting to UTF-8" do
          io = SimpleIOMemory.new(Base64.decode_string("ey8qx+Tl8fwg7+Dw4Ozl8vD7IOLo5+jy4CovfQ=="))
          io.set_encoding("UTF-8", invalid: :skip)
          io.gets_to_end.should eq "{/*  */}"
        end

        it "decodes incomplete multibyte sequence with skip (#3285)" do
          bytes = Bytes[195, 229, 237, 229, 240, 224, 246, 232, 255, 32, 241, 234, 240, 232, 239, 242, 224, 32, 48, 46, 48, 49, 50, 54, 32, 241, 229, 234, 243, 237, 228, 10]
          m = IO::Memory.new(bytes)
          m.set_encoding("UTF-8", invalid: :skip)
          m.gets_to_end.should eq("  0.0126 \n")
        end

        it "decodes incomplete multibyte sequence with skip (2) (#3285)" do
          str = File.read(datapath("io_data_incomplete_multibyte_sequence.txt"))
          m = IO::Memory.new(Base64.decode_string str)
          m.set_encoding("UTF-8", invalid: :skip)
          m.gets_to_end.bytesize.should eq(4277)
        end

        it "decodes incomplete multibyte sequence with skip (3) (#3285)" do
          str = File.read(datapath("io_data_incomplete_multibyte_sequence_2.txt"))
          m = IO::Memory.new(Base64.decode_string str)
          m.set_encoding("UTF-8", invalid: :skip)
          m.gets_to_end.bytesize.should eq(8977)
        end

        it "reads string" do
          str = "Hello world\r\nFoo\nBar"
          io = SimpleIOMemory.new(str.encode("UCS-2LE"))
          io.set_encoding("UCS-2LE")
          io.read_string(11).should eq("Hello world")
          io.gets_to_end.should eq("\r\nFoo\nBar")
        end

        it "gets ascii from socket (#9056)" do
          server = TCPServer.new "localhost", 0
          sock = TCPSocket.new "localhost", server.local_address.port
          begin
            sock.set_encoding("ascii")
            spawn do
              client = server.accept
              message = client.gets
              client << "#{message}\n"
            end
            sock << "K\n"
            sock.gets.should eq("K")
          ensure
            server.close
            sock.close
          end
        end
      end

      describe "encode" do
        it "prints a string" do
          str = "Hello world"
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.print str
          slice = io.to_slice
          slice.should eq(str.encode("UCS-2LE"))
        end

        it "prints numbers" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.print 0
          io.print 1_u8
          io.print 2_u16
          io.print 3_u32
          io.print 4_u64
          io.print 5_i8
          io.print 6_i16
          io.print 7_i32
          io.print 8_i64
          io.print 9.1_f32
          io.print 10.11_f64
          slice = io.to_slice
          slice.should eq("0123456789.110.11".encode("UCS-2LE"))
        end

        it "prints bool" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.print true
          io.print false
          slice = io.to_slice
          slice.should eq("truefalse".encode("UCS-2LE"))
        end

        it "prints char" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.print 'a'
          slice = io.to_slice
          slice.should eq("a".encode("UCS-2LE"))
        end

        it "prints symbol" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.print :foo
          slice = io.to_slice
          slice.should eq("foo".encode("UCS-2LE"))
        end

        it "prints big int" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.print 123_456.to_big_i
          slice = io.to_slice
          slice.should eq("123456".encode("UCS-2LE"))
        end

        it "puts" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.puts 1
          io.puts
          slice = io.to_slice
          slice.should eq("1\n\n".encode("UCS-2LE"))
        end

        it "printf" do
          io = SimpleIOMemory.new
          io.set_encoding("UCS-2LE")
          io.printf "%s-%d-%.2f", "hi", 123, 45.67
          slice = io.to_slice
          slice.should eq("hi-123-45.67".encode("UCS-2LE"))
        end

        it "raises on invalid byte sequence" do
          io = SimpleIOMemory.new
          io.set_encoding("EUC-JP")
          expect_raises ArgumentError, "Invalid multibyte sequence" do
            io.print "\xff"
          end
        end

        it "skips on invalid byte sequence" do
          io = SimpleIOMemory.new
          io.set_encoding("EUC-JP", invalid: :skip)
          io.print "ñ"
          io.print "foo"
        end

        it "raises on incomplete byte sequence" do
          io = SimpleIOMemory.new
          io.set_encoding("EUC-JP")
          expect_raises ArgumentError, "Incomplete multibyte sequence" do
            io.print "好".byte_slice(0, 1)
          end
        end

        it "says invalid encoding" do
          io = SimpleIOMemory.new
          io.set_encoding("FOO")
          expect_raises ArgumentError, "Invalid encoding: FOO" do
            io.puts "a"
          end
        end
      end

      describe "#encoding" do
        it "returns \"UTF-8\" if the encoding is not manually set" do
          SimpleIOMemory.new.encoding.should eq("UTF-8")
        end

        it "returns the name of the encoding set via #set_encoding" do
          io = SimpleIOMemory.new
          io.set_encoding("UTF-16LE")
          io.encoding.should eq("UTF-16LE")
        end
      end
    end
  {% end %}

  describe "#close" do
    it "aborts 'read' in a different fiber" do
      ch = Channel(SpecChannelStatus).new(1)

      IO.pipe do |read, write|
        f = spawn do
          ch.send :begin
          read.gets
        rescue
          ch.send :end
        end

        schedule_timeout ch

        ch.receive.should eq SpecChannelStatus::Begin
        wait_until_blocked f

        read.close
        ch.receive.should eq SpecChannelStatus::End
      end
    end

    it "aborts 'write' in a different fiber" do
      ch = Channel(SpecChannelStatus).new(1)

      IO.pipe do |read, write|
        f = spawn do
          ch.send :begin
          loop do
            write.puts "some line"
          end
        rescue
          ch.send :end
        end

        schedule_timeout ch

        ch.receive.should eq SpecChannelStatus::Begin
        wait_until_blocked f

        write.close
        ch.receive.should eq SpecChannelStatus::End
      end
    end
  end

  describe IO::Error do
    describe ".new" do
      it "accepts `cause` argument (#14241)" do
        cause = Exception.new("cause")
        error = IO::Error.new("foo", cause: cause)
        error.cause.should be cause
      end
    end
  end
end
