diff --git a/go/packages/golist_fallback.go b/go/packages/golist_fallback.go index 6b8d537a..ea484706 100644 --- a/go/packages/golist_fallback.go +++ b/go/packages/golist_fallback.go @@ -67,17 +67,32 @@ func golistDriverFallback(cfg *Config, words ...string) (*driverResponse, error) compiledGoFiles := absJoin(p.Dir, p.GoFiles) // Use a function to simplify control flow. It's just a bunch of gotos. var cgoErrors []error + var outdir string + getOutdir := func() (string, error) { + if outdir != "" { + return outdir, nil + } + if tmpdir == "" { + if tmpdir, err = ioutil.TempDir("", "gopackages"); err != nil { + return "", err + } + } + // Add a "go-build" component to the path to make the tests think the files are in the cache. + // This allows the same test to test the pre- and post-Go 1.11 go list logic because the Go 1.11 + // go list generates test mains in the cache, and the test code knows not to rely on paths in the + // cache to stay stable. + outdir = filepath.Join(tmpdir, "go-build", strings.Replace(p.ImportPath, "/", "_", -1)) + if err := os.MkdirAll(outdir, 0755); err != nil { + outdir = "" + return "", err + } + return outdir, nil + } processCgo := func() bool { // Suppress any cgo errors. Any relevant errors will show up in typechecking. // TODO(matloob): Skip running cgo if Mode < LoadTypes. - if tmpdir == "" { - if tmpdir, err = ioutil.TempDir("", "gopackages"); err != nil { - cgoErrors = append(cgoErrors, err) - return false - } - } - outdir := filepath.Join(tmpdir, strings.Replace(p.ImportPath, "/", "_", -1)) - if err := os.Mkdir(outdir, 0755); err != nil { + outdir, err := getOutdir() + if err != nil { cgoErrors = append(cgoErrors, err) return false } @@ -111,7 +126,7 @@ func golistDriverFallback(cfg *Config, words ...string) (*driverResponse, error) if isRoot { response.Roots = append(response.Roots, testID) } - response.Packages = append(response.Packages, &Package{ + testPkg := &Package{ ID: testID, Name: p.Name, GoFiles: absJoin(p.Dir, p.GoFiles, p.CgoFiles, p.TestGoFiles), @@ -120,7 +135,9 @@ func golistDriverFallback(cfg *Config, words ...string) (*driverResponse, error) PkgPath: pkgpath, Imports: importMap(append(p.Imports, p.TestImports...)), // TODO(matloob): set errors on the Package to cgoErrors - }) + } + response.Packages = append(response.Packages, testPkg) + var xtestPkg *Package if len(p.XTestGoFiles) > 0 { xtestID := fmt.Sprintf("%s_test [%s.test]", id, id) if isRoot { @@ -134,15 +151,51 @@ func golistDriverFallback(cfg *Config, words ...string) (*driverResponse, error) break } } - response.Packages = append(response.Packages, &Package{ + xtestPkg = &Package{ ID: xtestID, Name: p.Name + "_test", GoFiles: absJoin(p.Dir, p.XTestGoFiles), CompiledGoFiles: absJoin(p.Dir, p.XTestGoFiles), - PkgPath: pkgpath, + PkgPath: pkgpath + "_test", Imports: imports, - }) + } + response.Packages = append(response.Packages, xtestPkg) } + // testmain package + testmainID := id + ".test" + if isRoot { + response.Roots = append(response.Roots, testmainID) + } + imports := map[string]*Package{} + imports[testPkg.PkgPath] = &Package{ID: testPkg.ID} + if xtestPkg != nil { + imports[xtestPkg.PkgPath] = &Package{ID: xtestPkg.ID} + } + testmainPkg := &Package{ + ID: testmainID, + Name: "main", + PkgPath: testmainID, + Imports: imports, + } + response.Packages = append(response.Packages, testmainPkg) + outdir, err := getOutdir() + if err != nil { + testmainPkg.Errors = append(testmainPkg.Errors, + Error{"-", fmt.Sprintf("failed to generate testmain: %v", err)}) + return + } + testmain := filepath.Join(outdir, "testmain.go") + extradeps, err := generateTestmain(testmain, testPkg, xtestPkg) + if err != nil { + testmainPkg.Errors = append(testmainPkg.Errors, + Error{"-", fmt.Sprintf("failed to generate testmain: %v", err)}) + } + deps = append(deps, extradeps...) + for _, imp := range extradeps { // testing, testing/internal/testdeps, and maybe os + imports[imp] = &Package{ID: imp} + } + testmainPkg.GoFiles = []string{testmain} + testmainPkg.CompiledGoFiles = []string{testmain} } } } @@ -169,6 +222,11 @@ func golistDriverFallback(cfg *Config, words ...string) (*driverResponse, error) addPackage(p) } + // TODO(matloob): Is this the right ordering? + sort.SliceStable(response.Packages, func(i, j int) bool { + return response.Packages[i].PkgPath < response.Packages[j].PkgPath + }) + return &response, nil } diff --git a/go/packages/golist_fallback_testmain.go b/go/packages/golist_fallback_testmain.go new file mode 100644 index 00000000..37541707 --- /dev/null +++ b/go/packages/golist_fallback_testmain.go @@ -0,0 +1,268 @@ +// Copyright 2018 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 is largely based on the Go 1.10-era cmd/go/internal/test/test.go +// testmain generation code. + +package packages + +import ( + "errors" + "fmt" + "go/ast" + "go/doc" + "go/parser" + "go/token" + "os" + "sort" + "strings" + "text/template" + "unicode" + "unicode/utf8" +) + +// TODO(matloob): Delete this file once Go 1.12 is released. + +// This file complements golist_fallback.go by providing +// support for generating testmains. + +func generateTestmain(out string, testPkg, xtestPkg *Package) (extradeps []string, err error) { + testFuncs, err := loadTestFuncs(testPkg, xtestPkg) + if err != nil { + return nil, err + } + extradeps = []string{"testing/internal/testdeps", "testing"} + if testFuncs.TestMain == nil { + extradeps = append(extradeps, "os") + } + return extradeps, writeTestmain(out, testFuncs) +} + +// The following is adapted from the cmd/go testmain generation code. + +// isTestFunc tells whether fn has the type of a testing function. arg +// specifies the parameter type we look for: B, M or T. +func isTestFunc(fn *ast.FuncDecl, arg string) bool { + if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || + fn.Type.Params.List == nil || + len(fn.Type.Params.List) != 1 || + len(fn.Type.Params.List[0].Names) > 1 { + return false + } + ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr) + if !ok { + return false + } + // We can't easily check that the type is *testing.M + // because we don't know how testing has been imported, + // but at least check that it's *M or *something.M. + // Same applies for B and T. + if name, ok := ptr.X.(*ast.Ident); ok && name.Name == arg { + return true + } + if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == arg { + return true + } + return false +} + +// isTest tells whether name looks like a test (or benchmark, according to prefix). +// It is a Test (say) if there is a character after Test that is not a lower-case letter. +// We don't want TesticularCancer. +func isTest(name, prefix string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Test" is ok + return true + } + rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(rune) +} + +// loadTestFuncs returns the testFuncs describing the tests that will be run. +func loadTestFuncs(ptest, pxtest *Package) (*testFuncs, error) { + t := &testFuncs{ + TestPackage: ptest, + XTestPackage: pxtest, + } + for _, file := range ptest.GoFiles { + if !strings.HasSuffix(file, "_test.go") { + continue + } + if err := t.load(file, "_test", &t.ImportTest, &t.NeedTest); err != nil { + return nil, err + } + } + if pxtest != nil { + for _, file := range pxtest.GoFiles { + if err := t.load(file, "_xtest", &t.ImportXtest, &t.NeedXtest); err != nil { + return nil, err + } + } + } + return t, nil +} + +// writeTestmain writes the _testmain.go file for t to the file named out. +func writeTestmain(out string, t *testFuncs) error { + f, err := os.Create(out) + if err != nil { + return err + } + defer f.Close() + + if err := testmainTmpl.Execute(f, t); err != nil { + return err + } + + return nil +} + +type testFuncs struct { + Tests []testFunc + Benchmarks []testFunc + Examples []testFunc + TestMain *testFunc + TestPackage *Package + XTestPackage *Package + ImportTest bool + NeedTest bool + ImportXtest bool + NeedXtest bool +} + +// Tested returns the name of the package being tested. +func (t *testFuncs) Tested() string { + return t.TestPackage.Name +} + +type testFunc struct { + Package string // imported package name (_test or _xtest) + Name string // function name + Output string // output, for examples + Unordered bool // output is allowed to be unordered. +} + +func (t *testFuncs) load(filename, pkg string, doImport, seen *bool) error { + var fset = token.NewFileSet() + + f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + return errors.New("failed to parse test file " + filename) + } + for _, d := range f.Decls { + n, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + if n.Recv != nil { + continue + } + name := n.Name.String() + switch { + case name == "TestMain": + if isTestFunc(n, "T") { + t.Tests = append(t.Tests, testFunc{pkg, name, "", false}) + *doImport, *seen = true, true + continue + } + err := checkTestFunc(fset, n, "M") + if err != nil { + return err + } + if t.TestMain != nil { + return errors.New("multiple definitions of TestMain") + } + t.TestMain = &testFunc{pkg, name, "", false} + *doImport, *seen = true, true + case isTest(name, "Test"): + err := checkTestFunc(fset, n, "T") + if err != nil { + return err + } + t.Tests = append(t.Tests, testFunc{pkg, name, "", false}) + *doImport, *seen = true, true + case isTest(name, "Benchmark"): + err := checkTestFunc(fset, n, "B") + if err != nil { + return err + } + t.Benchmarks = append(t.Benchmarks, testFunc{pkg, name, "", false}) + *doImport, *seen = true, true + } + } + ex := doc.Examples(f) + sort.Slice(ex, func(i, j int) bool { return ex[i].Order < ex[j].Order }) + for _, e := range ex { + *doImport = true // import test file whether executed or not + if e.Output == "" && !e.EmptyOutput { + // Don't run examples with no output. + continue + } + t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output, e.Unordered}) + *seen = true + } + return nil +} + +func checkTestFunc(fset *token.FileSet, fn *ast.FuncDecl, arg string) error { + if !isTestFunc(fn, arg) { + name := fn.Name.String() + pos := fset.Position(fn.Pos()) + return fmt.Errorf("%s: wrong signature for %s, must be: func %s(%s *testing.%s)", pos, name, name, strings.ToLower(arg), arg) + } + return nil +} + +var testmainTmpl = template.Must(template.New("main").Parse(` +package main + +import ( +{{if not .TestMain}} + "os" +{{end}} + "testing" + "testing/internal/testdeps" + +{{if .ImportTest}} + {{if .NeedTest}}_test{{else}}_{{end}} {{.TestPackage.PkgPath | printf "%q"}} +{{end}} +{{if .ImportXtest}} + {{if .NeedXtest}}_xtest{{else}}_{{end}} {{.XTestPackage.PkgPath | printf "%q"}} +{{end}} +) + +var tests = []testing.InternalTest{ +{{range .Tests}} + {"{{.Name}}", {{.Package}}.{{.Name}}}, +{{end}} +} + +var benchmarks = []testing.InternalBenchmark{ +{{range .Benchmarks}} + {"{{.Name}}", {{.Package}}.{{.Name}}}, +{{end}} +} + +var examples = []testing.InternalExample{ +{{range .Examples}} + {"{{.Name}}", {{.Package}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}}, +{{end}} +} + +func init() { + testdeps.ImportPath = {{.TestPackage.PkgPath | printf "%q"}} +} + +func main() { + m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, examples) +{{with .TestMain}} + {{.Package}}.{{.Name}}(m) +{{else}} + os.Exit(m.Run()) +{{end}} +} + +`)) diff --git a/go/packages/packages110_test.go b/go/packages/packages110_test.go index 9bc72a2c..d4005682 100644 --- a/go/packages/packages110_test.go +++ b/go/packages/packages110_test.go @@ -6,51 +6,6 @@ package packages_test -import ( - "bytes" - "fmt" - "os" - "strings" - "testing" - - "golang.org/x/tools/go/packages" -) - func init() { usesOldGolist = true } - -func TestXTestImports(t *testing.T) { - tmp, cleanup := makeTree(t, map[string]string{ - "src/a/a_test.go": `package a_test; import "a"`, - "src/a/a.go": `package a`, - }) - defer cleanup() - - cfg := &packages.Config{ - Mode: packages.LoadImports, - Dir: tmp, - Env: append(os.Environ(), "GOPATH="+tmp, "GO111MODULE=off"), - Tests: true, - } - initial, err := packages.Load(cfg, "a") - if err != nil { - t.Fatal(err) - } - - var gotImports bytes.Buffer - for _, pkg := range initial { - var imports []string - for imp, pkg := range pkg.Imports { - imports = append(imports, fmt.Sprintf("%q: %q", imp, pkg.ID)) - } - fmt.Fprintf(&gotImports, "%s {%s}\n", pkg.ID, strings.Join(imports, ", ")) - } - wantImports := `a {} -a [a.test] {} -a_test [a.test] {"a": "a [a.test]"} -` - if gotImports.String() != wantImports { - t.Fatalf("wrong imports: got <<%s>>, want <<%s>>", gotImports.String(), wantImports) - } -} diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 6e71dc70..b39e77a5 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -152,32 +152,8 @@ func TestLoadImportsGraph(t *testing.T) { subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] `[1:] - // Legacy go list support does not create test main package. - wantOldGraph := ` - a - b -* c -* e - errors - math/bits -* subdir/d -* subdir/d [subdir/d.test] -* subdir/d_test [subdir/d.test] - unsafe - b -> a - b -> errors - c -> b - c -> unsafe - e -> b - e -> c - subdir/d [subdir/d.test] -> math/bits - subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] -`[1:] - - if graph != wantGraph && !usesOldGolist { + if graph != wantGraph { t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) - } else if graph != wantOldGraph && usesOldGolist { - t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantOldGraph) } // Check node information: kind, name, srcs. @@ -196,10 +172,6 @@ func TestLoadImportsGraph(t *testing.T) { {"subdir/d.test", "main", "command", "0.go"}, {"unsafe", "unsafe", "package", ""}, } { - if usesOldGolist && test.id == "subdir/d.test" { - // Legacy go list support does not create test main package. - continue - } p, ok := all[test.id] if !ok { t.Errorf("no package %s", test.id) @@ -261,20 +233,8 @@ func TestLoadImportsGraph(t *testing.T) { subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] `[1:] - // Legacy go list support does not create test main package. - wantOldGraph = ` - math/bits -* subdir/d -* subdir/d [subdir/d.test] -* subdir/d_test [subdir/d.test] -* subdir/e - subdir/d [subdir/d.test] -> math/bits - subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] -`[1:] - if graph != wantGraph && !usesOldGolist { + if graph != wantGraph { t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) - } else if graph != wantOldGraph && usesOldGolist { - t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantOldGraph) } } }