1
0
mirror of https://github.com/golang/go synced 2024-11-18 19:24:39 -07:00

go/ssa: synthesize Go 1.8-compatible test main package

...to match the new interface between "go test" and the standard testing
package.

Rather than generate SSA code directly, which was tricky and fragile, we
now generate source just as "go test" does, type-check it, and build an
SSA package from it.  crawshaw suggested I do this in the very first
version of testmain.go, but at the time I believed it to be infeasible.

The testMainStartBodyHook mechanism has gone away; installations that
needed it can now achieve the same results more easily by overriding the
templates.

Tested with Go 1.6, 1.7 and 1.8.

Fixes golang/go#17722

Change-Id: I3ffd25f01157f6fb7a39acd18af46f17e9c07b99
Reviewed-on: https://go-review.googlesource.com/32888
Reviewed-by: Robert Griesemer <gri@golang.org>
This commit is contained in:
Alan Donovan 2016-11-07 21:18:49 -05:00
parent 09079c88dc
commit 7ce0cddaad

View File

@ -9,14 +9,20 @@ package ssa
// CreateTestMainPackage synthesizes a main package that runs all the // CreateTestMainPackage synthesizes a main package that runs all the
// tests of the supplied packages. // tests of the supplied packages.
// It is closely coupled to $GOROOT/src/cmd/go/test.go and $GOROOT/src/testing. // It is closely coupled to $GOROOT/src/cmd/go/test.go and $GOROOT/src/testing.
//
// TODO(adonovan): this file no longer needs to live in the ssa package.
// Move it to ssautil.
import ( import (
"bytes"
"fmt"
"go/ast" "go/ast"
"go/token" "go/parser"
"go/types" "go/types"
"log" "log"
"os" "os"
"strings" "strings"
"text/template"
) )
// FindTests returns the Test, Benchmark, and Example functions // FindTests returns the Test, Benchmark, and Example functions
@ -77,11 +83,25 @@ func isTestSig(f *Function, prefix string, sig *types.Signature) bool {
return isTest(f.Name(), prefix) && types.Identical(f.Signature, sig) return isTest(f.Name(), prefix) && types.Identical(f.Signature, sig)
} }
// If non-nil, testMainStartBodyHook is called immediately after // Given the type of one of the three slice parameters of testing.Main,
// startBody for main.init and main.main, making it easy for users to // returns the function type.
// add custom imports and initialization steps for proprietary build func funcField(slice types.Type) *types.Signature {
// systems that don't exactly follow 'go test' conventions. return slice.(*types.Slice).Elem().Underlying().(*types.Struct).Field(1).Type().(*types.Signature)
var testMainStartBodyHook func(*Function) }
// 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.
// Plundered from $GOROOT/src/cmd/go/test.go
func isTest(name, prefix string) bool {
if !strings.HasPrefix(name, prefix) {
return false
}
if len(name) == len(prefix) { // "Test" is ok
return true
}
return ast.IsExported(name[len(prefix):])
}
// CreateTestMainPackage creates and returns a synthetic "testmain" // CreateTestMainPackage creates and returns a synthetic "testmain"
// package for the specified package if it defines tests, benchmarks or // package for the specified package if it defines tests, benchmarks or
@ -96,113 +116,28 @@ func (prog *Program) CreateTestMainPackage(pkg *Package) *Package {
log.Fatal("Package does not belong to Program") log.Fatal("Package does not belong to Program")
} }
tests, benchmarks, examples, testMainFunc := FindTests(pkg) // Template data
var data struct {
Pkg *Package
Tests, Benchmarks, Examples []*Function
Main *Function
Go18 bool
}
data.Pkg = pkg
if testMainFunc == nil && tests == nil && benchmarks == nil && examples == nil { // Enumerate tests.
data.Tests, data.Benchmarks, data.Examples, data.Main = FindTests(pkg)
if data.Main == nil &&
data.Tests == nil && data.Benchmarks == nil && data.Examples == nil {
return nil return nil
} }
testmain := &Package{ // Synthesize source for testmain package.
Prog: prog, path := pkg.Pkg.Path() + "$testmain"
Members: make(map[string]Member), tmpl := testmainTmpl
values: make(map[types.Object]Value),
Pkg: types.NewPackage(pkg.Pkg.Path()+"$testmain", "main"),
}
// Build package's init function.
init := &Function{
name: "init",
Signature: new(types.Signature),
Synthetic: "package initializer",
Pkg: testmain,
Prog: prog,
}
init.startBody()
if testMainStartBodyHook != nil {
testMainStartBodyHook(init)
}
// Initialize package under test.
var v Call
v.Call.Value = pkg.init
v.setType(types.NewTuple())
init.emit(&v)
init.emit(new(Return))
init.finishBody()
testmain.init = init
testmain.Pkg.MarkComplete()
testmain.Members[init.name] = init
// 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")
main.Synthetic = "test main function"
main.startBody()
if testMainStartBodyHook != nil {
testMainStartBodyHook(main)
}
if testingPkg := prog.ImportedPackage("testing"); testingPkg != nil { if testingPkg := prog.ImportedPackage("testing"); testingPkg != nil {
testingMain := testingPkg.Func("Main") // In Go 1.8, testing.MainStart's first argument is an interface, not a func.
testingMainParams := testingMain.Signature.Params() data.Go18 = types.IsInterface(testingPkg.Func("MainStart").Signature.Params().At(0).Type())
// The generated code is as if compiled from this:
//
// func main() {
// match := func(_, _ string) (bool, error) { return true, nil }
// tests := []testing.InternalTest{{"TestFoo", TestFoo}, ...}
// benchmarks := []testing.InternalBenchmark{...}
// examples := []testing.InternalExample{...}
// if TestMain is defined {
// m := testing.MainStart(match, tests, benchmarks, examples)
// return TestMain(m)
// } else {
// return testing.Main(match, tests, benchmarks, examples)
// }
// }
matcher := &Function{
name: "matcher",
Signature: testingMainParams.At(0).Type().(*types.Signature),
Synthetic: "test matcher predicate",
parent: main,
Pkg: testmain,
Prog: prog,
}
main.AnonFuncs = append(main.AnonFuncs, matcher)
matcher.startBody()
matcher.emit(&Return{Results: []Value{vTrue, nilConst(types.Universe.Lookup("error").Type())}})
matcher.finishBody()
var c Call
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 { } else {
// The program does not import "testing", but FindTests // The program does not import "testing", but FindTests
// returned non-nil, which must mean there were Examples // returned non-nil, which must mean there were Examples
@ -213,105 +148,119 @@ func (prog *Program) CreateTestMainPackage(pkg *Package) *Package {
// "Output:" comments. // "Output:" comments.
// (We should not execute an Example that has no // (We should not execute an Example that has no
// "Output:" comment, but it's impossible to tell here.) // "Output:" comment, but it's impossible to tell here.)
for _, eg := range examples { tmpl = examplesOnlyTmpl
var c Call }
c.Call.Value = eg var buf bytes.Buffer
c.setType(types.NewTuple()) if err := tmpl.Execute(&buf, data); err != nil {
main.emit(&c) log.Fatalf("internal error expanding template for %s: %v", path, err)
} }
main.emit(&Return{}) if false { // debugging
main.currentBlock = nil fmt.Fprintln(os.Stderr, buf.String())
} }
main.finishBody() // Parse and type-check the testmain package.
f, err := parser.ParseFile(prog.Fset, path+".go", &buf, parser.Mode(0))
testmain.Members["main"] = main if err != nil {
log.Fatalf("internal error parsing %s: %v", path, err)
if prog.mode&PrintPackages != 0 { }
printMu.Lock() conf := types.Config{
testmain.WriteTo(os.Stdout) DisableUnusedImportCheck: true,
printMu.Unlock() Importer: importer{pkg},
}
files := []*ast.File{f}
info := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Implicits: make(map[ast.Node]types.Object),
Scopes: make(map[ast.Node]*types.Scope),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
}
testmainPkg, err := conf.Check(path, prog.Fset, files, info)
if err != nil {
log.Fatalf("internal error type-checking %s: %v", path, err)
} }
if prog.mode&SanityCheckFunctions != 0 { // Create and build SSA code.
sanityCheckPackage(testmain) testmain := prog.CreatePackage(testmainPkg, files, info, false)
} testmain.SetDebugMode(false)
testmain.Build()
prog.packages[testmain.Pkg] = testmain testmain.Func("main").Synthetic = "test main function"
testmain.Func("init").Synthetic = "package initializer"
return testmain return testmain
} }
// testMainSlice emits to fn code to construct a slice of type slice // An implementation of types.Importer for an already loaded SSA program.
// (one of []testing.Internal{Test,Benchmark,Example}) for all type importer struct {
// functions in testfuncs. It returns the slice value. pkg *Package // package under test; may be non-importable
//
func testMainSlice(fn *Function, testfuncs []*Function, slice types.Type) Value {
if testfuncs == nil {
return nilConst(slice)
}
tElem := slice.(*types.Slice).Elem()
tPtrString := types.NewPointer(tString)
tPtrElem := types.NewPointer(tElem)
tPtrFunc := types.NewPointer(funcField(slice))
// TODO(adonovan): fix: populate the
// testing.InternalExample.Output field correctly so that tests
// work correctly under the interpreter. This requires that we
// do this step using ASTs, not *ssa.Functions---quite a
// redesign. See also the fake runExample in go/ssa/interp.
// Emit: array = new [n]testing.InternalTest
tArray := types.NewArray(tElem, int64(len(testfuncs)))
array := emitNew(fn, tArray, token.NoPos)
array.Comment = "test main"
for i, testfunc := range testfuncs {
// Emit: pitem = &array[i]
ia := &IndexAddr{X: array, Index: intConst(int64(i))}
ia.setType(tPtrElem)
pitem := fn.emit(ia)
// Emit: pname = &pitem.Name
fa := &FieldAddr{X: pitem, Field: 0} // .Name
fa.setType(tPtrString)
pname := fn.emit(fa)
// Emit: *pname = "testfunc"
emitStore(fn, pname, stringConst(testfunc.Name()), token.NoPos)
// Emit: pfunc = &pitem.F
fa = &FieldAddr{X: pitem, Field: 1} // .F
fa.setType(tPtrFunc)
pfunc := fn.emit(fa)
// Emit: *pfunc = testfunc
emitStore(fn, pfunc, testfunc, token.NoPos)
}
// Emit: slice array[:]
sl := &Slice{X: array}
sl.setType(slice)
return fn.emit(sl)
} }
// Given the type of one of the three slice parameters of testing.Main, func (imp importer) Import(path string) (*types.Package, error) {
// returns the function type. if p := imp.pkg.Prog.ImportedPackage(path); p != nil {
func funcField(slice types.Type) *types.Signature { return p.Pkg, nil
return slice.(*types.Slice).Elem().Underlying().(*types.Struct).Field(1).Type().(*types.Signature) }
if path == imp.pkg.Pkg.Path() {
return imp.pkg.Pkg, nil
}
return nil, fmt.Errorf("not found") // can't happen
} }
// Plundered from $GOROOT/src/cmd/go/test.go var testmainTmpl = template.Must(template.New("testmain").Parse(`
package main
// isTest tells whether name looks like a test (or benchmark, according to prefix). import "io"
// It is a Test (say) if there is a character after Test that is not a lower-case letter. import "os"
// We don't want TesticularCancer. import "testing"
func isTest(name, prefix string) bool { import p {{printf "%q" .Pkg.Pkg.Path}}
if !strings.HasPrefix(name, prefix) {
return false {{if .Go18}}
type deps struct{}
func (deps) MatchString(pat, str string) (bool, error) { return true, nil }
func (deps) StartCPUProfile(io.Writer) error { return nil }
func (deps) StopCPUProfile() {}
func (deps) WriteHeapProfile(io.Writer) error { return nil }
func (deps) WriteProfileTo(string, io.Writer, int) error { return nil }
var match deps
{{else}}
func match(_, _ string) (bool, error) { return true, nil }
{{end}}
func main() {
tests := []testing.InternalTest{
{{range .Tests}}
{ {{printf "%q" .Name}}, p.{{.Name}} },
{{end}}
} }
if len(name) == len(prefix) { // "Test" is ok benchmarks := []testing.InternalBenchmark{
return true {{range .Benchmarks}}
{ {{printf "%q" .Name}}, p.{{.Name}} },
{{end}}
} }
return ast.IsExported(name[len(prefix):]) examples := []testing.InternalExample{
{{range .Examples}}
{Name: {{printf "%q" .Name}}, F: p.{{.Name}}},
{{end}}
}
m := testing.MainStart(match, tests, benchmarks, examples)
{{with .Main}}
p.{{.Name}}(m)
{{else}}
os.Exit(m.Run())
{{end}}
} }
`))
var examplesOnlyTmpl = template.Must(template.New("examples").Parse(`
package main
import p {{printf "%q" .Pkg.Pkg.Path}}
func main() {
{{range .Examples}}
p.{{.Name}}()
{{end}}
}
`))