mirror of https://github.com/golang/go.git
exp/types/staging: test drivers
This code has been reviewed before. The most significant change is to check_test which now can handle more than one error at the same error position (due to spurious errors - should not happen in praxis once error handling has been fine-tuned). This change makes check_test easier to use during development. R=rsc CC=golang-dev https://golang.org/cl/6584057
This commit is contained in:
parent
7f710c2de9
commit
328f0e7f2e
|
|
@ -0,0 +1,257 @@
|
|||
// Copyright 2011 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 typechecker test harness. The packages specified
|
||||
// in tests are typechecked. Error messages reported by the typechecker are
|
||||
// compared against the error messages expected in the test files.
|
||||
//
|
||||
// Expected errors are indicated in the test files by putting a comment
|
||||
// of the form /* ERROR "rx" */ immediately following an offending token.
|
||||
// The harness will verify that an error matching the regular expression
|
||||
// rx is reported at that source position. Consecutive comments may be
|
||||
// used to indicate multiple errors for the same token position.
|
||||
//
|
||||
// For instance, the following test file indicates that a "not declared"
|
||||
// error should be reported for the undeclared variable x:
|
||||
//
|
||||
// package p
|
||||
// func f() {
|
||||
// _ = x /* ERROR "not declared" */ + 1
|
||||
// }
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var listErrors = flag.Bool("list", false, "list errors")
|
||||
|
||||
func init() {
|
||||
// declare builtins for testing
|
||||
def(ast.Fun, "assert").Type = &builtin{aType, _Assert, "assert", 1, false, true}
|
||||
def(ast.Fun, "trace").Type = &builtin{aType, _Trace, "trace", 0, true, true}
|
||||
}
|
||||
|
||||
// The test filenames do not end in .go so that they are invisible
|
||||
// to gofmt since they contain comments that must not change their
|
||||
// positions relative to surrounding tokens.
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
files []string
|
||||
}{
|
||||
{"decls0", []string{"testdata/decls0.src"}},
|
||||
{"decls1", []string{"testdata/decls1.src"}},
|
||||
{"decls2", []string{"testdata/decls2a.src", "testdata/decls2b.src"}},
|
||||
{"const0", []string{"testdata/const0.src"}},
|
||||
{"expr0", []string{"testdata/expr0.src"}},
|
||||
{"expr1", []string{"testdata/expr1.src"}},
|
||||
{"expr2", []string{"testdata/expr2.src"}},
|
||||
{"expr3", []string{"testdata/expr3.src"}},
|
||||
{"builtins", []string{"testdata/builtins.src"}},
|
||||
{"conversions", []string{"testdata/conversions.src"}},
|
||||
{"stmt0", []string{"testdata/stmt0.src"}},
|
||||
}
|
||||
|
||||
var fset = token.NewFileSet()
|
||||
|
||||
func getFile(filename string) (file *token.File) {
|
||||
fset.Iterate(func(f *token.File) bool {
|
||||
if f.Name() == filename {
|
||||
file = f
|
||||
return false // end iteration
|
||||
}
|
||||
return true
|
||||
})
|
||||
return file
|
||||
}
|
||||
|
||||
func getPos(filename string, offset int) token.Pos {
|
||||
if f := getFile(filename); f != nil {
|
||||
return f.Pos(offset)
|
||||
}
|
||||
return token.NoPos
|
||||
}
|
||||
|
||||
func parseFiles(t *testing.T, testname string, filenames []string) (map[string]*ast.File, error) {
|
||||
files := make(map[string]*ast.File)
|
||||
var errors scanner.ErrorList
|
||||
for _, filename := range filenames {
|
||||
if _, exists := files[filename]; exists {
|
||||
t.Fatalf("%s: duplicate file %s", testname, filename)
|
||||
}
|
||||
file, err := parser.ParseFile(fset, filename, nil, parser.DeclarationErrors)
|
||||
if file == nil {
|
||||
t.Fatalf("%s: could not parse file %s", testname, filename)
|
||||
}
|
||||
files[filename] = file
|
||||
if err != nil {
|
||||
// if the parser returns a non-scanner.ErrorList error
|
||||
// the file couldn't be read in the first place and
|
||||
// file == nil; in that case we shouldn't reach here
|
||||
errors = append(errors, err.(scanner.ErrorList)...)
|
||||
}
|
||||
|
||||
}
|
||||
return files, errors
|
||||
}
|
||||
|
||||
// ERROR comments must be of the form /* ERROR "rx" */ and rx is
|
||||
// a regular expression that matches the expected error message.
|
||||
//
|
||||
var errRx = regexp.MustCompile(`^/\* *ERROR *"([^"]*)" *\*/$`)
|
||||
|
||||
// expectedErrors collects the regular expressions of ERROR comments found
|
||||
// in files and returns them as a map of error positions to error messages.
|
||||
//
|
||||
func expectedErrors(t *testing.T, testname string, files map[string]*ast.File) map[token.Pos][]string {
|
||||
errors := make(map[token.Pos][]string)
|
||||
|
||||
for filename := range files {
|
||||
src, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: could not read %s", testname, filename)
|
||||
}
|
||||
|
||||
var s scanner.Scanner
|
||||
// file was parsed already - do not add it again to the file
|
||||
// set otherwise the position information returned here will
|
||||
// not match the position information collected by the parser
|
||||
s.Init(getFile(filename), src, nil, scanner.ScanComments)
|
||||
var prev token.Pos // position of last non-comment, non-semicolon token
|
||||
|
||||
scanFile:
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
switch tok {
|
||||
case token.EOF:
|
||||
break scanFile
|
||||
case token.COMMENT:
|
||||
s := errRx.FindStringSubmatch(lit)
|
||||
if len(s) == 2 {
|
||||
list := errors[prev]
|
||||
errors[prev] = append(list, string(s[1]))
|
||||
}
|
||||
case token.SEMICOLON:
|
||||
// ignore automatically inserted semicolon
|
||||
if lit == "\n" {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
prev = pos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func eliminate(t *testing.T, expected map[token.Pos][]string, errors error) {
|
||||
if *listErrors || errors == nil {
|
||||
return
|
||||
}
|
||||
for _, error := range errors.(scanner.ErrorList) {
|
||||
// error.Pos is a token.Position, but we want
|
||||
// a token.Pos so we can do a map lookup
|
||||
pos := getPos(error.Pos.Filename, error.Pos.Offset)
|
||||
list := expected[pos]
|
||||
index := -1 // list index of matching message, if any
|
||||
// we expect one of the messages in list to match the error at pos
|
||||
for i, msg := range list {
|
||||
rx, err := regexp.Compile(msg)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", error.Pos, err)
|
||||
continue
|
||||
}
|
||||
if match := rx.MatchString(error.Msg); match {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index >= 0 {
|
||||
// eliminate from list
|
||||
n := len(list) - 1
|
||||
if n > 0 {
|
||||
// not the last entry - swap in last element and shorten list by 1
|
||||
list[index] = list[n]
|
||||
expected[pos] = list[:n]
|
||||
} else {
|
||||
// last entry - remove list from map
|
||||
delete(expected, pos)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: no error expected: %q", error.Pos, error.Msg)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkFiles(t *testing.T, testname string, testfiles []string) {
|
||||
// TODO(gri) Eventually all these different phases should be
|
||||
// subsumed into a single function call that takes
|
||||
// a set of files and creates a fully resolved and
|
||||
// type-checked AST.
|
||||
|
||||
files, err := parseFiles(t, testname, testfiles)
|
||||
|
||||
// we are expecting the following errors
|
||||
// (collect these after parsing the files so that
|
||||
// they are found in the file set)
|
||||
errors := expectedErrors(t, testname, files)
|
||||
|
||||
// verify errors returned by the parser
|
||||
eliminate(t, errors, err)
|
||||
|
||||
// verify errors returned after resolving identifiers
|
||||
pkg, err := ast.NewPackage(fset, files, GcImport, Universe)
|
||||
eliminate(t, errors, err)
|
||||
|
||||
// verify errors returned by the typechecker
|
||||
var list scanner.ErrorList
|
||||
errh := func(pos token.Pos, msg string) {
|
||||
list.Add(fset.Position(pos), msg)
|
||||
}
|
||||
err = Check(fset, pkg, errh, nil)
|
||||
eliminate(t, errors, list)
|
||||
|
||||
if *listErrors {
|
||||
scanner.PrintError(os.Stdout, err)
|
||||
return
|
||||
}
|
||||
|
||||
// there should be no expected errors left
|
||||
if len(errors) > 0 {
|
||||
t.Errorf("%s: %d errors not reported:", testname, len(errors))
|
||||
for pos, msg := range errors {
|
||||
t.Errorf("%s: %s\n", fset.Position(pos), msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
// For easy debugging w/o changing the testing code,
|
||||
// if there is a local test file, only test that file.
|
||||
const testfile = "testdata/test.go"
|
||||
if fi, err := os.Stat(testfile); err == nil && !fi.IsDir() {
|
||||
fmt.Printf("WARNING: Testing only %s (remove it to run all tests)\n", testfile)
|
||||
checkFiles(t, testfile, []string{testfile})
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, run all the tests.
|
||||
for _, test := range tests {
|
||||
checkFiles(t, test.name, test.files)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var sources = []string{
|
||||
`package p
|
||||
import "fmt"
|
||||
import "math"
|
||||
const pi = math.Pi
|
||||
func sin(x float64) float64 {
|
||||
return math.Sin(x)
|
||||
}
|
||||
var Println = fmt.Println
|
||||
`,
|
||||
`package p
|
||||
import "fmt"
|
||||
func f() string {
|
||||
return fmt.Sprintf("%d", g())
|
||||
}
|
||||
`,
|
||||
`package p
|
||||
import . "go/parser"
|
||||
func g() Mode { return ImportsOnly }`,
|
||||
}
|
||||
|
||||
var pkgnames = []string{
|
||||
"fmt",
|
||||
"go/parser",
|
||||
"math",
|
||||
}
|
||||
|
||||
// ResolveQualifiedIdents resolves the selectors of qualified
|
||||
// identifiers by associating the correct ast.Object with them.
|
||||
// TODO(gri): Eventually, this functionality should be subsumed
|
||||
// by Check.
|
||||
//
|
||||
func ResolveQualifiedIdents(fset *token.FileSet, pkg *ast.Package) error {
|
||||
var errors scanner.ErrorList
|
||||
|
||||
findObj := func(pkg *ast.Object, name *ast.Ident) *ast.Object {
|
||||
scope := pkg.Data.(*ast.Scope)
|
||||
obj := scope.Lookup(name.Name)
|
||||
if obj == nil {
|
||||
errors.Add(fset.Position(name.Pos()), fmt.Sprintf("no %s in package %s", name.Name, pkg.Name))
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
ast.Inspect(pkg, func(n ast.Node) bool {
|
||||
if s, ok := n.(*ast.SelectorExpr); ok {
|
||||
if x, ok := s.X.(*ast.Ident); ok && x.Obj != nil && x.Obj.Kind == ast.Pkg {
|
||||
// find selector in respective package
|
||||
s.Sel.Obj = findObj(x.Obj, s.Sel)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return errors.Err()
|
||||
}
|
||||
|
||||
func TestResolveQualifiedIdents(t *testing.T) {
|
||||
// parse package files
|
||||
fset := token.NewFileSet()
|
||||
files := make(map[string]*ast.File)
|
||||
for i, src := range sources {
|
||||
filename := fmt.Sprintf("file%d", i)
|
||||
f, err := parser.ParseFile(fset, filename, src, parser.DeclarationErrors)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files[filename] = f
|
||||
}
|
||||
|
||||
// resolve package AST
|
||||
pkg, err := ast.NewPackage(fset, files, GcImport, Universe)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// check that all packages were imported
|
||||
for _, name := range pkgnames {
|
||||
if pkg.Imports[name] == nil {
|
||||
t.Errorf("package %s not imported", name)
|
||||
}
|
||||
}
|
||||
|
||||
// check that there are no top-level unresolved identifiers
|
||||
for _, f := range pkg.Files {
|
||||
for _, x := range f.Unresolved {
|
||||
t.Errorf("%s: unresolved global identifier %s", fset.Position(x.Pos()), x.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// resolve qualified identifiers
|
||||
if err := ResolveQualifiedIdents(fset, pkg); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// check that qualified identifiers are resolved
|
||||
ast.Inspect(pkg, func(n ast.Node) bool {
|
||||
if s, ok := n.(*ast.SelectorExpr); ok {
|
||||
if x, ok := s.X.(*ast.Ident); ok {
|
||||
if x.Obj == nil {
|
||||
t.Errorf("%s: unresolved qualified identifier %s", fset.Position(x.Pos()), x.Name)
|
||||
return false
|
||||
}
|
||||
if x.Obj.Kind == ast.Pkg && s.Sel != nil && s.Sel.Obj == nil {
|
||||
t.Errorf("%s: unresolved selector %s", fset.Position(s.Sel.Pos()), s.Sel.Name)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
// Copyright 2012 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 contains tests verifying the types associated with an AST after
|
||||
// type checking.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const filename = "<src>"
|
||||
|
||||
func makePkg(t *testing.T, src string) (*ast.Package, error) {
|
||||
file, err := parser.ParseFile(fset, filename, src, parser.DeclarationErrors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := map[string]*ast.File{filename: file}
|
||||
pkg, err := ast.NewPackage(fset, files, GcImport, Universe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := Check(fset, pkg, nil, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
type testEntry struct {
|
||||
src, str string
|
||||
}
|
||||
|
||||
// dup returns a testEntry where both src and str are the same.
|
||||
func dup(s string) testEntry {
|
||||
return testEntry{s, s}
|
||||
}
|
||||
|
||||
var testTypes = []testEntry{
|
||||
// basic types
|
||||
dup("int"),
|
||||
dup("float32"),
|
||||
dup("string"),
|
||||
|
||||
// arrays
|
||||
dup("[10]int"),
|
||||
|
||||
// slices
|
||||
dup("[]int"),
|
||||
dup("[][]int"),
|
||||
|
||||
// structs
|
||||
dup("struct{}"),
|
||||
dup("struct{x int}"),
|
||||
{`struct {
|
||||
x, y int
|
||||
z float32 "foo"
|
||||
}`, `struct{x int; y int; z float32 "foo"}`},
|
||||
{`struct {
|
||||
string
|
||||
elems []T
|
||||
}`, `struct{string; elems []T}`},
|
||||
|
||||
// pointers
|
||||
dup("*int"),
|
||||
dup("***struct{}"),
|
||||
dup("*struct{a int; b float32}"),
|
||||
|
||||
// functions
|
||||
dup("func()"),
|
||||
dup("func(x int)"),
|
||||
{"func(x, y int)", "func(x int, y int)"},
|
||||
{"func(x, y int, z string)", "func(x int, y int, z string)"},
|
||||
dup("func(int)"),
|
||||
{"func(int, string, byte)", "func(int, string, byte)"},
|
||||
|
||||
dup("func() int"),
|
||||
{"func() (string)", "func() string"},
|
||||
dup("func() (u int)"),
|
||||
{"func() (u, v int, w string)", "func() (u int, v int, w string)"},
|
||||
|
||||
dup("func(int) string"),
|
||||
dup("func(x int) string"),
|
||||
dup("func(x int) (u string)"),
|
||||
{"func(x, y int) (u string)", "func(x int, y int) (u string)"},
|
||||
|
||||
dup("func(...int) string"),
|
||||
dup("func(x ...int) string"),
|
||||
dup("func(x ...int) (u string)"),
|
||||
{"func(x, y ...int) (u string)", "func(x int, y ...int) (u string)"},
|
||||
|
||||
// interfaces
|
||||
dup("interface{}"),
|
||||
dup("interface{m()}"),
|
||||
{`interface{
|
||||
m(int) float32
|
||||
String() string
|
||||
}`, `interface{String() string; m(int) float32}`}, // methods are sorted
|
||||
// TODO(gri) add test for interface w/ anonymous field
|
||||
|
||||
// maps
|
||||
dup("map[string]int"),
|
||||
{"map[struct{x, y int}][]byte", "map[struct{x int; y int}][]byte"},
|
||||
|
||||
// channels
|
||||
dup("chan int"),
|
||||
dup("chan<- func()"),
|
||||
dup("<-chan []func() int"),
|
||||
}
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
for _, test := range testTypes {
|
||||
src := "package p; type T " + test.src
|
||||
pkg, err := makePkg(t, src)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %s", src, err)
|
||||
continue
|
||||
}
|
||||
typ := underlying(pkg.Scope.Lookup("T").Type.(Type))
|
||||
str := typeString(typ)
|
||||
if str != test.str {
|
||||
t.Errorf("%s: got %s, want %s", test.src, str, test.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testExprs = []testEntry{
|
||||
// basic type literals
|
||||
dup("x"),
|
||||
dup("true"),
|
||||
dup("42"),
|
||||
dup("3.1415"),
|
||||
dup("2.71828i"),
|
||||
dup(`'a'`),
|
||||
dup(`"foo"`),
|
||||
dup("`bar`"),
|
||||
|
||||
// arbitrary expressions
|
||||
dup("&x"),
|
||||
dup("*x"),
|
||||
dup("(x)"),
|
||||
dup("x + y"),
|
||||
dup("x + y * 10"),
|
||||
dup("s.foo"),
|
||||
dup("s[0]"),
|
||||
dup("s[x:y]"),
|
||||
dup("s[:y]"),
|
||||
dup("s[x:]"),
|
||||
dup("s[:]"),
|
||||
dup("f(1, 2.3)"),
|
||||
dup("-f(10, 20)"),
|
||||
dup("f(x + y, +3.1415)"),
|
||||
{"func(a, b int) {}", "(func literal)"},
|
||||
{"func(a, b int) []int {}()[x]", "(func literal)()[x]"},
|
||||
{"[]int{1, 2, 3}", "(composite literal)"},
|
||||
{"[]int{1, 2, 3}[x:]", "(composite literal)[x:]"},
|
||||
{"x.([]string)", "x.(...)"},
|
||||
}
|
||||
|
||||
func TestExprs(t *testing.T) {
|
||||
for _, test := range testExprs {
|
||||
src := "package p; var _ = " + test.src + "; var (x, y int; s []string; f func(int, float32))"
|
||||
pkg, err := makePkg(t, src)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %s", src, err)
|
||||
continue
|
||||
}
|
||||
expr := pkg.Files[filename].Decls[0].(*ast.GenDecl).Specs[0].(*ast.ValueSpec).Values[0]
|
||||
str := exprString(expr)
|
||||
if str != test.str {
|
||||
t.Errorf("%s: got %s, want %s", test.src, str, test.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue