diff --git a/TODO.txt b/TODO.txt index 848c55f..3df4ac3 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,4 @@ +Test State against both possible ReaderAt EOF behaviors Write Repeat tests Think about not requiring so much Pos() when making messages Rename Seq2 to Seq diff --git a/gigaparsec.go b/gigaparsec.go index 66cafb9..c5d7549 100644 --- a/gigaparsec.go +++ b/gigaparsec.go @@ -116,6 +116,22 @@ type ReaderAt[T any] interface { 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] { return State[In]{r: r} } diff --git a/state_test.go b/state_test.go index d385548..e6f3ce0 100644 --- a/state_test.go +++ b/state_test.go @@ -2,11 +2,13 @@ package gigaparsec_test import ( "bytes" + "cmp" "io" "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" @@ -102,3 +104,42 @@ func TestState(t *testing.T) { 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) + })) +}