go/packages: generate test main files from the golist fallback

Part of the testmain generation logic (sans coverage) has been taken
from go build and put into golist_fallback_testmain.go.
golist_fallback invokes this logic and builds up the package metadata
for the testmain.
The tests checking for testmain are now no longer skipped from
packages_test.go.

Change-Id: I487a947f087f3ad4161ea6c2bed06ebb2f833422
Reviewed-on: https://go-review.googlesource.com/134119
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Michael Matloob 2018-09-07 17:11:41 -07:00
parent 9f3b32b5c4
commit 90fa682c2a
4 changed files with 341 additions and 100 deletions

View File

@ -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
}

View File

@ -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}}
}
`))

View File

@ -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)
}
}

View File

@ -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)
}
}
}