diff options
Diffstat (limited to 'tester/tester.go')
-rw-r--r-- | tester/tester.go | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/tester/tester.go b/tester/tester.go new file mode 100644 index 0000000..ef3ca61 --- /dev/null +++ b/tester/tester.go @@ -0,0 +1,177 @@ +package tester + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "runtime/debug" + "strings" + + "github.com/nihei9/vartan/driver" + gspec "github.com/nihei9/vartan/spec/grammar" + tspec "github.com/nihei9/vartan/spec/test" +) + +type TestResult struct { + TestCasePath string + Error error + Diffs []*tspec.TreeDiff +} + +func (r *TestResult) String() string { + if r.Error != nil { + const indent1 = " " + const indent2 = indent1 + indent1 + + msgLines := strings.Split(r.Error.Error(), "\n") + msg := fmt.Sprintf("Failed %v:\n%v%v", r.TestCasePath, indent1, strings.Join(msgLines, "\n"+indent1)) + if len(r.Diffs) == 0 { + return msg + } + var diffLines []string + for _, diff := range r.Diffs { + diffLines = append(diffLines, diff.Message) + diffLines = append(diffLines, fmt.Sprintf("%vexpected path: %v", indent1, diff.ExpectedPath)) + diffLines = append(diffLines, fmt.Sprintf("%vactual path: %v", indent1, diff.ActualPath)) + } + return fmt.Sprintf("%v\n%v%v", msg, indent2, strings.Join(diffLines, "\n"+indent2)) + } + return fmt.Sprintf("Passed %v", r.TestCasePath) +} + +type TestCaseWithMetadata struct { + TestCase *tspec.TestCase + FilePath string + Error error +} + +func ListTestCases(testPath string) []*TestCaseWithMetadata { + fi, err := os.Stat(testPath) + if err != nil { + return []*TestCaseWithMetadata{ + { + FilePath: testPath, + Error: err, + }, + } + } + if !fi.IsDir() { + c, err := parseTestCase(testPath) + return []*TestCaseWithMetadata{ + { + TestCase: c, + FilePath: testPath, + Error: err, + }, + } + } + + es, err := os.ReadDir(testPath) + if err != nil { + return []*TestCaseWithMetadata{ + { + FilePath: testPath, + Error: err, + }, + } + } + var cases []*TestCaseWithMetadata + for _, e := range es { + cs := ListTestCases(filepath.Join(testPath, e.Name())) + cases = append(cases, cs...) + } + return cases +} + +func parseTestCase(testCasePath string) (*tspec.TestCase, error) { + f, err := os.Open(testCasePath) + if err != nil { + return nil, err + } + defer f.Close() + return tspec.ParseTestCase(f) +} + +type Tester struct { + Grammar *gspec.CompiledGrammar + Cases []*TestCaseWithMetadata +} + +func (t *Tester) Run() []*TestResult { + var rs []*TestResult + for _, c := range t.Cases { + rs = append(rs, runTest(t.Grammar, c)) + } + return rs +} + +func runTest(g *gspec.CompiledGrammar, c *TestCaseWithMetadata) *TestResult { + var p *driver.Parser + var tb *driver.DefaulSyntaxTreeBuilder + { + gram := driver.NewGrammar(g) + toks, err := driver.NewTokenStream(g, bytes.NewReader(c.TestCase.Source)) + if err != nil { + return &TestResult{ + TestCasePath: c.FilePath, + Error: err, + } + } + tb = driver.NewDefaultSyntaxTreeBuilder() + p, err = driver.NewParser(toks, gram, driver.SemanticAction(driver.NewASTActionSet(gram, tb))) + if err != nil { + return &TestResult{ + TestCasePath: c.FilePath, + Error: err, + } + } + } + + err := p.Parse() + if err != nil { + return &TestResult{ + TestCasePath: c.FilePath, + Error: err, + } + } + + if tb.Tree() == nil { + var err error + if len(p.SyntaxErrors()) > 0 { + err = fmt.Errorf("parse tree was not generated: syntax error occurred") + } else { + // The parser should always generate a parse tree in the vartan-test command, so if there is no parse + // tree, it is a bug. We also include a stack trace in the error message to be sure. + err = fmt.Errorf("parse tree was not generated: no syntax error:\n%v", string(debug.Stack())) + } + return &TestResult{ + TestCasePath: c.FilePath, + Error: err, + } + } + + // When a parse tree exists, the test continues regardless of whether or not syntax errors occurred. + diffs := tspec.DiffTree(genTree(tb.Tree()).Fill(), c.TestCase.Output) + if len(diffs) > 0 { + return &TestResult{ + TestCasePath: c.FilePath, + Error: fmt.Errorf("output mismatch"), + Diffs: diffs, + } + } + return &TestResult{ + TestCasePath: c.FilePath, + } +} + +func genTree(dTree *driver.Node) *tspec.Tree { + var children []*tspec.Tree + if len(dTree.Children) > 0 { + children = make([]*tspec.Tree, len(dTree.Children)) + for i, c := range dTree.Children { + children[i] = genTree(c) + } + } + return tspec.NewTree(dTree.KindName, children...) +} |