Compare commits

..

No commits in common. "6e572d27487ed3d3f077abca6c1023e4155b7402" and "5e6eafef6450aedf6160f68e5539e5b77679c97e" have entirely different histories.

2 changed files with 21 additions and 154 deletions

View File

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"slices" "slices"
"strings" "strings"
) )
@ -117,22 +116,6 @@ type ReaderAt[T any] interface {
ReadAt(p []T, off int64) (n int, err error) ReadAt(p []T, off int64) (n int, err error)
} }
type SliceReaderAt[T any] []T
func (s SliceReaderAt[T]) ReadAt(dst []T, off int64) (n int, err error) {
if off < 0 {
return 0, errors.New("SliceReaderAt.ReadAt: negative offset")
}
if off >= int64(len(s)) {
return 0, io.EOF
}
n = copy(dst, s[off:])
if n < len(dst) {
err = io.EOF
}
return n, err
}
func MakeState[In any](r ReaderAt[In]) State[In] { func MakeState[In any](r ReaderAt[In]) State[In] {
return State[In]{r: r} return State[In]{r: r}
} }
@ -142,36 +125,18 @@ type State[In any] struct {
pos uint64 pos uint64
} }
// Read fills dst with data from this State's position in the underlying source.
// It returns the number of data it read and a new State for the position at which
// the read ended, and an error if the read either (1) failed or (2) reached the
// end of the source before filling dst. All reads from a given State will return
// data from the same position the source.
// If the source had too few data left to fill dst, or if the State's position is
// at or past the end of the source, err will be io.EOF.
func (s State[In]) Read(dst []In) (n uint64, next State[In], err error) { func (s State[In]) Read(dst []In) (n uint64, next State[In], err error) {
if s.pos > math.MaxInt64 {
return 0, s, io.EOF
}
nread, err := s.r.ReadAt(dst, int64(s.pos)) nread, err := s.r.ReadAt(dst, int64(s.pos))
if nread > 0 { if nread > 0 {
s.pos += uint64(nread) s.pos += uint64(nread)
} }
if nread == len(dst) && err == io.EOF {
if nread == 0 {
return 0, s, io.EOF
}
return uint64(nread), s, nil
}
return uint64(nread), s, err return uint64(nread), s, err
} }
// Pos returns this State's position.
func (s State[In]) Pos() uint64 { func (s State[In]) Pos() uint64 {
return s.pos return s.pos
} }
// At returns a State pointing at pos in the same data source.
func (s State[In]) At(pos uint64) State[In] { func (s State[In]) At(pos uint64) State[In] {
return State[In]{r: s.r, pos: pos} return State[In]{r: s.r, pos: pos}
} }

View File

@ -2,44 +2,17 @@ package gigaparsec_test
import ( import (
"bytes" "bytes"
"cmp"
"io" "io"
"math"
"testing" "testing"
"git.codemonkeysoftware.net/b/gigaparsec" "git.codemonkeysoftware.net/b/gigaparsec"
ptest "git.codemonkeysoftware.net/b/gigaparsec/test" ptest "git.codemonkeysoftware.net/b/gigaparsec/test"
"git.codemonkeysoftware.net/b/gigaparsec/test/generator"
pgen "git.codemonkeysoftware.net/b/gigaparsec/test/generator" pgen "git.codemonkeysoftware.net/b/gigaparsec/test/generator"
"github.com/shoenig/test" "github.com/shoenig/test"
"github.com/shoenig/test/must" "github.com/shoenig/test/must"
"pgregory.net/rapid" "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) { func TestState(t *testing.T) {
t.Run("state reads the same position every time", rapid.MakeCheck(func(t *rapid.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") data := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "data")
@ -56,59 +29,19 @@ func TestState(t *testing.T) {
must.NoError(t, err) must.NoError(t, err)
must.SliceEqOp(t, expected, dst) must.SliceEqOp(t, expected, dst)
})) }))
t.Run("Read ends before end of source", rapid.MakeCheck(func(t *rapid.T) { t.Run("Read returns io.EOF iff it overruns source", rapid.MakeCheck(func(t *rapid.T) {
src := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "src") data := rapid.SliceOfN(rapid.Byte(), 0, 100).Draw(t, "data")
endReadAt := rapid.IntRange(0, len(src)-1).Draw(t, "endReadAt") dst := pgen.SliceOfNZero[byte](0, 200).Draw(t, "dst")
pos := rapid.Uint64Range(0, uint64(endReadAt)).Draw(t, "pos") st := gigaparsec.MakeState(bytes.NewReader(data))
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) n, _, err := st.Read(dst)
t.Logf("n=%d", n)
test.EqOp(t, uint64(len(dst)), n) must.EqOp(t, min(len(data), len(dst)), int(n))
ptest.StateIsAt(t, next, pos+n) if len(dst) > len(data) || st.Pos() == uint64(len(data)) {
test.NoError(t, err) must.ErrorIs(t, err, io.EOF)
test.SliceEqOp(t, src[pos:pos+n], dst) } else {
})) must.NoError(t, err)
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) { t.Run("next state reads next input", rapid.MakeCheck(func(t *rapid.T) {
const maxLen = 100 const maxLen = 100
@ -123,6 +56,14 @@ func TestState(t *testing.T) {
n, _, _ := next.Read(dst) n, _, _ := next.Read(dst)
must.SliceEqOp(t, data[skip:skip+int(n)], dst[:n]) must.SliceEqOp(t, data[skip:skip+int(n)], dst[:n])
})) }))
t.Run("Read returns io.EOF if n is less than requested", rapid.MakeCheck(func(t *rapid.T) {
data := rapid.SliceOfN(rapid.Byte(), 0, 100).Draw(t, "data")
st := gigaparsec.MakeState(bytes.NewReader(data))
n, _, err := st.Read(make([]byte, len(data)+1))
test.ErrorIs(t, err, io.EOF)
test.EqOp(t, len(data), int(n))
}))
t.Run("At sets state position", rapid.MakeCheck(func(t *rapid.T) { t.Run("At sets state position", rapid.MakeCheck(func(t *rapid.T) {
data := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "data") data := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "data")
pos := rapid.Uint64Range(0, uint64(len(data)-1)).Draw(t, "pos") pos := rapid.Uint64Range(0, uint64(len(data)-1)).Draw(t, "pos")
@ -152,7 +93,7 @@ func TestState(t *testing.T) {
})) }))
t.Run("Read returns an error if the ReaderAt fails", rapid.MakeCheck(func(t *rapid.T) { t.Run("Read returns an error if the ReaderAt fails", rapid.MakeCheck(func(t *rapid.T) {
expectedErr := pgen.Error().Draw(t, "expectedErr") expectedErr := pgen.Error().Draw(t, "expectedErr")
startPos := rapid.Uint64Max(math.MaxInt64).Draw(t, "startPos") startPos := rapid.Uint64().Draw(t, "startPos")
dst := pgen.SliceOfNZero[byte](0, 100).Draw(t, "dst") dst := pgen.SliceOfNZero[byte](0, 100).Draw(t, "dst")
st := gigaparsec.MakeState(ptest.ErrReaderAt(expectedErr)).At(startPos) st := gigaparsec.MakeState(ptest.ErrReaderAt(expectedErr)).At(startPos)
n, next, err := st.Read(dst) n, next, err := st.Read(dst)
@ -161,42 +102,3 @@ func TestState(t *testing.T) {
test.Zero(t, n) 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)
}))
}