aboutsummaryrefslogtreecommitdiff
path: root/spec/parser_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'spec/parser_test.go')
-rw-r--r--spec/parser_test.go153
1 files changed, 153 insertions, 0 deletions
diff --git a/spec/parser_test.go b/spec/parser_test.go
new file mode 100644
index 0000000..483c0b4
--- /dev/null
+++ b/spec/parser_test.go
@@ -0,0 +1,153 @@
+package spec
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestParse(t *testing.T) {
+ production := func(lhs string, alts ...*AlternativeNode) *ProductionNode {
+ return &ProductionNode{
+ LHS: lhs,
+ RHS: alts,
+ }
+ }
+ alternative := func(elems ...*ElementNode) *AlternativeNode {
+ return &AlternativeNode{
+ Elements: elems,
+ }
+ }
+ pattern := func(p string) *ElementNode {
+ return &ElementNode{
+ Pattern: p,
+ }
+ }
+
+ tests := []struct {
+ caption string
+ src string
+ ast *RootNode
+ synErr *SyntaxError
+ }{
+ {
+ caption: "single production is a valid grammar",
+ src: `a: "a";`,
+ ast: &RootNode{
+ Productions: []*ProductionNode{
+ production("a", alternative(pattern("a"))),
+ },
+ },
+ },
+ {
+ caption: "multiple productions are a valid grammar",
+ src: `
+a: "a";
+b: "b";
+c: "c";
+`,
+ ast: &RootNode{
+ Productions: []*ProductionNode{
+ production("a", alternative(pattern("a"))),
+ production("b", alternative(pattern("b"))),
+ production("c", alternative(pattern("c"))),
+ },
+ },
+ },
+ {
+ caption: "when a source contains an unknown token, the parser raises a syntax error",
+ src: `a: !;`,
+ synErr: synErrInvalidToken,
+ },
+ {
+ caption: "a grammar must have at least one production",
+ src: ``,
+ synErr: synErrNoProduction,
+ },
+ {
+ caption: "a production must have its name as the first element",
+ src: `: "a";`,
+ synErr: synErrNoProductionName,
+ },
+ {
+ caption: "':' must precede an alternative",
+ src: `a "a";`,
+ synErr: synErrNoColon,
+ },
+ {
+ caption: "an alternative must have at least one element",
+ src: `a:;`,
+ synErr: synErrNoElement,
+ },
+ {
+ caption: "';' must follow a production",
+ src: `a: "a"`,
+ synErr: synErrNoSemicolon,
+ },
+ {
+ caption: "';' can only appear at the end of a production",
+ src: `;`,
+ synErr: synErrNoProductionName,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.caption, func(t *testing.T) {
+ ast, err := Parse(strings.NewReader(tt.src))
+ if tt.synErr != nil {
+ if tt.synErr != err {
+ t.Fatalf("unexpected error; want: %v, got: %v", tt.synErr, err)
+ }
+ if ast != nil {
+ t.Fatalf("AST must be nil")
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if ast == nil {
+ t.Fatalf("AST must be non-nil")
+ }
+ testRootNode(t, ast, tt.ast)
+ }
+ })
+ }
+}
+
+func testRootNode(t *testing.T, root, expected *RootNode) {
+ t.Helper()
+ if len(root.Productions) != len(expected.Productions) {
+ t.Fatalf("unexpected length of productions; want: %v, got: %v", len(expected.Productions), len(root.Productions))
+ }
+ for i, prod := range root.Productions {
+ testProductionNode(t, prod, expected.Productions[i])
+ }
+}
+
+func testProductionNode(t *testing.T, prod, expected *ProductionNode) {
+ t.Helper()
+ if prod.LHS != expected.LHS {
+ t.Fatalf("unexpected LHS; want: %v, got: %v", expected.LHS, prod.LHS)
+ }
+ if len(prod.RHS) != len(expected.RHS) {
+ t.Fatalf("unexpected length of an RHS; want: %v, got: %v", len(expected.RHS), len(prod.RHS))
+ }
+ for i, alt := range prod.RHS {
+ testAlternativeNode(t, alt, expected.RHS[i])
+ }
+}
+
+func testAlternativeNode(t *testing.T, alt, expected *AlternativeNode) {
+ t.Helper()
+ if len(alt.Elements) != len(expected.Elements) {
+ t.Fatalf("unexpected length of elements; want: %v, got: %v", len(expected.Elements), len(alt.Elements))
+ }
+ for i, elem := range alt.Elements {
+ testElementNode(t, elem, expected.Elements[i])
+ }
+}
+
+func testElementNode(t *testing.T, elem, expected *ElementNode) {
+ t.Helper()
+ if elem.Pattern != expected.Pattern {
+ t.Fatalf("unexpected pattern; want: %v, got: %v", expected.Pattern, elem.Pattern)
+ }
+}