// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file implements a regression test harness for syntax errors.
// The files in the testdata directory are parsed and the reported
// errors are compared against the errors declared in those files.
//
// Errors are declared in place in the form of "error comments",
// just before (or on the same line as) the offending token.
//
// Error comments must be of the form // ERROR rx or /* ERROR rx */
// where rx is a regular expression that matches the reported error
// message. The rx text comprises the comment text after "ERROR ",
// with any white space around it stripped.
//
// If the line comment form is used, the reported error's line must
// match the line of the error comment.
//
// If the regular comment form is used, the reported error's position
// must match the position of the token immediately following the
// error comment. Thus, /* ERROR ... */ comments should appear
// immediately before the position where the error is reported.
//
// Currently, the test harness only supports one error comment per
// token. If multiple error comments appear before a token, only
// the last one is considered.
package syntax
import (
"flag"
"fmt"
"internal/testenv"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
const testdata = "testdata" // directory containing test files
var print = flag.Bool("print", false, "only print errors")
// A position represents a source position in the current file.
type position struct {
line, col uint
}
func (pos position) String() string {
return fmt.Sprintf("%d:%d", pos.line, pos.col)
}
func sortedPositions(m map[position]string) []position {
list := make([]position, len(m))
i := 0
for pos := range m {
list[i] = pos
i++
}
sort.Slice(list, func(i, j int) bool {
a, b := list[i], list[j]
return a.line < b.line || a.line == b.line && a.col < b.col
})
return list
}
// declaredErrors returns a map of source positions to error
// patterns, extracted from error comments in the given file.
// Error comments in the form of line comments use col = 0
// in their position.
func declaredErrors(t *testing.T, filename string) map[position]string {
f, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
defer f.Close()
declared := make(map[position]string)
var s scanner
var pattern string
s.init(f, func(line, col uint, msg string) {
// errors never start with '/' so they are automatically excluded here
switch {
case strings.HasPrefix(msg, "// ERROR "):
// we can't have another comment on the same line - just add it
declared[position{s.line, 0}] = strings.TrimSpace(msg[9:])
case strings.HasPrefix(msg, "/* ERROR "):
// we may have more comments before the next token - collect them
pattern = strings.TrimSpace(msg[9 : len(msg)-2])
}
}, comments)
// consume file
for {
s.next()
if pattern != "" {
declared[position{s.line, s.col}] = pattern
pattern = ""
}
if s.tok == _EOF {
break
}
}
return declared
}
func testSyntaxErrors(t *testing.T, filename string) {
declared := declaredErrors(t, filename)
if *print {
fmt.Println("Declared errors:")
for _, pos := range sortedPositions(declared) {
fmt.Printf("%s:%s: %s\n", filename, pos, declared[pos])
}
fmt.Println()
fmt.Println("Reported errors:")
}
f, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
defer f.Close()
ParseFile(filename, func(err error) {
e, ok := err.(Error)
if !ok {
return
}
if *print {
fmt.Println(err)
return
}
orig := position{e.Pos.Line(), e.Pos.Col()}
pos := orig
pattern, found := declared[pos]
if !found {
// try line comment (only line must match)
pos = position{e.Pos.Line(), 0}
pattern, found = declared[pos]
}
if found {
rx, err := regexp.Compile(pattern)
if err != nil {
t.Errorf("%s: %v", pos, err)
return
}
if match := rx.MatchString(e.Msg); !match {
t.Errorf("%s: %q does not match %q", pos, e.Msg, pattern)
return
}
// we have a match - eliminate this error
delete(declared, pos)
} else {
t.Errorf("%s: unexpected error: %s", orig, e.Msg)
}
}, nil, 0)
if *print {
fmt.Println()
return // we're done
}
// report expected but not reported errors
for pos, pattern := range declared {
t.Errorf("%s: missing error: %s", pos, pattern)
}
}
func TestSyntaxErrors(t *testing.T) {
testenv.MustHaveGoBuild(t) // we need access to source (testdata)
list, err := ioutil.ReadDir(testdata)
if err != nil {
t.Fatal(err)
}
for _, fi := range list {
name := fi.Name()
if !fi.IsDir() && !strings.HasPrefix(name, ".") {
testSyntaxErrors(t, filepath.Join(testdata, name))
}
}
}
|