go/packages: add Option.Tests bool, which affects pattern expansion

In the go build system, test packages and executables do not have a
name distinct from the package under test; they are implied, so
"go test fmt" means build those packages but "go build fmt" does not.

This change adds a Tests boolean option to indicate that implied
tests are desired during pattern expansion.
It has no effect on build systems that have explicit names
for tests, such as Blaze/Bazel.

The gopackages diagnostic tool now has a -test flag.

Change-Id: I424f343958c4286539e518d5f30067da19a57f3b
Reviewed-on: https://go-review.googlesource.com/123775
Reviewed-by: Michael Matloob <matloob@golang.org>
This commit is contained in:
Alan Donovan 2018-07-13 12:09:24 -04:00
parent 9d3ae49c73
commit 18e9dfbf20
5 changed files with 63 additions and 17 deletions

View File

@ -25,7 +25,7 @@ type GoTooOldError struct{ error }
// golistPackages uses the "go list" command to expand the // golistPackages uses the "go list" command to expand the
// pattern words and return metadata for the specified packages. // pattern words and return metadata for the specified packages.
func golistPackages(ctx context.Context, gopath string, cgo, export bool, words []string) ([]*Package, error) { func golistPackages(ctx context.Context, gopath string, cgo, export, tests bool, words []string) ([]*Package, error) {
// Fields must match go list; // Fields must match go list;
// see $GOROOT/src/cmd/go/internal/load/pkg.go. // see $GOROOT/src/cmd/go/internal/load/pkg.go.
type jsonPackage struct { type jsonPackage struct {
@ -62,7 +62,7 @@ func golistPackages(ctx context.Context, gopath string, cgo, export bool, words
// Run "go list" for complete // Run "go list" for complete
// information on the specified packages. // information on the specified packages.
buf, err := golist(ctx, gopath, cgo, export, words) buf, err := golist(ctx, gopath, cgo, export, tests, words)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -176,19 +176,13 @@ func absJoin(dir string, fileses ...[]string) (res []string) {
} }
// golist returns the JSON-encoded result of a "go list args..." query. // golist returns the JSON-encoded result of a "go list args..." query.
func golist(ctx context.Context, gopath string, cgo, export bool, args []string) (*bytes.Buffer, error) { func golist(ctx context.Context, gopath string, cgo, export, tests bool, args []string) (*bytes.Buffer, error) {
out := new(bytes.Buffer) out := new(bytes.Buffer)
if len(args) == 0 {
return out, nil
}
const test = true // TODO(adonovan): expose a flag for this.
cmd := exec.CommandContext(ctx, "go", append([]string{ cmd := exec.CommandContext(ctx, "go", append([]string{
"list", "list",
"-e", "-e",
fmt.Sprintf("-cgo=%t", cgo), fmt.Sprintf("-cgo=%t", cgo),
fmt.Sprintf("-test=%t", test), fmt.Sprintf("-test=%t", tests),
fmt.Sprintf("-export=%t", export), fmt.Sprintf("-export=%t", export),
"-deps", "-deps",
"-json", "-json",

View File

@ -27,6 +27,7 @@ import (
// flags // flags
var ( var (
depsFlag = flag.Bool("deps", false, "show dependencies too") depsFlag = flag.Bool("deps", false, "show dependencies too")
testFlag = flag.Bool("test", false, "include any tests implied by the patterns")
cgoFlag = flag.Bool("cgo", true, "process cgo files") cgoFlag = flag.Bool("cgo", true, "process cgo files")
mode = flag.String("mode", "metadata", "mode (one of metadata, typecheck, wholeprogram)") mode = flag.String("mode", "metadata", "mode (one of metadata, typecheck, wholeprogram)")
private = flag.Bool("private", false, "show non-exported declarations too") private = flag.Bool("private", false, "show non-exported declarations too")
@ -119,6 +120,7 @@ func main() {
opts := &packages.Options{ opts := &packages.Options{
Error: func(error) {}, // we'll take responsibility for printing errors Error: func(error) {}, // we'll take responsibility for printing errors
DisableCgo: !*cgoFlag, DisableCgo: !*cgoFlag,
Tests: *testFlag,
} }
lpkgs, err := load(opts, flag.Args()...) lpkgs, err := load(opts, flag.Args()...)
if err != nil { if err != nil {

View File

@ -40,6 +40,19 @@ type Options struct {
// Replace with flags/cwd/environ pass-through. // Replace with flags/cwd/environ pass-through.
GOPATH string GOPATH string
// The Tests flag causes the result to include any test packages
// implied by the patterns.
//
// For example, under 'go build', the "fmt" pattern ordinarily
// identifies a single importable package, but with the Tests
// flag it additionally denotes the "fmt.test" executable, which
// in turn depends on the variant of "fmt" augmented by its
// in-packages tests, and the "fmt_test" external test package.
//
// For build systems in which test names are explicit,
// this flag may have no effect.
Tests bool
// DisableCgo disables cgo-processing of files that import "C", // DisableCgo disables cgo-processing of files that import "C",
// and removes the 'cgo' build tag, which may affect source file selection. // and removes the 'cgo' build tag, which may affect source file selection.
// By default, TypeCheck, and WholeProgram queries process such // By default, TypeCheck, and WholeProgram queries process such
@ -298,9 +311,13 @@ func (ld *loader) load(patterns ...string) ([]*Package, error) {
ld.GOPATH = os.Getenv("GOPATH") ld.GOPATH = os.Getenv("GOPATH")
} }
if len(patterns) == 0 {
return nil, fmt.Errorf("no packages to load")
}
// Do the metadata query and partial build. // Do the metadata query and partial build.
// TODO(adonovan): support alternative build systems at this seam. // TODO(adonovan): support alternative build systems at this seam.
list, err := golistPackages(ld.Context, ld.GOPATH, ld.cgo, ld.mode == typeCheck, patterns) list, err := golistPackages(ld.Context, ld.GOPATH, ld.cgo, ld.mode == typeCheck, ld.Tests, patterns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -320,7 +337,7 @@ func (ld *loader) load(patterns ...string) ([]*Package, error) {
} }
} }
if len(pkgs) == 0 { if len(pkgs) == 0 {
return nil, fmt.Errorf("no packages to load") return nil, fmt.Errorf("packages not found")
} }
// Materialize the import graph. // Materialize the import graph.

View File

@ -36,7 +36,6 @@ import (
// import error) will result in a JSON blob with no name and a // import error) will result in a JSON blob with no name and a
// nonexistent testmain file in GoFiles. Test that we handle this // nonexistent testmain file in GoFiles. Test that we handle this
// gracefully. // gracefully.
// - import graph for synthetic testmain and "p [t.test]" packages.
// - IsTest boolean // - IsTest boolean
// //
// TypeCheck & WholeProgram modes: // TypeCheck & WholeProgram modes:
@ -44,6 +43,7 @@ import (
// - Packages.Info is correctly set. // - Packages.Info is correctly set.
// - typechecker configuration is honored // - typechecker configuration is honored
// - import cycles are gracefully handled in type checker. // - import cycles are gracefully handled in type checker.
// - test typechecking of generated test main and cgo.
func TestMetadataImportGraph(t *testing.T) { func TestMetadataImportGraph(t *testing.T) {
tmp, cleanup := enterTree(t, map[string]string{ tmp, cleanup := enterTree(t, map[string]string{
@ -74,6 +74,34 @@ func TestMetadataImportGraph(t *testing.T) {
a a
b b
* c * c
* e
errors
* subdir/d
unsafe
b -> a
b -> errors
c -> b
c -> unsafe
e -> b
e -> c
`[1:]
if graph != wantGraph {
t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph)
}
opts.Tests = true
initial, err = packages.Metadata(opts, "c", "subdir/d", "e")
if err != nil {
t.Fatal(err)
}
// Check graph topology.
graph, all = importGraph(initial)
wantGraph = `
a
b
* c
* e * e
errors errors
math/bits math/bits
@ -114,7 +142,7 @@ func TestMetadataImportGraph(t *testing.T) {
{"e", "main", "command", "e.go e2.go"}, {"e", "main", "command", "e.go e2.go"},
{"errors", "errors", "package", "errors.go"}, {"errors", "errors", "package", "errors.go"},
{"subdir/d", "d", "package", "d.go"}, {"subdir/d", "d", "package", "d.go"},
// {"subdir/d.test", "main", "test command", "<hideous generated file name>"}, {"subdir/d.test", "main", "test command", "0.go"},
{"unsafe", "unsafe", "package", ""}, {"unsafe", "unsafe", "package", ""},
} { } {
p, ok := all[test.id] p, ok := all[test.id]
@ -489,8 +517,13 @@ func errorMessages(errors []error) []string {
func srcs(p *packages.Package) (basenames []string) { func srcs(p *packages.Package) (basenames []string) {
// Ideally we would show the root-relative portion (e.g. after // Ideally we would show the root-relative portion (e.g. after
// src/) but vgo doesn't necessarily have a src/ dir. // src/) but vgo doesn't necessarily have a src/ dir.
for _, src := range p.Srcs { for i, src := range p.Srcs {
basenames = append(basenames, filepath.Base(src)) if strings.Contains(src, ".cache/go-build") {
src = fmt.Sprintf("%d.go", i) // make cache names predictable
} else {
src = filepath.Base(src)
}
basenames = append(basenames, src)
} }
return basenames return basenames
} }

View File

@ -44,7 +44,7 @@ func TestStdlibMetadata(t *testing.T) {
t.Logf("Loaded %d packages", len(pkgs)) t.Logf("Loaded %d packages", len(pkgs))
numPkgs := len(pkgs) numPkgs := len(pkgs)
if want := 340; numPkgs < want { if want := 186; numPkgs < want {
t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want) t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want)
} }