1
0
mirror of https://github.com/golang/go synced 2024-11-18 22:34:45 -07:00

go.tools/go/loader: allow loading of multiple test packages.

Now that go/types permits files to be added to a package
      incrementally (fixing bug 7114), this CL extends the loader
      to load and augment multiple test packages at once.

      TESTED:
      - go/loader/stdlib_test runs the type-checker on the entire
        standard library loaded from source in a single gulp, with
        each package augmented by tests.
      - Manually tested on:
        % ssadump -test -run unicode encoding/ascii85
            Both sets of tests are run (and pass).

LGTM=gri
R=gri
CC=golang-codereviews
https://golang.org/cl/61060043
This commit is contained in:
Alan Donovan 2014-03-11 15:41:57 -04:00
parent 2b8dd19c64
commit cd987a3c83
6 changed files with 253 additions and 95 deletions

View File

@ -12,6 +12,6 @@ import (
// PackageLocatorFunc exposes the address of parsePackageFiles to tests. // PackageLocatorFunc exposes the address of parsePackageFiles to tests.
// This is a temporary hack until we expose a proper PackageLocator interface. // This is a temporary hack until we expose a proper PackageLocator interface.
func PackageLocatorFunc() *func(ctxt *build.Context, fset *token.FileSet, path string, which string) ([]*ast.File, error) { func PackageLocatorFunc() *func(ctxt *build.Context, fset *token.FileSet, path string, which rune) ([]*ast.File, error) {
return &parsePackageFiles return &parsePackageFiles
} }

View File

@ -10,7 +10,6 @@ import (
"go/build" "go/build"
"go/token" "go/token"
"sort" "sort"
"strings"
"testing" "testing"
"code.google.com/p/go.tools/go/loader" "code.google.com/p/go.tools/go/loader"
@ -56,8 +55,8 @@ func TestLoadFromArgs(t *testing.T) {
for _, info := range prog.Created { for _, info := range prog.Created {
pkgnames = append(pkgnames, info.Pkg.Path()) pkgnames = append(pkgnames, info.Pkg.Path())
} }
// Only the first import path (currently) contributes tests. // All import paths may contribute tests.
if got, want := fmt.Sprint(pkgnames), "[fmt_test]"; got != want { if got, want := fmt.Sprint(pkgnames), "[fmt_test errors_test]"; got != want {
t.Errorf("Created: got %s, want %s", got, want) t.Errorf("Created: got %s, want %s", got, want)
} }
@ -67,7 +66,7 @@ func TestLoadFromArgs(t *testing.T) {
pkgnames = append(pkgnames, path) pkgnames = append(pkgnames, path)
} }
sort.Strings(pkgnames) sort.Strings(pkgnames)
// Only the first import path (currently) contributes tests. // All import paths may contribute tests.
if got, want := fmt.Sprint(pkgnames), "[errors fmt]"; got != want { if got, want := fmt.Sprint(pkgnames), "[errors fmt]"; got != want {
t.Errorf("Loaded: got %s, want %s", got, want) t.Errorf("Loaded: got %s, want %s", got, want)
} }
@ -124,8 +123,8 @@ func TestTransitivelyErrorFreeFlag(t *testing.T) {
// Temporary hack until we expose a principled PackageLocator. // Temporary hack until we expose a principled PackageLocator.
pfn := loader.PackageLocatorFunc() pfn := loader.PackageLocatorFunc()
saved := *pfn saved := *pfn
*pfn = func(_ *build.Context, fset *token.FileSet, path string, which string) (files []*ast.File, err error) { *pfn = func(_ *build.Context, fset *token.FileSet, path string, which rune) (files []*ast.File, err error) {
if !strings.Contains(which, "g") { if which != 'g' {
return nil, nil // no test/xtest files return nil, nil // no test/xtest files
} }
var contents string var contents string

View File

@ -68,6 +68,16 @@
// An AUGMENTED package is an importable package P plus all the // An AUGMENTED package is an importable package P plus all the
// *_test.go files with same 'package foo' declaration as P. // *_test.go files with same 'package foo' declaration as P.
// (go/build.Package calls these files TestFiles.) // (go/build.Package calls these files TestFiles.)
//
// The INITIAL packages are those specified in the configuration. A
// DEPENDENCY is a package loaded to satisfy an import in an initial
// package or another dependency.
//
package loader
// 'go test', in-package test files, and import cycles
// ---------------------------------------------------
//
// An external test package may depend upon members of the augmented // An external test package may depend upon members of the augmented
// package that are not in the unaugmented package, such as functions // package that are not in the unaugmented package, such as functions
// that expose internals. (See bufio/export_test.go for an example.) // that expose internals. (See bufio/export_test.go for an example.)
@ -77,24 +87,39 @@
// The import graph over n unaugmented packages must be acyclic; the // The import graph over n unaugmented packages must be acyclic; the
// import graph over n-1 unaugmented packages plus one augmented // import graph over n-1 unaugmented packages plus one augmented
// package must also be acyclic. ('go test' relies on this.) But the // package must also be acyclic. ('go test' relies on this.) But the
// import graph over n augmented packages may contain cycles, and // import graph over n augmented packages may contain cycles.
// currently, go/types is incapable of handling such inputs, so the
// loader will only augment (and create an external test package
// for) the first import path specified on the command-line.
// //
// The INITIAL packages are those specified in the configuration. A // First, all the (unaugmented) non-test packages and their
// DEPENDENCY is a package loaded to satisfy an import in an initial // dependencies are imported in the usual way; the loader reports an
// package or another dependency. // error if it detects an import cycle.
// //
package loader // Then, each package P for which testing is desired is augmented by
// the list P' of its in-package test files, by calling
// (*types.Checker).Files. This arrangement ensures that P' may
// reference definitions within P, but P may not reference definitions
// within P'. Furthermore, P' may import any other package, including
// ones that depend upon P, without an import cycle error.
//
// Consider two packages A and B, both of which have lists of
// in-package test files we'll call A' and B', and which have the
// following import graph edges:
// B imports A
// B' imports A
// A' imports B
// This last edge would be expected to create an error were it not
// for the special type-checking discipline above.
// Cycles of size greater than two are possible. For example:
// compress/bzip2/bzip2_test.go (package bzip2) imports "io/ioutil"
// io/ioutil/tempfile_test.go (package ioutil) imports "regexp"
// regexp/exec_test.go (package regexp) imports "compress/bzip2"
// TODO(adonovan): // TODO(adonovan):
// - (*Config).ParseFile is very handy, but feels like feature creep. // - (*Config).ParseFile is very handy, but feels like feature creep.
// (*Config).CreateFromFiles has a nasty precondition. // (*Config).CreateFromFiles has a nasty precondition.
// - Ideally some of this logic would move under the umbrella of
// go/types; see bug 7114.
// - s/path/importPath/g to avoid ambiguity with other meanings of // - s/path/importPath/g to avoid ambiguity with other meanings of
// "path": a file name, a colon-separated directory list. // "path": a file name, a colon-separated directory list.
// - cache the calls to build.Import so we don't do it three times per
// test package.
// - Thorough overhaul of package documentation. // - Thorough overhaul of package documentation.
import ( import (
@ -180,10 +205,7 @@ type Config struct {
// source. The map keys are package import paths, used to // source. The map keys are package import paths, used to
// locate the package relative to $GOROOT. The corresponding // locate the package relative to $GOROOT. The corresponding
// values indicate whether to augment the package by *_test.go // values indicate whether to augment the package by *_test.go
// files. // files in a second pass.
//
// Due to current type-checker limitations, at most one entry
// may be augmented (true).
ImportPkgs map[string]bool ImportPkgs map[string]bool
} }
@ -350,16 +372,8 @@ func (conf *Config) ImportWithTests(path string) error {
} }
conf.Import(path) conf.Import(path)
// TODO(adonovan): due to limitations of the current type
// checker design, we can augment at most one package.
for _, augmented := range conf.ImportPkgs {
if augmented {
return nil // don't attempt a second
}
}
// Load the external test package. // Load the external test package.
xtestFiles, err := parsePackageFiles(conf.build(), conf.fset(), path, "x") xtestFiles, err := parsePackageFiles(conf.build(), conf.fset(), path, 'x')
if err != nil { if err != nil {
return err return err
} }
@ -476,12 +490,29 @@ func (conf *Config) Load() (*Program, error) {
prog.Imported[path] = info prog.Imported[path] = info
} }
// Now augment those packages that need it.
for path, augment := range conf.ImportPkgs {
if augment {
ii := imp.imported[path]
// Find and create the actual package.
files, err := parsePackageFiles(imp.conf.build(), imp.conf.fset(), path, 't')
// Prefer the earlier error, if any.
if err != nil && ii.err == nil {
ii.err = err // e.g. parse error.
}
typeCheckFiles(ii.info, files...)
}
}
for _, create := range conf.CreatePkgs { for _, create := range conf.CreatePkgs {
path := create.Path path := create.Path
if create.Path == "" && len(create.Files) > 0 { if create.Path == "" && len(create.Files) > 0 {
path = create.Files[0].Name.Name path = create.Files[0].Name.Name
} }
prog.Created = append(prog.Created, imp.createPackage(path, create.Files...)) info := imp.newPackageInfo(path)
typeCheckFiles(info, create.Files...)
prog.Created = append(prog.Created, info)
} }
if len(prog.Imported)+len(prog.Created) == 0 { if len(prog.Imported)+len(prog.Created) == 0 {
@ -491,8 +522,11 @@ func (conf *Config) Load() (*Program, error) {
// Create infos for indirectly imported packages. // Create infos for indirectly imported packages.
// e.g. incomplete packages without syntax, loaded from export data. // e.g. incomplete packages without syntax, loaded from export data.
for _, obj := range prog.ImportMap { for _, obj := range prog.ImportMap {
if prog.AllPackages[obj] == nil { info := prog.AllPackages[obj]
if info == nil {
prog.AllPackages[obj] = &PackageInfo{Pkg: obj, Importable: true} prog.AllPackages[obj] = &PackageInfo{Pkg: obj, Importable: true}
} else {
info.checker = nil // finished
} }
} }
@ -567,10 +601,7 @@ func (conf *Config) build() *build.Context {
// doImport imports the package denoted by path. // doImport imports the package denoted by path.
// It implements the types.Importer signature. // It implements the types.Importer signature.
// //
// imports is the import map of the importing package, later // imports is the type-checker's package canonicalization map.
// accessible as types.Package.Imports(). If non-nil, doImport will
// update it to include this import. (It may be nil in recursive
// calls for prefetching.)
// //
// It returns an error if a package could not be created // It returns an error if a package could not be created
// (e.g. go/build or parse error), but type errors are reported via // (e.g. go/build or parse error), but type errors are reported via
@ -607,19 +638,12 @@ func (imp *importer) importPackage(path string) (*PackageInfo, error) {
// In preorder, initialize the map entry to a cycle // In preorder, initialize the map entry to a cycle
// error in case importPackage(path) is called again // error in case importPackage(path) is called again
// before the import is completed. // before the import is completed.
// TODO(adonovan): go/types should be responsible for
// reporting cycles; see bug 7114.
ii = &importInfo{err: fmt.Errorf("import cycle in package %s", path)} ii = &importInfo{err: fmt.Errorf("import cycle in package %s", path)}
imp.imported[path] = ii imp.imported[path] = ii
// Find and create the actual package. // Find and create the actual package.
if augment, ok := imp.conf.ImportPkgs[path]; ok || imp.conf.SourceImports { if _, ok := imp.conf.ImportPkgs[path]; ok || imp.conf.SourceImports {
which := "g" // load *.go files ii.info, ii.err = imp.importFromSource(path)
if augment {
which = "gt" // augment package by in-package *_test.go files
}
ii.info, ii.err = imp.importFromSource(path, which)
} else { } else {
ii.info, ii.err = imp.importFromBinary(path) ii.info, ii.err = imp.importFromBinary(path)
} }
@ -650,37 +674,49 @@ func (imp *importer) importFromBinary(path string) (*PackageInfo, error) {
} }
// importFromSource implements package loading by parsing Go source files // importFromSource implements package loading by parsing Go source files
// located by go/build. which indicates which files to include in the // located by go/build.
// package.
// //
func (imp *importer) importFromSource(path string, which string) (*PackageInfo, error) { func (imp *importer) importFromSource(path string) (*PackageInfo, error) {
files, err := parsePackageFiles(imp.conf.build(), imp.conf.fset(), path, which) files, err := parsePackageFiles(imp.conf.build(), imp.conf.fset(), path, 'g')
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Type-check the package. // Type-check the package.
return imp.createPackage(path, files...), nil info := imp.newPackageInfo(path)
typeCheckFiles(info, files...)
return info, nil
} }
// createPackage creates and type-checks a package from the specified // typeCheckFiles adds the specified files to info and type-checks them.
// list of parsed files, importing their dependencies. It returns a
// PackageInfo containing the resulting types.Package, the ASTs, and
// other type information.
//
// The order of files determines the package initialization order. // The order of files determines the package initialization order.
// It may be called multiple times.
// //
// path will be the resulting package's Path(). // Any error is stored in the info.TypeError field.
// For an ad-hoc package, this is not necessarily unique. func typeCheckFiles(info *PackageInfo, files ...*ast.File) {
// info.Files = append(info.Files, files...)
// The resulting package is accessible via AllPackages but is not if err := info.checker.Files(files); err != nil {
// importable, i.e. no 'import' spec can resolve to it. // Prefer the earlier error, if any.
// if info.TypeError == nil {
// createPackage never fails, but the resulting package may contain type info.TypeError = err
// errors; the first of these is recorded in PackageInfo.TypeError. }
// }
func (imp *importer) createPackage(path string, files ...*ast.File) *PackageInfo { }
func (imp *importer) newPackageInfo(path string) *PackageInfo {
// Use a copy of the types.Config so we can vary IgnoreFuncBodies.
tc := imp.conf.TypeChecker
tc.IgnoreFuncBodies = false
if f := imp.conf.TypeCheckFuncBodies; f != nil {
tc.IgnoreFuncBodies = !f(path)
}
if tc.Error == nil {
tc.Error = func(e error) { fmt.Fprintln(os.Stderr, e) }
}
tc.Import = imp.doImport // doImport wraps the user's importfn, effectively
pkg := types.NewPackage(path, "")
info := &PackageInfo{ info := &PackageInfo{
Files: files, Pkg: pkg,
Info: types.Info{ Info: types.Info{
Types: make(map[ast.Expr]types.TypeAndValue), Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object), Defs: make(map[*ast.Ident]types.Object),
@ -690,19 +726,7 @@ func (imp *importer) createPackage(path string, files ...*ast.File) *PackageInfo
Selections: make(map[*ast.SelectorExpr]*types.Selection), Selections: make(map[*ast.SelectorExpr]*types.Selection),
}, },
} }
info.checker = types.NewChecker(&tc, imp.conf.fset(), pkg, &info.Info)
// Use a copy of the types.Config so we can vary IgnoreFuncBodies. imp.prog.AllPackages[pkg] = info
tc := imp.conf.TypeChecker
tc.IgnoreFuncBodies = false
if f := imp.conf.TypeCheckFuncBodies; f != nil {
tc.IgnoreFuncBodies = !f(path)
}
if tc.Error == nil {
// TODO(adonovan): also save errors in the PackageInfo?
tc.Error = func(e error) { fmt.Fprintln(os.Stderr, e) }
}
tc.Import = imp.doImport // doImport wraps the user's importfn, effectively
info.Pkg, info.TypeError = tc.Check(path, imp.conf.fset(), files, &info.Info)
imp.prog.AllPackages[info.Pkg] = info
return info return info
} }

View File

@ -15,7 +15,7 @@ import (
// PackageInfo holds the ASTs and facts derived by the type-checker // PackageInfo holds the ASTs and facts derived by the type-checker
// for a single package. // for a single package.
// //
// Not mutated once constructed. // Not mutated once exposed via the API.
// //
type PackageInfo struct { type PackageInfo struct {
Pkg *types.Package Pkg *types.Package
@ -24,6 +24,10 @@ type PackageInfo struct {
Files []*ast.File // abstract syntax for the package's files Files []*ast.File // abstract syntax for the package's files
TypeError error // non-nil if the package had type errors TypeError error // non-nil if the package had type errors
types.Info // type-checker deductions. types.Info // type-checker deductions.
checker interface {
Files(files []*ast.File) error
} // transient type-checker state
} }
func (info *PackageInfo) String() string { func (info *PackageInfo) String() string {

135
go/loader/stdlib_test.go Normal file
View File

@ -0,0 +1,135 @@
// Copyright 2013 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 loader_test
// This file enumerates all packages beneath $GOROOT, loads them, plus
// their external tests if any, runs the type checker on them, and
// prints some summary information.
//
// Run test with GOMAXPROCS=8.
import (
"fmt"
"go/ast"
"go/token"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"code.google.com/p/go.tools/go/loader"
"code.google.com/p/go.tools/go/types"
)
func allPackages() []string {
var pkgs []string
root := filepath.Join(runtime.GOROOT(), "src/pkg") + string(os.PathSeparator)
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
// Prune the search if we encounter any of these names:
switch filepath.Base(path) {
case "testdata", ".hg":
return filepath.SkipDir
}
if info.IsDir() {
pkg := strings.TrimPrefix(path, root)
switch pkg {
case "builtin", "pkg":
return filepath.SkipDir // skip these subtrees
case "":
return nil // ignore root of tree
}
pkgs = append(pkgs, pkg)
}
return nil
})
return pkgs
}
func TestStdlib(t *testing.T) {
runtime.GC()
t0 := time.Now()
var memstats runtime.MemStats
runtime.ReadMemStats(&memstats)
alloc := memstats.Alloc
// Load, parse and type-check the program.
var conf loader.Config
for _, path := range allPackages() {
if err := conf.ImportWithTests(path); err != nil {
t.Error(err)
}
}
prog, err := conf.Load()
if err != nil {
t.Fatalf("Load failed: %v", err)
}
t1 := time.Now()
runtime.GC()
runtime.ReadMemStats(&memstats)
numPkgs := len(prog.AllPackages)
if want := 206; numPkgs < want {
t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want)
}
// Dump package members.
if false {
for pkg := range prog.AllPackages {
fmt.Printf("Package %s:\n", pkg.Path())
scope := pkg.Scope()
for _, name := range scope.Names() {
if ast.IsExported(name) {
fmt.Printf("\t%s\n", types.ObjectString(pkg, scope.Lookup(name)))
}
}
fmt.Println()
}
}
// Check that Test functions for io/ioutil, regexp and
// compress/bzip2 are all simultaneously present.
// (The apparent cycle formed when augmenting all three of
// these packages by their tests was the original motivation
// for reporting b/7114.)
//
// compress/bzip2.TestBitReader in bzip2_test.go imports io/ioutil
// io/ioutil.TestTempFile in tempfile_test.go imports regexp
// regexp.TestRE2Search in exec_test.go imports compress/bzip2
for _, test := range []struct{ pkg, fn string }{
{"io/ioutil", "TestTempFile"},
{"regexp", "TestRE2Search"},
{"compress/bzip2", "TestBitReader"},
} {
info := prog.Imported[test.pkg]
if info == nil {
t.Errorf("failed to load package %q", test.pkg)
continue
}
obj, _ := info.Pkg.Scope().Lookup(test.fn).(*types.Func)
if obj == nil {
t.Errorf("package %q has no func %q", test.pkg, test.fn)
continue
}
}
// Dump some statistics.
// determine line count
var lineCount int
prog.Fset.Iterate(func(f *token.File) bool {
lineCount += f.LineCount()
return true
})
t.Log("GOMAXPROCS: ", runtime.GOMAXPROCS(0))
t.Log("#Source lines: ", lineCount)
t.Log("Load/parse/typecheck: ", t1.Sub(t0))
t.Log("#MB: ", int64(memstats.Alloc-alloc)/1000000)
}

View File

@ -26,7 +26,7 @@ import (
// "PackageLocator" interface for use proprietary build sytems that // "PackageLocator" interface for use proprietary build sytems that
// are incompatible with "go test", and also for testing. // are incompatible with "go test", and also for testing.
// //
var parsePackageFiles = func(ctxt *build.Context, fset *token.FileSet, path string, which string) ([]*ast.File, error) { var parsePackageFiles = func(ctxt *build.Context, fset *token.FileSet, path string, which rune) ([]*ast.File, error) {
// Set the "!cgo" go/build tag, preferring (dummy) Go to // Set the "!cgo" go/build tag, preferring (dummy) Go to
// native C implementations of net.cgoLookupHost et al. // native C implementations of net.cgoLookupHost et al.
ctxt2 := *ctxt ctxt2 := *ctxt
@ -42,19 +42,15 @@ var parsePackageFiles = func(ctxt *build.Context, fset *token.FileSet, path stri
} }
var filenames []string var filenames []string
for _, c := range which { switch which {
var s []string case 'g':
switch c { filenames = bp.GoFiles
case 'g': case 't':
s = bp.GoFiles filenames = bp.TestGoFiles
case 't': case 'x':
s = bp.TestGoFiles filenames = bp.XTestGoFiles
case 'x': default:
s = bp.XTestGoFiles panic(which)
default:
panic(c)
}
filenames = append(filenames, s...)
} }
return parseFiles(fset, bp.Dir, filenames...) return parseFiles(fset, bp.Dir, filenames...)
} }