go/ssa: support custom TestMain functions in test packages

Supporting user-defined TestMain functions requires that we generate a
"testmain" package for each testable package, rather than a single one
for the entire program.  This entails these API changes:

1. (*ssa.Program).{CreateTestMainPackage,FindTests} both now
   accept only a single package.  Existing clients that pass them
   multiple packages must call them from a loop.

2. (*ssa.Program).FindTests returns an additional result, the the
   optional TestMain *ssa.Function.  Existing clients may discard it.

Also:
- Test the new logic using the SSA interpreter
- add ssautil.MainPackages helper
- callgraph: allow multiple main packages, and analyze them all
- ssadump -run: allow multiple main/test packages, and run each in a new interpreter
- minor simplifications to some callers (e.g. guru)

Fixes golang/go#9553

Change-Id: Ia7de9bd27448fb08b8d172ba5cdbcf37a762b7a0
Reviewed-on: https://go-review.googlesource.com/25102
Reviewed-by: Robert Griesemer <gri@golang.org>
This commit is contained in:
Alan Donovan 2016-07-20 15:03:06 -04:00
parent 860883f6da
commit 8e53eb937b
11 changed files with 220 additions and 177 deletions

View File

@ -221,12 +221,12 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
}
}
main, err := mainPackage(prog, tests)
mains, err := mainPackages(prog, tests)
if err != nil {
return err
}
config := &pointer.Config{
Mains: []*ssa.Package{main},
Mains: mains,
BuildCallGraph: true,
Log: ptalog,
}
@ -237,13 +237,13 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
cg = ptares.CallGraph
case "rta":
main, err := mainPackage(prog, tests)
mains, err := mainPackages(prog, tests)
if err != nil {
return err
}
roots := []*ssa.Function{
main.Func("init"),
main.Func("main"),
var roots []*ssa.Function
for _, main := range mains {
roots = append(roots, main.Func("init"), main.Func("main"))
}
rtares := rta.Analyze(roots, true)
cg = rtares.CallGraph
@ -303,35 +303,31 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
return nil
}
// mainPackage returns the main package to analyze.
// The resulting package has a main() function.
func mainPackage(prog *ssa.Program, tests bool) (*ssa.Package, error) {
pkgs := prog.AllPackages()
// TODO(adonovan): allow independent control over tests, mains and libraries.
// TODO(adonovan): put this logic in a library; we keep reinventing it.
// 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.
var mains []*ssa.Package
if tests {
// If -test, use all packages' tests.
if len(pkgs) > 0 {
if main := prog.CreateTestMainPackage(pkgs...); main != nil {
return main, nil
for _, pkg := range pkgs {
if main := prog.CreateTestMainPackage(pkg); main != nil {
mains = append(mains, main)
}
}
if mains == nil {
return nil, fmt.Errorf("no tests")
}
// Otherwise, use the first package named main.
for _, pkg := range pkgs {
if pkg.Pkg.Name() == "main" {
if pkg.Func("main") == nil {
return nil, fmt.Errorf("no func main() in main package")
}
return pkg, nil
}
return mains, nil
}
return nil, fmt.Errorf("no main package")
// Otherwise, use the main packages.
mains = append(mains, ssautil.MainPackages(pkgs)...)
if len(mains) == 0 {
return nil, fmt.Errorf("no main packages")
}
return mains, nil
}
type Edge struct {

View File

@ -47,14 +47,14 @@ func TestCallgraph(t *testing.T) {
}},
// tests: main is not called.
{"rta", format, true, []string{
`pkg$testmain.init --> pkg.init`,
`pkg.Example --> (pkg.C).f`,
`test$main.init --> pkg.init`,
}},
{"pta", format, true, []string{
`<root> --> pkg$testmain.init`,
`<root> --> pkg.Example`,
`<root> --> test$main.init`,
`pkg$testmain.init --> pkg.init`,
`pkg.Example --> (pkg.C).f`,
`test$main.init --> pkg.init`,
}},
} {
stdout = new(bytes.Buffer)

View File

@ -129,26 +129,18 @@ func setPTAScope(lconf *loader.Config, scope []string) error {
// Create a pointer.Config whose scope is the initial packages of lprog
// and their dependencies.
func setupPTA(prog *ssa.Program, lprog *loader.Program, ptaLog io.Writer, reflection bool) (*pointer.Config, error) {
// TODO(adonovan): the body of this function is essentially
// duplicated in all go/pointer clients. Refactor.
// For each initial package (specified on the command line),
// if it has a main function, analyze that,
// otherwise analyze its tests, if any.
var testPkgs, mains []*ssa.Package
var mains []*ssa.Package
for _, info := range lprog.InitialPackages() {
initialPkg := prog.Package(info.Pkg)
p := prog.Package(info.Pkg)
// Add package to the pointer analysis scope.
if initialPkg.Func("main") != nil {
mains = append(mains, initialPkg)
} else {
testPkgs = append(testPkgs, initialPkg)
}
}
if testPkgs != nil {
if p := prog.CreateTestMainPackage(testPkgs...); p != nil {
if p.Pkg.Name() == "main" && p.Func("main") != nil {
mains = append(mains, p)
} else if main := prog.CreateTestMainPackage(p); main != nil {
mains = append(mains, main)
}
}
if mains == nil {

View File

@ -125,46 +125,44 @@ func doMain() error {
}
// Load, parse and type-check the whole program.
iprog, err := conf.Load()
lprog, err := conf.Load()
if err != nil {
return err
}
// Create and build SSA-form program representation.
prog := ssautil.CreateProgram(iprog, mode)
prog := ssautil.CreateProgram(lprog, mode)
// Build and display only the initial packages
// (and synthetic wrappers), unless -run is specified.
for _, info := range iprog.InitialPackages() {
prog.Package(info.Pkg).Build()
var initpkgs []*ssa.Package
for _, info := range lprog.InitialPackages() {
ssapkg := prog.Package(info.Pkg)
ssapkg.Build()
if info.Pkg.Path() != "runtime" {
initpkgs = append(initpkgs, ssapkg)
}
}
// Run the interpreter.
if *runFlag {
prog.Build()
var main *ssa.Package
pkgs := prog.AllPackages()
var mains []*ssa.Package
if *testFlag {
// If -test, run all packages' tests.
if len(pkgs) > 0 {
main = prog.CreateTestMainPackage(pkgs...)
// If -test, run the tests.
for _, pkg := range initpkgs {
if main := prog.CreateTestMainPackage(pkg); main != nil {
mains = append(mains, main)
}
if main == nil {
}
if mains == nil {
return fmt.Errorf("no tests")
}
} else {
// Otherwise, run main.main.
for _, pkg := range pkgs {
if pkg.Pkg.Name() == "main" {
main = pkg
if main.Func("main") == nil {
return fmt.Errorf("no func main() in main package")
}
break
}
}
if main == nil {
// Otherwise, run the main packages.
mains := ssautil.MainPackages(initpkgs)
if len(mains) == 0 {
return fmt.Errorf("no main package")
}
}
@ -174,7 +172,12 @@ func doMain() error {
build.Default.GOARCH, runtime.GOARCH)
}
for _, main := range mains {
if len(mains) > 1 {
fmt.Fprintf(os.Stderr, "Running: %s\n", main.Pkg.Path())
}
interp.Interpret(main, interpMode, conf.TypeChecker.Sizes, main.Pkg.Path(), args)
}
}
return nil
}

View File

@ -60,20 +60,22 @@ func TestStdlib(t *testing.T) {
}
// Determine the set of packages/tests to analyze.
var testPkgs []*ssa.Package
var mains []*ssa.Package
for _, info := range iprog.InitialPackages() {
testPkgs = append(testPkgs, prog.Package(info.Pkg))
ssapkg := prog.Package(info.Pkg)
if main := prog.CreateTestMainPackage(ssapkg); main != nil {
mains = append(mains, main)
}
testmain := prog.CreateTestMainPackage(testPkgs...)
if testmain == nil {
t.Fatal("analysis scope has tests")
}
if mains == nil {
t.Fatal("no tests found in analysis scope")
}
// Run the analysis.
config := &Config{
Reflection: false, // TODO(adonovan): fix remaining bug in rVCallConstraint, then enable.
BuildCallGraph: true,
Mains: []*ssa.Package{testmain},
Mains: mains,
}
// TODO(adonovan): add some query values (affects track bits).

View File

@ -222,29 +222,25 @@ func run(t *testing.T, dir, input string, success successPredicate) bool {
prog := ssautil.CreateProgram(iprog, ssa.SanityCheckFunctions)
prog.Build()
// Find first main or test package among the initial packages.
var mainPkg *ssa.Package
var initialPkgs []*ssa.Package
for _, info := range iprog.InitialPackages() {
if info.Pkg.Path() == "runtime" {
continue // not an initial package
}
p := prog.Package(info.Pkg)
initialPkgs = append(initialPkgs, p)
if mainPkg == nil && p.Func("main") != nil {
if p.Pkg.Name() == "main" && p.Func("main") != nil {
mainPkg = p
break
}
mainPkg = prog.CreateTestMainPackage(p)
if mainPkg != nil {
break
}
}
if mainPkg == nil {
testmainPkg := prog.CreateTestMainPackage(initialPkgs...)
if testmainPkg == nil {
t.Errorf("CreateTestMainPackage(%s) returned nil", mainPkg)
return false
}
if testmainPkg.Func("main") == nil {
t.Errorf("synthetic testmain package has no main")
return false
}
mainPkg = testmainPkg
t.Fatalf("no main or test packages among initial packages: %s", inputs)
}
var out bytes.Buffer
@ -346,6 +342,23 @@ func TestTestmainPackage(t *testing.T) {
return nil
}
run(t, "testdata"+slash, "a_test.go", success)
// Run a test with a custom TestMain function and ensure that it
// is executed, and that m.Run runs the tests.
success = func(exitcode int, output string) error {
if exitcode != 0 {
return fmt.Errorf("unexpected failure; output=%s", output)
}
if want := `TestMain start
TestC
PASS
TestMain end
`; output != want {
return fmt.Errorf("output was %q, want %q", output, want)
}
return nil
}
run(t, "testdata"+slash, "c_test.go", success)
}
// CreateTestMainPackage should return nil if there were no tests.

17
go/ssa/interp/testdata/c_test.go vendored Normal file
View File

@ -0,0 +1,17 @@
package c_test
import (
"os"
"testing"
)
func TestC(t *testing.T) {
println("TestC")
}
func TestMain(m *testing.M) {
println("TestMain start")
code := m.Run()
println("TestMain end")
os.Exit(code)
}

View File

@ -64,3 +64,16 @@ func (visit *visitor) function(fn *ssa.Function) {
}
}
}
// MainPackages returns the subset of the specified packages
// named "main" that define a main function.
// The result may include synthetic "testmain" packages.
func MainPackages(pkgs []*ssa.Package) []*ssa.Package {
var mains []*ssa.Package
for _, pkg := range pkgs {
if pkg.Pkg.Name() == "main" && pkg.Func("main") != nil {
mains = append(mains, pkg)
}
}
return mains
}

View File

@ -12,23 +12,18 @@ package ssa
import (
"go/ast"
exact "go/constant"
"go/token"
"go/types"
"log"
"os"
"sort"
"strings"
)
// FindTests returns the list of packages that define at least one Test,
// Example or Benchmark function (as defined by "go test"), and the
// lists of all such functions.
//
func FindTests(pkgs []*Package) (testpkgs []*Package, tests, benchmarks, examples []*Function) {
if len(pkgs) == 0 {
return
}
prog := pkgs[0].Prog
// FindTests returns the Test, Benchmark, and Example functions
// (as defined by "go test") defined in the specified package,
// and its TestMain function, if any.
func FindTests(pkg *Package) (tests, benchmarks, examples []*Function, main *Function) {
prog := pkg.Prog
// The first two of these may be nil: if the program doesn't import "testing",
// it can't contain any tests, but it may yet contain Examples.
@ -36,17 +31,24 @@ func FindTests(pkgs []*Package) (testpkgs []*Package, tests, benchmarks, example
var benchmarkSig *types.Signature // func(*testing.B)
var exampleSig = types.NewSignature(nil, nil, nil, false) // func()
// Obtain the types from the parameters of testing.Main().
// Obtain the types from the parameters of testing.MainStart.
if testingPkg := prog.ImportedPackage("testing"); testingPkg != nil {
params := testingPkg.Func("Main").Signature.Params()
mainStart := testingPkg.Func("MainStart")
params := mainStart.Signature.Params()
testSig = funcField(params.At(1).Type())
benchmarkSig = funcField(params.At(2).Type())
}
seen := make(map[*Package]bool)
for _, pkg := range pkgs {
if pkg.Prog != prog {
panic("wrong Program")
// Does the package define this function?
// func TestMain(*testing.M)
if f := pkg.Func("TestMain"); f != nil {
sig := f.Type().(*types.Signature)
starM := mainStart.Signature.Results().At(0).Type() // *testing.M
if sig.Results().Len() == 0 &&
sig.Params().Len() == 1 &&
types.Identical(sig.Params().At(0).Type(), starM) {
main = f
}
}
}
// TODO(adonovan): use a stable order, e.g. lexical.
@ -65,12 +67,6 @@ func FindTests(pkgs []*Package) (testpkgs []*Package, tests, benchmarks, example
default:
continue
}
if !seen[pkg] {
seen[pkg] = true
testpkgs = append(testpkgs, pkg)
}
}
}
}
return
@ -87,15 +83,22 @@ func isTestSig(f *Function, prefix string, sig *types.Signature) bool {
// systems that don't exactly follow 'go test' conventions.
var testMainStartBodyHook func(*Function)
// CreateTestMainPackage creates and returns a synthetic "main"
// package that runs all the tests of the supplied packages, similar
// to the one that would be created by the 'go test' tool.
// CreateTestMainPackage creates and returns a synthetic "testmain"
// package for the specified package if it defines tests, benchmarks or
// executable examples, or nil otherwise. The new package is named
// "main" and provides a function named "main" that runs the tests,
// similar to the one that would be created by the 'go test' tool.
//
// It returns nil if the program contains no tests.
//
func (prog *Program) CreateTestMainPackage(pkgs ...*Package) *Package {
pkgs, tests, benchmarks, examples := FindTests(pkgs)
if len(pkgs) == 0 {
// Subsequent calls to prog.AllPackages include the new package.
// The package pkg must belong to the program prog.
func (prog *Program) CreateTestMainPackage(pkg *Package) *Package {
if pkg.Prog != prog {
log.Fatal("Package does not belong to Program")
}
tests, benchmarks, examples, testMainFunc := FindTests(pkg)
if testMainFunc == nil && tests == nil && benchmarks == nil && examples == nil {
return nil
}
@ -103,7 +106,7 @@ func (prog *Program) CreateTestMainPackage(pkgs ...*Package) *Package {
Prog: prog,
Members: make(map[string]Member),
values: make(map[types.Object]Value),
Pkg: types.NewPackage("test$main", "main"),
Pkg: types.NewPackage(pkg.Pkg.Path()+"$testmain", "main"),
}
// Build package's init function.
@ -120,30 +123,18 @@ func (prog *Program) CreateTestMainPackage(pkgs ...*Package) *Package {
testMainStartBodyHook(init)
}
// Initialize packages to test.
var pkgpaths []string
for _, pkg := range pkgs {
// Initialize package under test.
var v Call
v.Call.Value = pkg.init
v.setType(types.NewTuple())
init.emit(&v)
pkgpaths = append(pkgpaths, pkg.Pkg.Path())
}
sort.Strings(pkgpaths)
init.emit(new(Return))
init.finishBody()
testmain.init = init
testmain.Pkg.MarkComplete()
testmain.Members[init.name] = init
// For debugging convenience, define an unexported const
// that enumerates the packages.
packagesConst := types.NewConst(token.NoPos, testmain.Pkg, "packages", tString,
exact.MakeString(strings.Join(pkgpaths, " ")))
memberFromObject(testmain, packagesConst, nil)
// Create main *types.Func and *ssa.Function
// Create main *types.Func and *Function
mainFunc := types.NewFunc(token.NoPos, testmain.Pkg, "main", new(types.Signature))
memberFromObject(testmain, mainFunc, nil)
main := testmain.Func("main")
@ -166,7 +157,12 @@ func (prog *Program) CreateTestMainPackage(pkgs ...*Package) *Package {
// tests := []testing.InternalTest{{"TestFoo", TestFoo}, ...}
// benchmarks := []testing.InternalBenchmark{...}
// examples := []testing.InternalExample{...}
// testing.Main(match, tests, benchmarks, examples)
// if TestMain is defined {
// m := testing.MainStart(match, tests, benchmarks, examples)
// return TestMain(m)
// } else {
// return testing.Main(match, tests, benchmarks, examples)
// }
// }
matcher := &Function{
@ -182,23 +178,41 @@ func (prog *Program) CreateTestMainPackage(pkgs ...*Package) *Package {
matcher.emit(&Return{Results: []Value{vTrue, nilConst(types.Universe.Lookup("error").Type())}})
matcher.finishBody()
// Emit call: testing.Main(matcher, tests, benchmarks, examples).
var c Call
c.Call.Value = testingMain
c.Call.Args = []Value{
matcher,
testMainSlice(main, tests, testingMainParams.At(1).Type()),
testMainSlice(main, benchmarks, testingMainParams.At(2).Type()),
testMainSlice(main, examples, testingMainParams.At(3).Type()),
}
if testMainFunc != nil {
// Emit: m := testing.MainStart(matcher, tests, benchmarks, examples).
// (Main and MainStart have the same parameters.)
mainStart := testingPkg.Func("MainStart")
c.Call.Value = mainStart
c.setType(mainStart.Signature.Results().At(0).Type()) // *testing.M
m := main.emit(&c)
// Emit: return TestMain(m)
var c2 Call
c2.Call.Value = testMainFunc
c2.Call.Args = []Value{m}
emitTailCall(main, &c2)
} else {
// Emit: return testing.Main(matcher, tests, benchmarks, examples)
c.Call.Value = testingMain
emitTailCall(main, &c)
}
} else {
// The program does not import "testing", but FindTests
// returned non-nil, which must mean there were Examples
// but no Tests or Benchmarks.
// but no Test, Benchmark, or TestMain functions.
// We'll simply call them from testmain.main; this will
// ensure they don't panic, but will not check any
// "Output:" comments.
// (We should not execute an Example that has no
// "Output:" comment, but it's impossible to tell here.)
for _, eg := range examples {
var c Call
c.Call.Value = eg

View File

@ -17,7 +17,7 @@ import (
"golang.org/x/tools/go/ssa/ssautil"
)
func create(t *testing.T, content string) []*ssa.Package {
func create(t *testing.T, content string) *ssa.Package {
var conf loader.Config
f, err := conf.ParseFile("foo_test.go", content)
if err != nil {
@ -25,13 +25,14 @@ func create(t *testing.T, content string) []*ssa.Package {
}
conf.CreateFromFiles("foo", f)
iprog, err := conf.Load()
lprog, err := conf.Load()
if err != nil {
t.Fatal(err)
}
// We needn't call Build.
return ssautil.CreateProgram(iprog, ssa.SanityCheckFunctions).AllPackages()
foo := lprog.Package("foo").Pkg
return ssautil.CreateProgram(lprog, ssa.SanityCheckFunctions).Package(foo)
}
func TestFindTests(t *testing.T) {
@ -74,8 +75,8 @@ func ExampleD(t *testing.T) {}
func exampleE() int { return 0 }
func (T) Example() {}
`
pkgs := create(t, test)
_, tests, benchmarks, examples := ssa.FindTests(pkgs)
pkg := create(t, test)
tests, benchmarks, examples, _ := ssa.FindTests(pkg)
sort.Sort(funcsByPos(tests))
if got, want := fmt.Sprint(tests), "[foo.Test foo.TestA foo.TestB]"; got != want {
@ -102,8 +103,8 @@ package foo
func Example() {}
func ExampleA() {}
`
pkgs := create(t, test)
_, tests, benchmarks, examples := ssa.FindTests(pkgs)
pkg := create(t, test)
tests, benchmarks, examples, _ := ssa.FindTests(pkg)
if len(tests) > 0 {
t.Errorf("FindTests.tests = %s, want none", tests)
}

View File

@ -47,7 +47,6 @@ package analysis // import "golang.org/x/tools/godoc/analysis"
import (
"fmt"
"go/build"
exact "go/constant"
"go/scanner"
"go/token"
"go/types"
@ -396,21 +395,12 @@ func Run(pta bool, result *Result) {
// Only the transitively error-free packages are used.
prog := ssautil.CreateProgram(iprog, ssa.GlobalDebug)
// Compute the set of main packages, including testmain.
allPackages := prog.AllPackages()
var mainPkgs []*ssa.Package
if testmain := prog.CreateTestMainPackage(allPackages...); testmain != nil {
mainPkgs = append(mainPkgs, testmain)
if p := testmain.Const("packages"); p != nil {
log.Printf("Tested packages: %v", exact.StringVal(p.Value.Value))
// Create a "testmain" package for each package with tests.
for _, pkg := range prog.AllPackages() {
if testmain := prog.CreateTestMainPackage(pkg); testmain != nil {
log.Printf("Adding tests for %s", pkg.Pkg.Path())
}
}
for _, pkg := range allPackages {
if pkg.Pkg.Name() == "main" && pkg.Func("main") != nil {
mainPkgs = append(mainPkgs, pkg)
}
}
log.Print("Transitively error-free main packages: ", mainPkgs)
// Build SSA code for bodies of all functions in the whole program.
result.setStatusf("Constructing SSA form...")
@ -505,6 +495,8 @@ func Run(pta bool, result *Result) {
result.setStatusf("Type analysis complete.")
if pta {
mainPkgs := ssautil.MainPackages(prog.AllPackages())
log.Print("Transitively error-free main packages: ", mainPkgs)
a.pointer(mainPkgs)
}
}