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 not requiring so much Pos() when making messages
Think about changing "consume" to "commit"
Rename Seq2 to Seq Rename Seq2 to Seq
Document Seq Document Seq
Should MakeState be private now that there's Run? 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 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. // 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] { 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 values []Out
var consumed bool var consumed bool
next := s currState := state
for { for {
result, err := p(next) result, err := p(currState)
if err != nil { if err != nil {
return Result[In, []Out]{}, fmt.Errorf("AtLeastN: %w", err) return Result[In, []Out]{}, fmt.Errorf("AtLeastN: %w", err)
} }
consumed = consumed || result.Consumed() consumed = consumed || result.Consumed()
var value Out var value Out
var success bool var success bool
success, value, next = result.Status() success, value, nextState := result.Status()
if !success { if !success {
if len(values) >= minCount { 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 return Fail[In, []Out](consumed, result.Message()), nil
} }
currState = nextState
values = append(values, value) values = append(values, value)
} }
} }

View File

@ -4,6 +4,8 @@ package gigaparsec_test
import ( import (
"bytes" "bytes"
"errors"
"io"
"testing" "testing"
"git.codemonkeysoftware.net/b/gigaparsec" "git.codemonkeysoftware.net/b/gigaparsec"
@ -138,24 +140,69 @@ func TestEnd(t *testing.T) {
} }
func TestRepeat(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 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") minCount := rapid.IntRange(0, maxParses).Draw(t, "minCount")
successes := rapid.IntRange(0, maxParses).Draw(t, "successes") successes := rapid.IntRange(0, maxParses).Draw(t, "successes")
shouldSucceed := successes >= minCount
input := append(ptest.SliceOfN(good, successes), bad) input := append(ptest.SliceOfN(true, successes), false)
p := gigaparsec.Repeat(minCount, gigaparsec.Match(good)) p := gigaparsec.Repeat(minCount, gigaparsec.Match(true))
result, err := p(gigaparsec.MakeState(bytes.NewReader(input))) result, err := p(gigaparsec.MakeState(gigaparsec.SliceReaderAt[bool](input)))
must.NoError(t, err) must.NoError(t, err)
success, _, _ := result.Status() success, _, next := result.Status()
test.EqOp(t, successes >= minCount, success, test.Sprint("expected successes ≥ minCount")) test.EqOp(t, shouldSucceed, success)
})) if success {
t.Run("consumes iff at least one application consumes", Todo) test.EqOp(t, uint64(successes), next.Pos())
t.Run("fails on error", Todo) }
t.Run("position is unchanged on failure", Todo) }))
t.Run("position follows last success on overall success", Todo) 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)
})
} }