Compare commits

..

2 Commits

Author SHA1 Message Date
9c5e8fff0e Added Repeat tests 2024-10-16 11:58:12 -06:00
bfc9a9ae58 Test Repeat success and next state 2024-10-16 09:06:25 -06:00
3 changed files with 67 additions and 19 deletions

View File

@ -1,5 +1,5 @@
Write Repeat tests
Think about not requiring so much Pos() when making messages
Think about changing "consume" to "commit"
Rename Seq2 to Seq
Document Seq
Should MakeState be private now that there's Run?

View File

@ -379,25 +379,26 @@ func Pipe[In, Ignore, Through any](p Parser[In, Ignore]) func(Through) Parser[In
// It succeeds if and only if p succeeds at least minCount times.
// It consumes if and only if at least one of the applications of p consumes.
func Repeat[In, Out any](minCount int, p Parser[In, Out]) Parser[In, []Out] {
return func(s State[In]) (Result[In, []Out], error) {
return func(state State[In]) (Result[In, []Out], error) {
var values []Out
var consumed bool
next := s
currState := state
for {
result, err := p(next)
result, err := p(currState)
if err != nil {
return Result[In, []Out]{}, fmt.Errorf("AtLeastN: %w", err)
}
consumed = consumed || result.Consumed()
var value Out
var success bool
success, value, next = result.Status()
success, value, nextState := result.Status()
if !success {
if len(values) >= minCount {
return Succeed(consumed, values, next, MessageOK(s.Pos())), nil
return Succeed(consumed, values, currState, MessageOK(state.Pos())), nil
}
return Fail[In, []Out](consumed, result.Message()), nil
}
currState = nextState
values = append(values, value)
}
}

View File

@ -4,6 +4,8 @@ package gigaparsec_test
import (
"bytes"
"errors"
"io"
"testing"
"git.codemonkeysoftware.net/b/gigaparsec"
@ -138,24 +140,69 @@ func TestEnd(t *testing.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
t.Run("succeeds iff number of successes ≥ minCount", rapid.MakeCheck(func(t *rapid.T) {
minCount := rapid.IntRange(0, maxParses).Draw(t, "minCount")
successes := rapid.IntRange(0, maxParses).Draw(t, "successes")
shouldSucceed := successes >= minCount
input := append(ptest.SliceOfN(good, successes), bad)
p := gigaparsec.Repeat(minCount, gigaparsec.Match(good))
result, err := p(gigaparsec.MakeState(bytes.NewReader(input)))
input := append(ptest.SliceOfN(true, successes), false)
p := gigaparsec.Repeat(minCount, gigaparsec.Match(true))
result, err := p(gigaparsec.MakeState(gigaparsec.SliceReaderAt[bool](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)
success, _, next := result.Status()
test.EqOp(t, shouldSucceed, success)
if success {
test.EqOp(t, uint64(successes), next.Pos())
}
}))
t.Run("consumes iff at least one application consumes", rapid.MakeCheck(func(t *rapid.T) {
type Token struct {
Consume, Succeed bool
}
// p will succeed and consume based on the values in the parsed token.
var p gigaparsec.Parser[Token, Token] = func(s gigaparsec.State[Token]) (gigaparsec.Result[Token, Token], error) {
buf := make([]Token, 1)
_, next, err := s.Read(buf)
if errors.Is(err, io.EOF) {
return gigaparsec.Fail[Token, Token](false, gigaparsec.MessageEnd(s.Pos())), nil
}
if err != nil {
return gigaparsec.Result[Token, Token]{}, err
}
tok := buf[0]
if tok.Succeed {
return gigaparsec.Succeed(tok.Consume, tok, next, gigaparsec.MessageOK(s.Pos())), nil
} else {
return gigaparsec.Fail[Token, Token](tok.Consume, gigaparsec.MakeMessage(s.Pos(), "false", "true")), nil
}
}
input := rapid.Map(rapid.SliceOfN(rapid.Just(Token{Succeed: true}), 0, 100),
func(ts []Token) []Token { return append(ts, Token{}) }).Draw(t, "input")
consumeAt := rapid.Ptr(rapid.IntRange(0, len(input)-1), true).Draw(t, "consumeAt")
if consumeAt != nil {
input[*consumeAt].Consume = true
}
shouldConsume := consumeAt != nil
result, err := gigaparsec.Repeat(0, p)(gigaparsec.MakeState(gigaparsec.SliceReaderAt[Token](input)))
must.NoError(t, err)
test.EqOp(t, shouldConsume, result.Consumed())
}))
t.Run("does not consume on empty input", func(t *testing.T) {
p := gigaparsec.Repeat(0, gigaparsec.Match(0))
result, err := p(gigaparsec.MakeState(gigaparsec.SliceReaderAt[int](nil)))
must.NoError(t, err)
must.False(t, result.Consumed())
})
t.Run("fails on error", func(t *testing.T) {
expectedErr := errors.New("it broke")
p := gigaparsec.Repeat(0, gigaparsec.Match(byte(0)))
result, err := p(gigaparsec.MakeState(ptest.ErrReaderAt(expectedErr)))
succeeded, _, _ := result.Status()
test.ErrorIs(t, err, expectedErr)
test.False(t, succeeded)
})
}