Commit 328f0e7f authored by Robert Griesemer's avatar Robert Griesemer

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
parent 7f710c2d
// 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)
}
}
// 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
})
}
// 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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment