328 lines
9.1 KiB
Go
328 lines
9.1 KiB
Go
package parsekit_test
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"git.makaay.nl/mauricem/go-parsekit"
|
|
)
|
|
|
|
func ExampleParser_usingAcceptedRunes() {
|
|
// Easy access to the parsekit definitions.
|
|
a := parsekit.A
|
|
|
|
matches := []string{}
|
|
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
for p.On(a.AnyRune).Accept() {
|
|
matches = append(matches, p.Result().String())
|
|
}
|
|
p.ExpectEndOfFile()
|
|
})
|
|
err := parser.Execute("¡Any will dö!")
|
|
|
|
fmt.Printf("Matches = %q, Error = %s\n", matches, err)
|
|
// Output:
|
|
// Matches = ["¡" "A" "n" "y" " " "w" "i" "l" "l" " " "d" "ö" "!"], Error = <nil>
|
|
}
|
|
|
|
func ExampleParser_usingTokens() {
|
|
// Easy access to the parsekit definitions.
|
|
c, a, tok := parsekit.C, parsekit.A, parsekit.T
|
|
|
|
var tokens []*parsekit.Token
|
|
var accepted string
|
|
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
if p.On(c.OneOrMore(tok.Rune("a rune", a.AnyRune))).Accept() {
|
|
tokens = p.Result().Tokens()
|
|
accepted = p.Result().String()
|
|
}
|
|
p.ExpectEndOfFile()
|
|
})
|
|
parser.Execute("¡Any will dö!")
|
|
|
|
fmt.Printf("Runes accepted: %q\n", accepted)
|
|
fmt.Printf("Token values: ")
|
|
for _, t := range tokens {
|
|
fmt.Printf("%c ", t.Value)
|
|
}
|
|
// Output:
|
|
// Runes accepted: "¡Any will dö!"
|
|
// Token values: ¡ A n y w i l l d ö !
|
|
}
|
|
|
|
func ExampleParseAPI_UnexpectedInput() {
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
p.Expects("a thing")
|
|
p.UnexpectedInput()
|
|
})
|
|
err := parser.Execute("Whatever, this parser will never be happy...")
|
|
fmt.Println(err.Full())
|
|
|
|
// Output:
|
|
// unexpected character 'W' (expected a thing) at start of file
|
|
}
|
|
|
|
func ExampleParseAPIOnAction_Accept() {
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
// When a case-insensitive match on "Yowza!" is found by the
|
|
// tokenizer, then Accept() will make the result available
|
|
// through ParseAPI.Result()
|
|
if p.On(parsekit.A.StrNoCase("Yowza!")).Accept() {
|
|
// Result.String() returns a string containing all
|
|
// accepted runes that were matched against.
|
|
fmt.Println(p.Result().String())
|
|
}
|
|
})
|
|
parser.Execute("YOWZA!")
|
|
|
|
// Output:
|
|
// YOWZA!
|
|
}
|
|
|
|
func ExampleParseAPIOnAction_Skip() {
|
|
var result string
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
for loop := true; loop; {
|
|
switch {
|
|
case p.On(parsekit.A.Rune('X')).Skip():
|
|
// NOOP, skip this rune
|
|
case p.On(parsekit.A.AnyRune).Accept():
|
|
result += p.Result().String()
|
|
default:
|
|
loop = false
|
|
}
|
|
}
|
|
})
|
|
parser.Execute("HXeXllXoXX, XXwoXrlXXXd!")
|
|
fmt.Println(result)
|
|
|
|
// Output:
|
|
// Hello, world!
|
|
}
|
|
|
|
func ExampleParseAPI_Stop() {
|
|
C, A := parsekit.C, parsekit.A
|
|
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
fmt.Printf("First word: ")
|
|
for p.On(C.Not(A.Space)).Accept() {
|
|
fmt.Printf("%s", p.Result())
|
|
}
|
|
p.Stop()
|
|
})
|
|
parser.Execute("Input with spaces")
|
|
|
|
// Output:
|
|
// First word: Input
|
|
}
|
|
|
|
func ExampleParseAPI_Stop_notCalledAndNoInputPending() {
|
|
C, A := parsekit.C, parsekit.A
|
|
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
fmt.Printf("Word: ")
|
|
for p.On(C.Not(A.Space)).Accept() {
|
|
fmt.Printf("%s", p.Result())
|
|
}
|
|
fmt.Printf("\n")
|
|
})
|
|
err := parser.Execute("Troglodyte")
|
|
fmt.Printf("Error is nil: %t\n", err == nil)
|
|
|
|
// Output:
|
|
// Word: Troglodyte
|
|
// Error is nil: true
|
|
}
|
|
|
|
func ExampleParseAPI_Stop_notCalledButInputPending() {
|
|
C, A := parsekit.C, parsekit.A
|
|
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
fmt.Printf("First word: ")
|
|
for p.On(C.Not(A.Space)).Accept() {
|
|
fmt.Printf("%s", p.Result())
|
|
}
|
|
fmt.Printf("\n")
|
|
})
|
|
err := parser.Execute("Input with spaces")
|
|
fmt.Printf("Error: %s\n", err.Full())
|
|
|
|
// Output:
|
|
// First word: Input
|
|
// Error: unexpected character ' ' (expected end of file) at line 1, column 6
|
|
}
|
|
|
|
func ExampleParseAPIOnAction_Stay() {
|
|
// Definition of a fantasy serial number format.
|
|
C, A := parsekit.C, parsekit.A
|
|
serialnr := C.Seq(A.Asterisk, A.ASCIIUpper, A.ASCIIUpper, A.Digits)
|
|
|
|
// This handler is able to handle serial numbers.
|
|
serialnrHandler := func(p *parsekit.ParseAPI) {
|
|
if p.On(serialnr).Accept() {
|
|
fmt.Println(p.Result().String())
|
|
}
|
|
}
|
|
|
|
// Start could function as a sort of dispatcher, handing over
|
|
// control to the correct ParseHandler function, based on the input.
|
|
start := func(p *parsekit.ParseAPI) {
|
|
if p.On(parsekit.A.Asterisk).Stay() {
|
|
p.Handle(serialnrHandler)
|
|
return
|
|
}
|
|
// ... other cases could go here ...
|
|
}
|
|
|
|
parser := parsekit.NewParser(start)
|
|
parser.Execute("#XX1234")
|
|
parser.Execute("*ay432566")
|
|
parser.Execute("*ZD987112")
|
|
|
|
// Output:
|
|
// *ZD987112
|
|
}
|
|
|
|
func TestGivenNullHandler_NewParserPanics(t *testing.T) {
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { parsekit.NewParser(nil) },
|
|
Regexp: true,
|
|
Expect: `parsekit\.NewParser\(\): NewParser\(\) called ` +
|
|
`with nil input at /.*/parser_test\.go:\d+`})
|
|
}
|
|
|
|
func TestGivenNullHandler_HandlePanics(t *testing.T) {
|
|
brokenParseHandler := func(p *parsekit.ParseAPI) {
|
|
p.Handle(nil)
|
|
}
|
|
parser := parsekit.NewParser(brokenParseHandler)
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { parser.Execute("") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI\.Handle\(\): Handle\(\) called with nil input ` +
|
|
`at /.*/parser_test\.go:\d+`})
|
|
}
|
|
func TestGivenNilTokenHandler_OnPanics(t *testing.T) {
|
|
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
p.On(nil)
|
|
})
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { p.Execute("") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI\.On\(\): On\(\) called with nil ` +
|
|
`tokenHandler argument at /.*/parser_test\.go:\d+`})
|
|
}
|
|
|
|
func TestGivenStoppedParser_HandlePanics(t *testing.T) {
|
|
otherHandler := func(p *parsekit.ParseAPI) {
|
|
panic("This is not the handler you're looking for")
|
|
}
|
|
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
p.Stop()
|
|
p.Handle(otherHandler)
|
|
})
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { p.Execute("") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI\.Handle\(\): Illegal call to Handle\(\) ` +
|
|
`at /.*/parser_test\.go:\d+: no calls allowed after ParseAPI\.Stop\(\)`})
|
|
}
|
|
|
|
func TestGivenParserWithErrorSet_HandlePanics(t *testing.T) {
|
|
otherHandler := func(p *parsekit.ParseAPI) {
|
|
panic("This is not the handler you're looking for")
|
|
}
|
|
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
p.Error("It ends here")
|
|
p.Handle(otherHandler)
|
|
})
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { p.Execute("") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI\.Handle\(\): Illegal call to Handle\(\) ` +
|
|
`at /.*/parser_test\.go:\d+: no calls allowed after ParseAPI\.Error\(\)`})
|
|
}
|
|
|
|
func TestGivenParserWithoutCallToAccept_ResultPanics(t *testing.T) {
|
|
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
p.Result()
|
|
})
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { p.Execute("") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI\.TokenResult\(\): TokenResult\(\) called at ` +
|
|
`/.*/parser_test.go:\d+ without calling ParseAPI.Accept\(\) on beforehand`})
|
|
}
|
|
|
|
func TestGivenParserWhichIsNotStopped_WithNoMoreInput_FallbackExpectEndOfFileKicksIn(t *testing.T) {
|
|
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {})
|
|
err := p.Execute("")
|
|
parsekit.AssertTrue(t, err == nil, "err")
|
|
}
|
|
|
|
func TestGivenParserWhichIsNotStopped_WithMoreInput_ProducesError(t *testing.T) {
|
|
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {})
|
|
err := p.Execute("x")
|
|
parsekit.AssertEqual(t, "unexpected character 'x' (expected end of file) at start of file", err.Full(), "err")
|
|
}
|
|
|
|
type parserWithLoop struct {
|
|
loopCounter int
|
|
}
|
|
|
|
func (l *parserWithLoop) first(p *parsekit.ParseAPI) {
|
|
p.On(parsekit.A.ASCII).Accept()
|
|
p.Handle(l.second)
|
|
}
|
|
|
|
func (l *parserWithLoop) second(p *parsekit.ParseAPI) {
|
|
p.On(parsekit.A.ASCII).Accept()
|
|
p.Handle(l.third)
|
|
}
|
|
|
|
func (l *parserWithLoop) third(p *parsekit.ParseAPI) {
|
|
if l.loopCounter++; l.loopCounter > 100 {
|
|
p.Error("Loop not detected by parsekit")
|
|
return
|
|
}
|
|
p.On(parsekit.A.ASCII).Accept()
|
|
p.Handle(l.first)
|
|
}
|
|
|
|
func TestGivenLoopingParserDefinition_ParserPanics(t *testing.T) {
|
|
looper := &parserWithLoop{}
|
|
parser := parsekit.NewParser(looper.first)
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { parser.Execute("Het houdt niet op, niet vanzelf") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI: Loop detected in parser at /.*/parser_test.go:\d+`})
|
|
}
|
|
|
|
// This test incorporates an actual loop bug that I dropped on myself and
|
|
// that I could not easily spot in my code. It sounded so logical:
|
|
// I want to get chunks of 5 chars from the input, so I simply loop on:
|
|
//
|
|
// p.On(c.Max(5, a.AnyRune))
|
|
//
|
|
// The problem here is that Max(5, ...) will also match when there is
|
|
// no more input, since Max(5, ---) is actually MinMax(0, 5, ...).
|
|
// Therefore the loop will never stop. Solving the loop was simple:
|
|
//
|
|
// p.On(c.MinMax(1, 5, a.AnyRune))
|
|
//
|
|
// Now the loop stops when the parser finds no more matching input data.
|
|
func TestGivenLoopingParserDefinition2_ParserPanics(t *testing.T) {
|
|
var c, a = parsekit.C, parsekit.A
|
|
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
|
|
for p.On(c.Max(5, a.AnyRune)).Accept() {
|
|
}
|
|
p.Stop()
|
|
})
|
|
parsekit.AssertPanic(t, parsekit.PanicT{
|
|
Function: func() { parser.Execute("This will end soon") },
|
|
Regexp: true,
|
|
Expect: `parsekit\.ParseAPI: Loop detected in parser at .*/parser_test.go:\d+`})
|
|
}
|