268 lines
8.1 KiB
Go
268 lines
8.1 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssa
|
|
|
|
// CreateTestMainPackage synthesizes a main package that runs all the
|
|
// tests of the supplied packages.
|
|
// 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/types"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
// 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.
|
|
var testSig *types.Signature // func(*testing.T)
|
|
var benchmarkSig *types.Signature // func(*testing.B)
|
|
var exampleSig = types.NewSignature(nil, nil, nil, false) // func()
|
|
|
|
// Obtain the types from the parameters of testing.MainStart.
|
|
if testingPkg := prog.ImportedPackage("testing"); testingPkg != nil {
|
|
mainStart := testingPkg.Func("MainStart")
|
|
params := mainStart.Signature.Params()
|
|
testSig = funcField(params.At(1).Type())
|
|
benchmarkSig = funcField(params.At(2).Type())
|
|
|
|
// 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.
|
|
for _, mem := range pkg.Members {
|
|
if f, ok := mem.(*Function); ok &&
|
|
ast.IsExported(f.Name()) &&
|
|
strings.HasSuffix(prog.Fset.Position(f.Pos()).Filename, "_test.go") {
|
|
|
|
switch {
|
|
case testSig != nil && isTestSig(f, "Test", testSig):
|
|
tests = append(tests, f)
|
|
case benchmarkSig != nil && isTestSig(f, "Benchmark", benchmarkSig):
|
|
benchmarks = append(benchmarks, f)
|
|
case isTestSig(f, "Example", exampleSig):
|
|
examples = append(examples, f)
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Like isTest, but checks the signature too.
|
|
func isTestSig(f *Function, prefix string, sig *types.Signature) bool {
|
|
return isTest(f.Name(), prefix) && types.Identical(f.Signature, sig)
|
|
}
|
|
|
|
// Given the type of one of the three slice parameters of testing.Main,
|
|
// returns the function type.
|
|
func funcField(slice types.Type) *types.Signature {
|
|
return slice.(*types.Slice).Elem().Underlying().(*types.Struct).Field(1).Type().(*types.Signature)
|
|
}
|
|
|
|
// 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"
|
|
// 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.
|
|
//
|
|
// 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")
|
|
}
|
|
|
|
// Template data
|
|
var data struct {
|
|
Pkg *Package
|
|
Tests, Benchmarks, Examples []*Function
|
|
Main *Function
|
|
Go18 bool
|
|
}
|
|
data.Pkg = pkg
|
|
|
|
// 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
|
|
}
|
|
|
|
// Synthesize source for testmain package.
|
|
path := pkg.Pkg.Path() + "$testmain"
|
|
tmpl := testmainTmpl
|
|
if testingPkg := prog.ImportedPackage("testing"); testingPkg != nil {
|
|
// In Go 1.8, testing.MainStart's first argument is an interface, not a func.
|
|
data.Go18 = types.IsInterface(testingPkg.Func("MainStart").Signature.Params().At(0).Type())
|
|
} else {
|
|
// The program does not import "testing", but FindTests
|
|
// returned non-nil, which must mean there were Examples
|
|
// 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.)
|
|
tmpl = examplesOnlyTmpl
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
log.Fatalf("internal error expanding template for %s: %v", path, err)
|
|
}
|
|
if false { // debugging
|
|
fmt.Fprintln(os.Stderr, buf.String())
|
|
}
|
|
|
|
// Parse and type-check the testmain package.
|
|
f, err := parser.ParseFile(prog.Fset, path+".go", &buf, parser.Mode(0))
|
|
if err != nil {
|
|
log.Fatalf("internal error parsing %s: %v", path, err)
|
|
}
|
|
conf := types.Config{
|
|
DisableUnusedImportCheck: true,
|
|
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)
|
|
}
|
|
|
|
// Create and build SSA code.
|
|
testmain := prog.CreatePackage(testmainPkg, files, info, false)
|
|
testmain.SetDebugMode(false)
|
|
testmain.Build()
|
|
testmain.Func("main").Synthetic = "test main function"
|
|
testmain.Func("init").Synthetic = "package initializer"
|
|
return testmain
|
|
}
|
|
|
|
// An implementation of types.Importer for an already loaded SSA program.
|
|
type importer struct {
|
|
pkg *Package // package under test; may be non-importable
|
|
}
|
|
|
|
func (imp importer) Import(path string) (*types.Package, error) {
|
|
if p := imp.pkg.Prog.ImportedPackage(path); p != nil {
|
|
return p.Pkg, nil
|
|
}
|
|
if path == imp.pkg.Pkg.Path() {
|
|
return imp.pkg.Pkg, nil
|
|
}
|
|
return nil, fmt.Errorf("not found") // can't happen
|
|
}
|
|
|
|
var testmainTmpl = template.Must(template.New("testmain").Parse(`
|
|
package main
|
|
|
|
import "io"
|
|
import "os"
|
|
import "testing"
|
|
import p {{printf "%q" .Pkg.Pkg.Path}}
|
|
|
|
{{if .Go18}}
|
|
type deps struct{}
|
|
|
|
func (deps) ImportPath() string { return "" }
|
|
func (deps) MatchString(pat, str string) (bool, error) { return true, nil }
|
|
func (deps) StartCPUProfile(io.Writer) error { return nil }
|
|
func (deps) StartTestLog(io.Writer) {}
|
|
func (deps) StopCPUProfile() {}
|
|
func (deps) StopTestLog() error { return nil }
|
|
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}}
|
|
}
|
|
benchmarks := []testing.InternalBenchmark{
|
|
{{range .Benchmarks}}
|
|
{ {{printf "%q" .Name}}, p.{{.Name}} },
|
|
{{end}}
|
|
}
|
|
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}}
|
|
}
|
|
`))
|