diff --git a/cursor/cursor.go b/cursor/cursor.go index c998088..4b9303d 100644 --- a/cursor/cursor.go +++ b/cursor/cursor.go @@ -17,8 +17,9 @@ type Cursor[Datum any] interface { // source. It returns the number of data it read and a new Cursor for // the position at which the read ended, or an error if the read failed. // All calls to a given Cursor will return data from the same position. - // If n < len(dst), Read will return an error explaining why it read fewer - // bytes than requested. If Read tried to read past the end of the source, + // If n < len(dst) or if the cursor's position is at the end of the data source, + // Read will return an error explaining why it read fewer bytes than requested. + // If the error was due to the cursor reaching the end of the data source, // err will be io.EOF. Read(dst []Datum) (n uint64, next Cursor[Datum], err error) @@ -31,9 +32,17 @@ type SliceCursor[Datum any] struct { offset uint64 } -func NewSlice[Datum any]([]Datum) SliceCursor[Datum] { panic("not implemented") } +func NewSlice[Datum any](data []Datum) SliceCursor[Datum] { + return SliceCursor[Datum]{ + data: data, + offset: 0, + } +} func (sc SliceCursor[Datum]) Read(dst []Datum) (n uint64, next Cursor[Datum], err error) { + if sc.offset == uint64(len(sc.data)) { + return 0, sc, io.EOF + } copied := copy(dst, sc.data[sc.offset:]) if copied < len(dst) { err = io.EOF @@ -81,6 +90,9 @@ func NewString(s string) StringCursor { } func (sc StringCursor) Read(dst []byte) (n uint64, next Cursor[byte], err error) { + if sc.offset == uint64(len(sc.source)) { + return 0, sc, io.EOF + } copied := copy(dst, sc.source[sc.offset:]) if copied < len(dst) { err = io.EOF diff --git a/cursor/cursor_test.go b/cursor/cursor_test.go new file mode 100644 index 0000000..d963ebe --- /dev/null +++ b/cursor/cursor_test.go @@ -0,0 +1,92 @@ +package cursor_test + +import ( + "bytes" + "io" + "testing" + + "git.codemonkeysoftware.net/b/gigaparsec/cursor" + "github.com/shoenig/test" + "github.com/shoenig/test/must" + "pgregory.net/rapid" +) + +func Todo(t *testing.T) { + t.Errorf("TODO") +} + +func SliceOfNZero[T any](minLen, maxLen int) *rapid.Generator[[]T] { + return rapid.Map(rapid.IntRange(minLen, maxLen), func(n int) []T { + return make([]T, n) + }) +} + +func testCursor[C cursor.Cursor[byte]](t *testing.T, makeCursor func([]byte) C) { + t.Helper() + t.Run("cursor reads the same position every time", rapid.MakeCheck(func(t *rapid.T) { + data := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "data") + dst := SliceOfNZero[byte](0, len(data)-1).Draw(t, "dst") + expected := data[:len(dst)] + c := makeCursor(data) + + _, next, err := c.Read(dst) + must.NoError(t, err) + must.SliceEqOp(t, expected, dst) + + next.Read(dst) + _, _, err = c.Read(dst) + must.NoError(t, err) + must.SliceEqOp(t, expected, dst) + })) + t.Run("Read returns io.EOF iff it overruns source", rapid.MakeCheck(func(t *rapid.T) { + data := rapid.SliceOfN(rapid.Byte(), 0, 100).Draw(t, "data") + dst := SliceOfNZero[byte](0, 200).Draw(t, "dst") + c := makeCursor(data) + + n, _, err := c.Read(dst) + t.Logf("n=%d", n) + must.EqOp(t, min(len(data), len(dst)), int(n)) + if len(dst) > len(data) || c.Pos() == uint64(len(data)) { + must.ErrorIs(t, err, io.EOF) + } else { + must.NoError(t, err) + } + })) + t.Run("next cursor 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, "data") + c := makeCursor(data) + + _, next, err := c.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("Read returns an error if n is less than requested", rapid.MakeCheck(func(t *rapid.T) { + data := rapid.SliceOfN(rapid.Byte(), 0, 100).Draw(t, "data") + c := makeCursor(data) + + n, _, err := c.Read(make([]byte, len(data)+1)) + test.ErrorIs(t, err, io.EOF) + test.EqOp(t, len(data), int(n)) + })) +} + +func TestSliceCursor(t *testing.T) { + testCursor(t, cursor.NewSlice[byte]) +} + +func TestStringCursor(t *testing.T) { + testCursor(t, func(b []byte) cursor.StringCursor { + return cursor.NewString(string(b)) + }) +} + +func TestReaderAtCursor(t *testing.T) { + testCursor(t, func(b []byte) cursor.ReaderAtCursor { + return cursor.NewReaderAt(bytes.NewReader(b)) + }) +} diff --git a/cursor/helper.go b/cursor/helper.go index a5eaa72..ff946f6 100644 --- a/cursor/helper.go +++ b/cursor/helper.go @@ -6,7 +6,9 @@ import "io" // with limited backward seeking. type BufferedReaderAt struct{} -func NewBufferedReaderAt(r io.Reader, minBuffer uint64) *BufferedReaderAt +func NewBufferedReaderAt(r io.Reader, minBuffer uint64) *BufferedReaderAt { + return nil +} // ReadAt reads bytes from the underlying reader. If the offset is after // the end of the buffer, ReadAt will first read and ignore bytes from the @@ -15,7 +17,9 @@ func NewBufferedReaderAt(r io.Reader, minBuffer uint64) *BufferedReaderAt // // If your parser needs unlimited lookahead, you should probably // just read the whole input into a slice and use BytesCursor. -func (b *BufferedReaderAt) ReadAt(dst []byte, offset int64) (int, error) +func (b *BufferedReaderAt) ReadAt(dst []byte, offset int64) (int, error) { + return 0, nil +} // RuneReader is an io.RuneReader backed by a Cursor, for compatibility // with the regexp package. diff --git a/go.mod b/go.mod index 9e5d34e..7755b1d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,12 @@ module git.codemonkeysoftware.net/b/gigaparsec go 1.23 + +require ( + github.com/shoenig/test v1.9.1 + pgregory.net/rapid v1.1.0 +) + +require github.com/google/go-cmp v0.6.0 // indirect + +replace github.com/shoenig/test v1.9.1 => ../shoenig-test diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..93587e3 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=