1
0
mirror of https://github.com/golang/go synced 2024-11-18 12:54:44 -07:00

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

View File

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