Added cursor tests

This commit is contained in:
Brandon Dyck 2024-09-03 13:55:32 -06:00
parent b06454a4bc
commit 49bfcb7462
5 changed files with 126 additions and 5 deletions

View File

@ -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

92
cursor/cursor_test.go Normal file
View File

@ -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))
})
}

View File

@ -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.

9
go.mod
View File

@ -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

4
go.sum Normal file
View File

@ -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=