diff options
Diffstat (limited to 'spec/parser_test.go')
-rw-r--r-- | spec/parser_test.go | 153 |
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) + } +} |