gigaparsec/parser_test.go

162 lines
5.0 KiB
Go

// SPDX-License-Identifier: Unlicense
package gigaparsec_test
import (
"bytes"
"testing"
"git.codemonkeysoftware.net/b/gigaparsec"
ptest "git.codemonkeysoftware.net/b/gigaparsec/test"
"git.codemonkeysoftware.net/b/gigaparsec/test/generator"
"github.com/shoenig/test"
"github.com/shoenig/test/must"
"pgregory.net/rapid"
)
func Todo(t *testing.T) {
t.Errorf("TODO")
}
func not[T any](pred func(T) bool) func(T) bool {
return func(x T) bool { return !pred(x) }
}
func hasPrefix(prefix []byte) func([]byte) bool {
return func(b []byte) bool { return bytes.HasPrefix(b, prefix) }
}
func TestSlice(t *testing.T) {
assertParseFails := func(t rapid.TB, input []byte, p gigaparsec.Parser[byte, []byte]) {
t.Helper()
start := gigaparsec.MakeState(bytes.NewReader(input))
result, err := p(start)
must.NoError(t, err)
success, _, _ := result.Status()
test.False(t, success)
test.False(t, result.Consumed())
if t.Failed() {
t.FailNow()
}
}
t.Run("fails with wrong contents", rapid.MakeCheck(func(t *rapid.T) {
s := rapid.SliceOfN(rapid.Byte(), 1, -1).Draw(t, "s")
input := rapid.SliceOfN(rapid.Byte(), len(s), -1).
Filter(not(hasPrefix(s))).Draw(t, "input")
assertParseFails(t, input, gigaparsec.MatchSlice(s))
}))
t.Run("fails at end of input", rapid.MakeCheck(func(t *rapid.T) {
s := rapid.SliceOfN(rapid.Byte(), 1, -1).Draw(t, "s")
input := rapid.Map(rapid.IntRange(0, len(s)-1),
func(n int) []byte { return s[:n] }).Draw(t, "inputLen")
assertParseFails(t, input, gigaparsec.MatchSlice(s))
}))
t.Run("fails when read fails", rapid.MakeCheck(func(t *rapid.T) {
expectedErr := generator.Error().Draw(t, "expectedErr")
r := ptest.ErrReaderAt(expectedErr)
s := rapid.SliceOfN(rapid.Byte(), 0, 100).Draw(t, "s")
result, err := gigaparsec.MatchSlice(s)(gigaparsec.MakeState(r))
test.ErrorIs(t, err, expectedErr)
success, _, _ := result.Status()
test.False(t, success)
}))
t.Run("succeeds when contents match", rapid.MakeCheck(func(t *rapid.T) {
input := rapid.SliceOfN(rapid.Byte(), 1, -1).Draw(t, "input")
s := rapid.Map(rapid.IntRange(0, len(input)),
func(n int) []byte { return input[:n] }).Draw(t, "s")
start := gigaparsec.MakeState(bytes.NewReader(input))
result, err := gigaparsec.MatchSlice(s)(start)
must.NoError(t, err)
success, value, next := result.Status()
test.True(t, success)
test.True(t, result.Consumed())
test.SliceEqOp(t, s, value)
test.EqOp(t, uint64(len(s)), next.Pos())
}))
}
func TestChoose(t *testing.T) {
Todo(t)
}
func TestBind(t *testing.T) {
t.Run("fails without constructing second parser if the first parser fails", Todo)
t.Run("returns an error without constructing second parser if the first returns an error", Todo)
t.Run("fails if the second parser fails", Todo)
t.Run("returns an error if the second parser returns an error", Todo)
t.Run("succeeds if both parsers succeed", rapid.MakeCheck(func(t *rapid.T) {
// s := rapid.SliceOfN(rapid.Byte(), 0, 100)
t.Errorf("TODO")
}))
/*
If the first parser fails, then consumption depends on the first parser.
If the first parser succeeds, then bound parser consumes iff either parser succeeded.
For BindN:
If all parsers before i succeed, bound parser consumes if any parser before i consumed.
*/
t.Run("consumption on success", rapid.MakeCheck(func(t *rapid.T) {
makeParser := func(consume bool) gigaparsec.Parser[byte, struct{}] {
return func(s gigaparsec.State[byte]) (gigaparsec.Result[byte, struct{}], error) {
return gigaparsec.Succeed(consume, struct{}{}, s, gigaparsec.MessageOK(0)), nil
}
}
pConsume := rapid.Bool().Draw(t, "pShouldConsume")
qConsume := rapid.Bool().Draw(t, "qShouldConsume")
p := makeParser(pConsume)
q := func(struct{}) gigaparsec.Parser[byte, struct{}] { return makeParser(qConsume) }
result, err := gigaparsec.Bind(p, q)(gigaparsec.MakeState(bytes.NewReader(nil)))
must.NoError(t, err)
must.EqOp(t, pConsume || qConsume, result.Consumed())
}))
}
func TestReturn(t *testing.T) {
Todo(t)
}
func TestMap(t *testing.T) {
Todo(t)
}
func TestSatisfy(t *testing.T) {
Todo(t)
}
func Try(t *testing.T) {
Todo(t)
}
func TestLabel(t *testing.T) {
Todo(t)
}
func TestEnd(t *testing.T) {
Todo(t)
}
func TestRepeat(t *testing.T) {
t.Run("succeeds iff number of successes ≥ minCount", rapid.MakeCheck(func(t *rapid.T) {
const good byte = 'o'
const bad byte = 'x'
const maxParses = 100
minCount := rapid.IntRange(0, maxParses).Draw(t, "minCount")
successes := rapid.IntRange(0, maxParses).Draw(t, "successes")
input := append(ptest.SliceOfN(good, successes), bad)
p := gigaparsec.Repeat(minCount, gigaparsec.Match(good))
result, err := p(gigaparsec.MakeState(bytes.NewReader(input)))
must.NoError(t, err)
success, _, _ := result.Status()
test.EqOp(t, successes >= minCount, success, test.Sprint("expected successes ≥ minCount"))
}))
t.Run("consumes iff at least one application consumes", Todo)
t.Run("fails on error", Todo)
t.Run("position is unchanged on failure", Todo)
t.Run("position follows last success on overall success", Todo)
}