| // 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 ( |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/scanner" |
| "go/token" |
| "io/ioutil" |
| "os" |
| "regexp" |
| "testing" |
| ) |
| |
| // 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 |
| }{ |
| {"test0", []string{"testdata/test0.src"}}, |
| } |
| |
| var fset = token.NewFileSet() |
| |
| // TODO(gri) This functionality should be in token.Fileset. |
| func getFile(filename string) *token.File { |
| for f := range fset.Files() { |
| if f.Name() == filename { |
| return f |
| } |
| } |
| return nil |
| } |
| |
| // TODO(gri) This functionality should be in token.Fileset. |
| func getPos(filename string, offset int) token.Pos { |
| if f := getFile(filename); f != nil { |
| return f.Pos(offset) |
| } |
| return token.NoPos |
| } |
| |
| // TODO(gri) Need to revisit parser interface. We should be able to use parser.ParseFiles |
| // or a similar function instead. |
| 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 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 { |
| errors[prev] = string(s[1]) |
| } |
| default: |
| prev = pos |
| } |
| } |
| } |
| return errors |
| } |
| |
| func eliminate(t *testing.T, expected map[token.Pos]string, errors error) { |
| if 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 |
| // TODO(gri) Need to move scanner.Errors over |
| // to use token.Pos and file set info. |
| pos := getPos(error.Pos.Filename, error.Pos.Offset) |
| if msg, found := expected[pos]; found { |
| // we expect a message at pos; check if it matches |
| rx, err := regexp.Compile(msg) |
| if err != nil { |
| t.Errorf("%s: %v", error.Pos, err) |
| continue |
| } |
| if match := rx.MatchString(error.Msg); !match { |
| t.Errorf("%s: %q does not match %q", error.Pos, error.Msg, msg) |
| continue |
| } |
| // we have a match - eliminate this error |
| delete(expected, pos) |
| } else { |
| // To keep in mind when analyzing failed test output: |
| // If the same error position occurs multiple times in errors, |
| // this message will be triggered (because the first error at |
| // the position removes this position from the expected errors). |
| t.Errorf("%s: no (multiple?) error expected, but found: %s", error.Pos, error.Msg) |
| } |
| } |
| } |
| |
| func check(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, GcImporter, Universe) |
| eliminate(t, errors, err) |
| |
| // verify errors returned by the typechecker |
| _, err = Check(fset, pkg) |
| eliminate(t, errors, err) |
| |
| // 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 = "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) |
| check(t, testfile, []string{testfile}) |
| return |
| } |
| |
| // Otherwise, run all the tests. |
| for _, test := range tests { |
| check(t, test.name, test.files) |
| } |
| } |