From: Robert Griesemer Date: Mon, 8 Oct 2012 01:00:56 +0000 (-0700) Subject: exp/types/staging: test drivers X-Git-Tag: go1.1rc2~2197 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=328f0e7f2efd9ad96b4c09119a18e622e74b3802;p=gostls13.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 --- diff --git a/src/pkg/exp/types/staging/check_test.go b/src/pkg/exp/types/staging/check_test.go new file mode 100644 index 0000000000..abcfcfb2cd --- /dev/null +++ b/src/pkg/exp/types/staging/check_test.go @@ -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) + } +} diff --git a/src/pkg/exp/types/staging/resolver_test.go b/src/pkg/exp/types/staging/resolver_test.go new file mode 100644 index 0000000000..4e9aa0938d --- /dev/null +++ b/src/pkg/exp/types/staging/resolver_test.go @@ -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 + }) +} diff --git a/src/pkg/exp/types/staging/types_test.go b/src/pkg/exp/types/staging/types_test.go new file mode 100644 index 0000000000..d6ddfabc31 --- /dev/null +++ b/src/pkg/exp/types/staging/types_test.go @@ -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 = "" + +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) + } + } +}