require "spec" private class PartialReaderIO < IO @slice : Bytes def initialize(data : String) @slice = data.to_slice end def read(slice : Bytes) return 0 if @slice.size == 0 max_read_size = {slice.size, @slice.size}.min read_size = rand(1..max_read_size) slice.copy_from(@slice[0, read_size]) @slice += read_size read_size end def write(slice : Bytes) : NoReturn raise "write" end end private class MemoryIOWithoutPeek < IO::Memory def peek nil end end private class MemoryIOWithFixedPeek < IO::Memory property peek_size = 0 def peek Slice.new(@buffer + @pos, {@bytesize - @pos, peek_size}.min) end end describe "IO::Delimited" do describe "#read" do context "without peeking" do it "doesn't read past the limit" do io = MemoryIOWithoutPeek.new("abcderzzrfgzr") delimited = IO::Delimited.new(io, read_delimiter: "zr") delimited.gets_to_end.should eq("abcderz") io.gets_to_end.should eq("fgzr") end it "doesn't read past the limit (char-by-char)" do io = MemoryIOWithoutPeek.new("abcderzzrfg") delimited = IO::Delimited.new(io, read_delimiter: "zr") delimited.read_char.should eq('a') delimited.read_char.should eq('b') delimited.read_char.should eq('c') delimited.read_char.should eq('d') delimited.read_char.should eq('e') delimited.read_char.should eq('r') delimited.read_char.should eq('z') delimited.read_char.should be_nil delimited.read_char.should be_nil delimited.read_char.should be_nil delimited.read_char.should be_nil io.read_char.should eq('f') io.read_char.should eq('g') end it "doesn't clobber active_delimiter_buffer" do io = MemoryIOWithoutPeek.new("ab12312") delimited = IO::Delimited.new(io, read_delimiter: "12345") delimited.gets_to_end.should eq("ab12312") end it "handles the delimiter at the start" do io = MemoryIOWithoutPeek.new("ab12312") delimited = IO::Delimited.new(io, read_delimiter: "ab1") delimited.read_char.should be_nil end it "handles the delimiter at the end" do io = MemoryIOWithoutPeek.new("ab12312z") delimited = IO::Delimited.new(io, read_delimiter: "z") delimited.gets_to_end.should eq("ab12312") end it "handles nearly a delimiter at the end" do io = MemoryIOWithoutPeek.new("ab12312") delimited = IO::Delimited.new(io, read_delimiter: "122") delimited.gets_to_end.should eq("ab12312") end it "doesn't clobber the buffer on closely-offset partial matches" do io = MemoryIOWithoutPeek.new("abab1234abcdefgh") delimited = IO::Delimited.new(io, read_delimiter: "abcdefgh") delimited.gets_to_end.should eq("abab1234") end end context "with partial read" do it "handles partial reads" do io = PartialReaderIO.new("abab1234abcdefgh") delimited = IO::Delimited.new(io, read_delimiter: "abcdefgh") delimited.gets_to_end.should eq("abab1234") end end context "with peeking" do it "returns empty when there's no data" do io = IO::Memory.new("") delimited = IO::Delimited.new(io, read_delimiter: "zr") delimited.peek.should eq("".to_slice) delimited.gets_to_end.should eq("") io.gets_to_end.should eq("") end it "doesn't read past the limit" do io = IO::Memory.new("abcderzzrfgzr") delimited = IO::Delimited.new(io, read_delimiter: "zr") delimited.peek.should eq("abcderz".to_slice) delimited.gets_to_end.should eq("abcderz") io.gets_to_end.should eq("fgzr") end it "doesn't read past the limit, single byte" do io = IO::Memory.new("abcderzzrfgzr") delimited = IO::Delimited.new(io, read_delimiter: "f") delimited.peek.should eq("abcderzzr".to_slice) delimited.gets_to_end.should eq("abcderzzr") io.gets_to_end.should eq("gzr") end it "doesn't read past the limit (char-by-char)" do io = IO::Memory.new("abcderzzrfg") delimited = IO::Delimited.new(io, read_delimiter: "zr") delimited.read_char.should eq('a') delimited.read_char.should eq('b') delimited.read_char.should eq('c') delimited.read_char.should eq('d') delimited.read_char.should eq('e') delimited.read_char.should eq('r') delimited.read_char.should eq('z') delimited.read_char.should be_nil delimited.read_char.should be_nil delimited.read_char.should be_nil delimited.read_char.should be_nil io.read_char.should eq('f') io.read_char.should eq('g') end it "doesn't clobber active_delimiter_buffer" do io = IO::Memory.new("ab12312") delimited = IO::Delimited.new(io, read_delimiter: "12345") delimited.peek.should eq("ab123".to_slice) delimited.gets_to_end.should eq("ab12312") end it "handles the delimiter at the start" do io = IO::Memory.new("ab12312") delimited = IO::Delimited.new(io, read_delimiter: "ab1") delimited.peek.should eq(Bytes.empty) delimited.read_char.should be_nil end it "handles the delimiter at the end" do io = IO::Memory.new("ab12312z") delimited = IO::Delimited.new(io, read_delimiter: "z") delimited.peek.should eq("ab12312".to_slice) delimited.gets_to_end.should eq("ab12312") end it "handles nearly a delimiter at the end" do io = IO::Memory.new("ab12312") delimited = IO::Delimited.new(io, read_delimiter: "122") delimited.peek.should eq("ab123".to_slice) delimited.gets_to_end.should eq("ab12312") end it "doesn't clobber the buffer on closely-offset partial matches" do io = IO::Memory.new("abab1234abcdefgh") delimited = IO::Delimited.new(io, read_delimiter: "abcdefgh") delimited.peek.should eq("abab1234".to_slice) delimited.gets_to_end.should eq("abab1234") end it "handles the case of peek matching first byte, not having enough room, but rest not matching" do # not a delimiter # --- io = MemoryIOWithFixedPeek.new("abcdefwhi") # ------- # peek io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "fgh") delimited.peek.should eq("abcde".to_slice) delimited.gets_to_end.should eq("abcdefwhi") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("") end it "handles the case of peek matching first byte, not having enough room, but rest not immediately matching (with a potential match afterwards)" do # not a delimiter, but the second 'f' will actually match the delimiter # --- io = MemoryIOWithFixedPeek.new("abcdeffghijk") # ------- # peek io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "fgh") delimited.peek.should eq("abcde".to_slice) delimited.gets_to_end.should eq("abcdef") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("ijk") end it "handles the case of peek matching first byte, not having enough room, but later matching" do # delimiter # --- io = MemoryIOWithFixedPeek.new("abcdefghijk") # ------- # peek io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "fgh") delimited.peek.should eq("abcde".to_slice) delimited.gets_to_end.should eq("abcde") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("ijk") end it "handles the case of peek matching first byte, not having enough room, but later not matching" do # not a delimiter # --- io = MemoryIOWithFixedPeek.new("abcdefgwijkfghhello") # ------- --- # peek delimiter io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "fgh") delimited.peek.should eq("abcde".to_slice) delimited.gets_to_end.should eq("abcdefgwijk") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("hello") end it "handles the case of peek matching first byte, not having enough room, but later not matching (limited slice)" do # not a delimiter # --- io = MemoryIOWithFixedPeek.new("abcdefgwijkfghhello") # ------- --- # peek delimiter io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "fgh") delimited.peek.should eq("abcde".to_slice) delimited.read_string(6).should eq "abcdef" delimited.read_string(5).should eq("gwijk") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("hello") end it "handles the case of peek matching first byte, not having enough room, later only partially matching" do # delimiter # ------------ io = MemoryIOWithFixedPeek.new("abcdefghijklmnopqrst") # -------~~~~~~~ # peek peek io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "fghijklmnopq") delimited.peek.should eq("abcde".to_slice) delimited.gets_to_end.should eq("abcde") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("rst") end it "peeks, everything matches but we can't know what will happen after that" do io = MemoryIOWithFixedPeek.new("fgh") io.peek_size = 2 delimited = IO::Delimited.new(io, read_delimiter: "fgh") delimited.peek.should be_nil end it "handles the case of the active delimited buffer including the delimiter" do # delimiter # --- io = MemoryIOWithFixedPeek.new("aaabcde") # -- # peek io.peek_size = 2 delimited = IO::Delimited.new(io, read_delimiter: "aab") delimited.peek.should be_nil delimited.gets_to_end.should eq("a") delimited.gets_to_end.should eq("") io.gets_to_end.should eq("cde") end it "can peek if first byte found but doesn't fully match, and there's that first byte again in the peek buffer" do # delimiter # ----- io = MemoryIOWithFixedPeek.new("holhhello") # ---- # peek io.peek_size = 4 delimited = IO::Delimited.new(io, read_delimiter: "hello") delimited.peek.should eq("hol".to_slice) end it "can peek if first byte found but doesn't fully match, and the byte isn't there in the peek buffer" do # delimiter # ----- io = MemoryIOWithFixedPeek.new("holahello") # ---- # peek io.peek_size = 4 delimited = IO::Delimited.new(io, read_delimiter: "hello") delimited.peek.should eq("hola".to_slice) end end end describe "#gets with peeking when can't peek" do it "gets" do io = MemoryIOWithFixedPeek.new("abcdefghel") io.peek_size = 7 delimited = IO::Delimited.new(io, read_delimiter: "hello") # We first peek "abcdefg". # The delimiter's first byte isn't there so we read everything. # Next we peek "hel". It matches the delimiter's beginning # but `peek` can't tell whether there's more content or not # after that, and it can't return an empty buffer because that # means EOF. So it must return `nil`. So `IO#gets` should # handle this case where `peek` becomes `nil`. delimited.gets.should eq("abcdefghel") end it "peeks" do io = MemoryIOWithFixedPeek.new("hel") io.peek_size = 1 delimited = IO::Delimited.new(io, read_delimiter: "hello") delimited.read(Bytes.new(1)) delimited.peek.should eq("el".to_slice) end end describe "#write" do it "raises" do delimited = IO::Delimited.new(IO::Memory.new, read_delimiter: "zr") expect_raises(IO::Error, "Can't write to IO::Delimited") do delimited.puts "test string" end end end describe "#close" do it "stops reading" do io = IO::Memory.new "abcdefg" delimited = IO::Delimited.new(io, read_delimiter: "zr") delimited.read_char.should eq('a') delimited.read_char.should eq('b') delimited.close delimited.closed?.should be_true expect_raises(IO::Error, "Closed stream") do delimited.read_char end end it "closes the underlying stream if sync_close is true" do io = IO::Memory.new "abcdefg" delimited = IO::Delimited.new(io, read_delimiter: "zr", sync_close: true) delimited.sync_close?.should be_true io.closed?.should be_false delimited.close io.closed?.should be_true end end end