// SPDX-License-Identifier: Unlicense package gigaparsec_test import ( "bytes" "cmp" "io" "math" "testing" "git.codemonkeysoftware.net/b/gigaparsec" ptest "git.codemonkeysoftware.net/b/gigaparsec/test" "git.codemonkeysoftware.net/b/gigaparsec/test/generator" pgen "git.codemonkeysoftware.net/b/gigaparsec/test/generator" "github.com/shoenig/test" "github.com/shoenig/test/must" "pgregory.net/rapid" ) type customEOFReaderAt struct { r *bytes.Reader eofAtExactFit bool } func newCustomEOFReaderAt(b []byte, eofAtExactFit bool) customEOFReaderAt { return customEOFReaderAt{ r: bytes.NewReader(b), eofAtExactFit: eofAtExactFit, } } func (r customEOFReaderAt) ReadAt(p []byte, off int64) (n int, err error) { n, err = r.r.ReadAt(p, off) if int64(len(p))+off >= r.r.Size() { if r.eofAtExactFit { err = io.EOF } else { err = nil } } return n, err } func TestState(t *testing.T) { t.Run("state reads the same position every time", rapid.MakeCheck(func(t *rapid.T) { data := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "data") dst := pgen.SliceOfNZero[byte](0, len(data)-1).Draw(t, "dst") expected := data[:len(dst)] st := gigaparsec.MakeState(bytes.NewReader(data)) _, next, err := st.Read(dst) must.NoError(t, err) must.SliceEqOp(t, expected, dst) next.Read(dst) _, _, err = st.Read(dst) must.NoError(t, err) must.SliceEqOp(t, expected, dst) })) t.Run("Read ends before end of source", rapid.MakeCheck(func(t *rapid.T) { src := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "src") endReadAt := rapid.IntRange(0, len(src)-1).Draw(t, "endReadAt") pos := rapid.Uint64Range(0, uint64(endReadAt)).Draw(t, "pos") dst := pgen.SliceOfNZero[byte](0, endReadAt-int(pos)).Draw(t, "dst") st := gigaparsec.MakeState(bytes.NewReader(src)).At(pos) n, next, err := st.At(pos).Read(dst) test.EqOp(t, uint64(len(dst)), n) ptest.StateIsAt(t, next, pos+n) test.NoError(t, err) test.SliceEqOp(t, src[pos:pos+n], dst) })) t.Run("Non-empty Read ends at end of source", rapid.MakeCheck(func(t *rapid.T) { readerReturnsEOF := rapid.Bool().Draw(t, "readerReturnsEOF") src := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "src") dst := pgen.SliceOfNZero[byte](1, len(src)).Draw(t, "dst") pos := uint64(len(src) - len(dst)) st := gigaparsec.MakeState(newCustomEOFReaderAt(src, readerReturnsEOF)) n, next, err := st.At(pos).Read(dst) test.EqOp(t, uint64(len(dst)), n) ptest.StateIsAt(t, next, pos+n) test.NoError(t, err) test.SliceEqOp(t, src[pos:pos+n], dst) })) t.Run("Read overruns source", rapid.MakeCheck(func(t *rapid.T) { src := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "src") pos := rapid.Uint64Range(0, uint64(len(src))-1).Draw(t, "pos") minDstLen := len(src) - int(pos) + 1 dst := pgen.SliceOfNZero[byte](minDstLen, minDstLen+10).Draw(t, "dst") st := gigaparsec.MakeState(bytes.NewReader(src)).At(pos) n, next, err := st.Read(dst) test.EqOp(t, uint64(len(src)), n+pos) ptest.StateIsAt(t, next, pos+n) test.ErrorIs(t, err, io.EOF) test.SliceEqOp(t, src[pos:pos+n], dst[:n]) })) t.Run("Read starts after end of source", rapid.MakeCheck(func(t *rapid.T) { src := rapid.SliceOfN(rapid.Byte(), 0, 100).Draw(t, "src") dst := pgen.SliceOfNZero[byte](0, 100).Draw(t, "dst") pos := rapid.Uint64Min(uint64(len(src))).Draw(t, "pos") st := gigaparsec.MakeState(bytes.NewReader(src)).At(pos) n, next, err := st.Read(dst) test.EqOp(t, 0, n) ptest.StateIsAt(t, next, pos) test.ErrorIs(t, err, io.EOF) })) t.Run("next state reads next input", rapid.MakeCheck(func(t *rapid.T) { const maxLen = 100 data := rapid.SliceOfN(rapid.Byte(), 1, maxLen).Draw(t, "data") skip := rapid.IntRange(0, len(data)-1).Draw(t, "skip") st := gigaparsec.MakeState(bytes.NewReader(data)) _, next, err := st.Read(make([]byte, skip)) must.NoError(t, err) must.EqOp(t, skip, int(next.Pos())) dst := make([]byte, maxLen) n, _, _ := next.Read(dst) must.SliceEqOp(t, data[skip:skip+int(n)], dst[:n]) })) t.Run("At sets state position", rapid.MakeCheck(func(t *rapid.T) { data := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "data") pos := rapid.Uint64Range(0, uint64(len(data)-1)).Draw(t, "pos") st := gigaparsec.MakeState(bytes.NewReader(data)).At(pos) dst := make([]byte, 1) n, _, err := st.Read(dst) test.EqOp(t, 1, n) test.NoError(t, err) test.EqOp(t, data[pos], dst[0]) })) t.Run("Pos returns correct position after At", rapid.MakeCheck(func(t *rapid.T) { var data []byte pos := rapid.Uint64().Draw(t, "pos") st := gigaparsec.MakeState(bytes.NewReader(data)).At(pos) test.EqOp(t, pos, st.Pos()) })) t.Run("Pos returns correct position after Read", rapid.MakeCheck(func(t *rapid.T) { const maxLen = 100 data := rapid.SliceOfN(rapid.Byte(), 1, maxLen).Draw(t, "data") skip := rapid.Uint64Range(0, uint64(len(data)-1)).Draw(t, "skip") st := gigaparsec.MakeState(bytes.NewReader(data)) _, next, err := st.Read(make([]byte, skip)) must.NoError(t, err) test.EqOp(t, skip, next.Pos()) })) t.Run("Read returns an error if the ReaderAt fails", rapid.MakeCheck(func(t *rapid.T) { expectedErr := pgen.Error().Draw(t, "expectedErr") startPos := rapid.Uint64Max(math.MaxInt64).Draw(t, "startPos") dst := pgen.SliceOfNZero[byte](0, 100).Draw(t, "dst") st := gigaparsec.MakeState(ptest.ErrReaderAt(expectedErr)).At(startPos) n, next, err := st.Read(dst) test.ErrorIs(t, err, expectedErr) test.EqOp(t, startPos, next.Pos()) test.Zero(t, n) })) } func TestSliceReaderAt(t *testing.T) { const maxLen = 100 t.Run("offset ≥ 0", rapid.MakeCheck(func(t *rapid.T) { src := rapid.SliceOfN(rapid.Byte(), 0, maxLen).Draw(t, "src") dst := generator.SliceOfNZero[byte](0, maxLen).Draw(t, "dst") offset := rapid.Int64Range(0, int64(len(src))+10).Draw(t, "offset") n, err := gigaparsec.SliceReaderAt[byte](src).ReadAt(dst, offset) switch cmp.Compare(len(src), int(offset)+len(dst)) { case -1: // Read overruns src. test.ErrorIs(t, err, io.EOF) test.EqOp(t, max(0, len(src)-int(offset)), n) case 0: // Read exactly reaches end of source. // io.ReaderAt spec allows error to be either io.EOF or nil. test.EqOp(t, len(dst), n) case 1: // Read ends before end of source. test.NoError(t, err) test.EqOp(t, len(dst), n) } if offset < int64(len(src)) { test.SliceEqOp(t, src[offset:offset+int64(n)], dst[:n]) } })) t.Run("offset < 0", rapid.MakeCheck(func(t *rapid.T) { src := rapid.SliceOfN(rapid.Byte(), 0, maxLen).Draw(t, "src") dst := generator.SliceOfNZero[byte](0, maxLen).Draw(t, "dst") offset := rapid.Int64Max(-1).Draw(t, "offset") n, err := gigaparsec.SliceReaderAt[byte](src).ReadAt(dst, offset) test.Error(t, err) test.EqOp(t, 0, n) })) }