cmd/callgraph: use go/packages

Because go/packages presents each synthesized test main package
as a first-class package, the tests now analyze all of the dependencies
of "testing", which they previously avoided.  This makes the tests
slower and the resulting call graph much larger,
so they now look for a subset match, not an exact match,
on the set of graph edges.

Change-Id: I9d7acf420e41cbffc03ca8423f5afb3ef671d775
Reviewed-on: https://go-review.googlesource.com/128695
Reviewed-by: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
This commit is contained in:
Alan Donovan 2018-08-08 17:02:13 -04:00
parent 2fad9c5652
commit 1bd72987c2
3 changed files with 59 additions and 68 deletions

View File

@ -37,7 +37,7 @@ import (
"golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/callgraph/cha"
"golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/callgraph/rta"
"golang.org/x/tools/go/callgraph/static" "golang.org/x/tools/go/callgraph/static"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/packages"
"golang.org/x/tools/go/pointer" "golang.org/x/tools/go/pointer"
"golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil" "golang.org/x/tools/go/ssa/ssautil"
@ -67,7 +67,7 @@ const Usage = `callgraph: display the the call graph of a Go program.
Usage: Usage:
callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] <args>... callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] package...
Flags: Flags:
@ -118,8 +118,6 @@ Flags:
import path of the enclosing package. Consult the go/ssa import path of the enclosing package. Consult the go/ssa
API documentation for details. API documentation for details.
` + loader.FromArgsUsage + `
Examples: Examples:
Show the call graph of the trivial web server application: Show the call graph of the trivial web server application:
@ -158,7 +156,7 @@ func init() {
func main() { func main() {
flag.Parse() flag.Parse()
if err := doCallgraph(&build.Default, *algoFlag, *formatFlag, *testFlag, flag.Args()); err != nil { if err := doCallgraph("", "", *algoFlag, *formatFlag, *testFlag, flag.Args()); err != nil {
fmt.Fprintf(os.Stderr, "callgraph: %s\n", err) fmt.Fprintf(os.Stderr, "callgraph: %s\n", err)
os.Exit(1) os.Exit(1)
} }
@ -166,28 +164,27 @@ func main() {
var stdout io.Writer = os.Stdout var stdout io.Writer = os.Stdout
func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []string) error { func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) error {
conf := loader.Config{Build: ctxt}
if len(args) == 0 { if len(args) == 0 {
fmt.Fprintln(os.Stderr, Usage) fmt.Fprintln(os.Stderr, Usage)
return nil return nil
} }
// Use the initial packages from the command line. cfg := &packages.Config{
_, err := conf.FromArgs(args, tests) Mode: packages.LoadAllSyntax,
if err != nil { Tests: tests,
return err Dir: dir,
} }
if gopath != "" {
// Load, parse and type-check the whole program. cfg.Env = append(os.Environ(), "GOPATH="+gopath) // to enable testing
iprog, err := conf.Load() }
initial, err := packages.Load(cfg, args...)
if err != nil { if err != nil {
return err return err
} }
// Create and build SSA-form program representation. // Create and build SSA-form program representation.
prog := ssautil.CreateProgram(iprog, 0) prog, pkgs := ssautil.Packages(initial, 0)
prog.Build() prog.Build()
// -- call graph construction ------------------------------------------ // -- call graph construction ------------------------------------------
@ -221,7 +218,7 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
} }
} }
mains, err := mainPackages(prog, tests) mains, err := mainPackages(pkgs)
if err != nil { if err != nil {
return err return err
} }
@ -237,7 +234,7 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
cg = ptares.CallGraph cg = ptares.CallGraph
case "rta": case "rta":
mains, err := mainPackages(prog, tests) mains, err := mainPackages(pkgs)
if err != nil { if err != nil {
return err return err
} }
@ -305,25 +302,13 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
// mainPackages returns the main packages to analyze. // mainPackages returns the main packages to analyze.
// Each resulting package is named "main" and has a main function. // Each resulting package is named "main" and has a main function.
func mainPackages(prog *ssa.Program, tests bool) ([]*ssa.Package, error) { func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) {
pkgs := prog.AllPackages() // TODO(adonovan): use only initial packages
// If tests, create a "testmain" package for each test.
var mains []*ssa.Package var mains []*ssa.Package
if tests { for _, p := range pkgs {
for _, pkg := range pkgs { if p != nil && p.Pkg.Name() == "main" && p.Func("main") != nil {
if main := prog.CreateTestMainPackage(pkg); main != nil { mains = append(mains, p)
mains = append(mains, main)
} }
} }
if mains == nil {
return nil, fmt.Errorf("no tests")
}
return mains, nil
}
// Otherwise, use the main packages.
mains = append(mains, ssautil.MainPackages(pkgs)...)
if len(mains) == 0 { if len(mains) == 0 {
return nil, fmt.Errorf("no main packages") return nil, fmt.Errorf("no main packages")
} }

View File

@ -11,25 +11,23 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"go/build" "path/filepath"
"reflect"
"sort"
"strings" "strings"
"testing" "testing"
) )
func TestCallgraph(t *testing.T) { func TestCallgraph(t *testing.T) {
ctxt := build.Default // copy gopath, err := filepath.Abs("testdata")
ctxt.GOPATH = "testdata" if err != nil {
t.Fatal(err)
const format = "{{.Caller}} --> {{.Callee}}" }
for _, test := range []struct { for _, test := range []struct {
algo, format string algo string
tests bool tests bool
want []string want []string
}{ }{
{"rta", format, false, []string{ {"rta", false, []string{
// rta imprecisely shows cross product of {main,main2} x {C,D} // rta imprecisely shows cross product of {main,main2} x {C,D}
`pkg.main --> (pkg.C).f`, `pkg.main --> (pkg.C).f`,
`pkg.main --> (pkg.D).f`, `pkg.main --> (pkg.D).f`,
@ -37,7 +35,7 @@ func TestCallgraph(t *testing.T) {
`pkg.main2 --> (pkg.C).f`, `pkg.main2 --> (pkg.C).f`,
`pkg.main2 --> (pkg.D).f`, `pkg.main2 --> (pkg.D).f`,
}}, }},
{"pta", format, false, []string{ {"pta", false, []string{
// pta distinguishes main->C, main2->D. Also has a root node. // pta distinguishes main->C, main2->D. Also has a root node.
`<root> --> pkg.init`, `<root> --> pkg.init`,
`<root> --> pkg.main`, `<root> --> pkg.main`,
@ -45,37 +43,42 @@ func TestCallgraph(t *testing.T) {
`pkg.main --> pkg.main2`, `pkg.main --> pkg.main2`,
`pkg.main2 --> (pkg.D).f`, `pkg.main2 --> (pkg.D).f`,
}}, }},
// tests: main is not called. // tests: both the package's main and the test's main are called.
{"rta", format, true, []string{ // The callgraph includes all the guts of the "testing" package.
`pkg$testmain.init --> pkg.init`, {"rta", true, []string{
`pkg.test.main --> testing.MainStart`,
`testing.runExample --> pkg.Example`,
`pkg.Example --> (pkg.C).f`, `pkg.Example --> (pkg.C).f`,
`pkg.main --> (pkg.C).f`,
}}, }},
{"pta", format, true, []string{ {"pta", true, []string{
`<root> --> pkg$testmain.init`, `<root> --> pkg.test.main`,
`<root> --> pkg.Example`, `<root> --> pkg.main`,
`pkg$testmain.init --> pkg.init`, `pkg.test.main --> testing.MainStart`,
`testing.runExample --> pkg.Example`,
`pkg.Example --> (pkg.C).f`, `pkg.Example --> (pkg.C).f`,
`pkg.main --> (pkg.C).f`,
}}, }},
} { } {
const format = "{{.Caller}} --> {{.Callee}}"
stdout = new(bytes.Buffer) stdout = new(bytes.Buffer)
if err := doCallgraph(&ctxt, test.algo, test.format, test.tests, []string{"pkg"}); err != nil { if err := doCallgraph("testdata/src", gopath, test.algo, format, test.tests, []string{"pkg"}); err != nil {
t.Error(err) t.Error(err)
continue continue
} }
got := sortedLines(fmt.Sprint(stdout)) edges := make(map[string]bool)
if !reflect.DeepEqual(got, test.want) { for _, line := range strings.Split(fmt.Sprint(stdout), "\n") {
t.Errorf("callgraph(%q, %q, %t):\ngot:\n%s\nwant:\n%s", edges[line] = true
test.algo, test.format, test.tests, }
strings.Join(got, "\n"), for _, edge := range test.want {
strings.Join(test.want, "\n")) if !edges[edge] {
t.Errorf("callgraph(%q, %t): missing edge: %s",
test.algo, test.tests, edge)
}
}
if t.Failed() {
t.Log("got:\n", stdout)
} }
} }
} }
func sortedLines(s string) []string {
s = strings.TrimSpace(s)
lines := strings.Split(s, "\n")
sort.Strings(lines)
return lines
}

View File

@ -1,7 +1,10 @@
package main package main
// Don't import "testing", it adds a lot of callgraph edges. // An Example function must have an "Output:" comment for the go build
// system to generate a call to it from the test main package.
func Example() { func Example() {
C(0).f() C(0).f()
// Output:
} }