blob: f1b3257d586c79646e01f1e833bf1499fbfee78a [file]
package testmain
import (
"bytes"
"errors"
"fmt"
"go/ast"
gobuild "go/build"
"go/doc"
"go/parser"
"go/token"
"path"
"sort"
"strings"
"text/template"
"unicode"
"unicode/utf8"
"github.com/gopherjs/gopherjs/build"
"golang.org/x/tools/go/buildutil"
)
// FuncLocation describes whether a test function is in-package or external
// (i.e. in the xxx_test package).
type FuncLocation uint8
const (
// LocUnknown is the default, invalid value of the PkgType.
LocUnknown FuncLocation = iota
// LocInPackage is an in-package test.
LocInPackage
// LocExternal is an external test (i.e. in the xxx_test package).
LocExternal
)
func (tl FuncLocation) String() string {
switch tl {
case LocInPackage:
return "_test"
case LocExternal:
return "_xtest"
default:
return "<unknown>"
}
}
// TestFunc describes a single test/benchmark/fuzz function in a package.
type TestFunc struct {
Location FuncLocation // Where the function is defined.
Name string // Function name.
}
// ExampleFunc describes an example.
type ExampleFunc struct {
Location FuncLocation // Where the function is defined.
Name string // Function name.
Output string // Expected output.
Unordered bool // Output is allowed to be unordered.
EmptyOutput bool // Whether the output is expected to be empty.
}
// Executable returns true if the example function should be executed with tests.
func (ef ExampleFunc) Executable() bool {
return ef.EmptyOutput || ef.Output != ""
}
// TestMain is a helper type responsible for generation of the test main package.
type TestMain struct {
Package *build.PackageData
Tests []TestFunc
Benchmarks []TestFunc
Fuzz []TestFunc
Examples []ExampleFunc
TestMain *TestFunc
}
// Scan package for tests functions.
func (tm *TestMain) Scan(fset *token.FileSet) error {
if err := tm.scanPkg(fset, tm.Package.TestGoFiles, LocInPackage); err != nil {
return err
}
if err := tm.scanPkg(fset, tm.Package.XTestGoFiles, LocExternal); err != nil {
return err
}
return nil
}
func (tm *TestMain) scanPkg(fset *token.FileSet, files []string, loc FuncLocation) error {
for _, name := range files {
srcPath := path.Join(tm.Package.Dir, name)
f, err := buildutil.OpenFile(tm.Package.InternalBuildContext(), srcPath)
if err != nil {
return fmt.Errorf("failed to open source file %q: %w", srcPath, err)
}
defer f.Close()
parsed, err := parser.ParseFile(fset, srcPath, f, parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse %q: %w", srcPath, err)
}
if err := tm.scanFile(parsed, loc); err != nil {
return err
}
}
return nil
}
func (tm *TestMain) scanFile(f *ast.File, loc FuncLocation) error {
for _, d := range f.Decls {
n, ok := d.(*ast.FuncDecl)
if !ok {
continue
}
if n.Recv != nil {
continue
}
name := n.Name.String()
switch {
case isTestMain(n):
if tm.TestMain != nil {
return errors.New("multiple definitions of TestMain")
}
tm.TestMain = &TestFunc{
Location: loc,
Name: name,
}
case isTest(name, "Test"):
tm.Tests = append(tm.Tests, TestFunc{
Location: loc,
Name: name,
})
case isTest(name, "Benchmark"):
tm.Benchmarks = append(tm.Benchmarks, TestFunc{
Location: loc,
Name: name,
})
case isTest(name, "Fuzz"):
tm.Fuzz = append(tm.Fuzz, TestFunc{
Location: loc,
Name: name,
})
}
}
ex := doc.Examples(f)
sort.Slice(ex, func(i, j int) bool { return ex[i].Order < ex[j].Order })
for _, e := range ex {
tm.Examples = append(tm.Examples, ExampleFunc{
Location: loc,
Name: "Example" + e.Name,
Output: e.Output,
Unordered: e.Unordered,
EmptyOutput: e.EmptyOutput,
})
}
return nil
}
// Synthesize main package for the tests.
func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.File, error) {
buf := &bytes.Buffer{}
if err := testmainTmpl.Execute(buf, tm); err != nil {
return nil, nil, fmt.Errorf("failed to generate testmain source for package %s: %w", tm.Package.ImportPath, err)
}
src, err := parser.ParseFile(fset, "_testmain.go", buf, 0)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse testmain source for package %s: %w", tm.Package.ImportPath, err)
}
pkg := &build.PackageData{
Package: &gobuild.Package{
ImportPath: tm.Package.ImportPath + ".testmain",
Name: "main",
GoFiles: []string{"_testmain.go"},
},
}
return pkg, src, nil
}
func (tm *TestMain) hasTests(loc FuncLocation, executableOnly bool) bool {
if tm.TestMain != nil && tm.TestMain.Location == loc {
return true
}
// Tests, Benchmarks and Fuzz targets are always executable.
all := []TestFunc{}
all = append(all, tm.Tests...)
all = append(all, tm.Benchmarks...)
for _, t := range all {
if t.Location == loc {
return true
}
}
for _, e := range tm.Examples {
if e.Location == loc && (e.Executable() || !executableOnly) {
return true
}
}
return false
}
// ImportTest returns true if in-package test package needs to be imported.
func (tm *TestMain) ImportTest() bool { return tm.hasTests(LocInPackage, false) }
// ImportXTest returns true if external test package needs to be imported.
func (tm *TestMain) ImportXTest() bool { return tm.hasTests(LocExternal, false) }
// ExecutesTest returns true if in-package test package has executable tests.
func (tm *TestMain) ExecutesTest() bool { return tm.hasTests(LocInPackage, true) }
// ExecutesXTest returns true if external package test package has executable tests.
func (tm *TestMain) ExecutesXTest() bool { return tm.hasTests(LocExternal, true) }
// isTestMain tells whether fn is a TestMain(m *testing.M) function.
func isTestMain(fn *ast.FuncDecl) bool {
if fn.Name.String() != "TestMain" ||
fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
fn.Type.Params == nil ||
len(fn.Type.Params.List) != 1 ||
len(fn.Type.Params.List[0].Names) > 1 {
return false
}
ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
if !ok {
return false
}
// We can't easily check that the type is *testing.M
// because we don't know how testing has been imported,
// but at least check that it's *M or *something.M.
if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "M" {
return true
}
if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "M" {
return true
}
return false
}
// 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.
func isTest(name, prefix string) bool {
if !strings.HasPrefix(name, prefix) {
return false
}
if len(name) == len(prefix) { // "Test" is ok
return true
}
rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
return !unicode.IsLower(rune)
}
var testmainTmpl = template.Must(template.New("main").Parse(`
package main
import (
{{if not .TestMain}}
"os"
{{end}}
"testing"
"testing/internal/testdeps"
{{if .ImportTest}}
{{if .ExecutesTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
{{end -}}
{{- if .ImportXTest -}}
{{if .ExecutesXTest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
)
var tests = []testing.InternalTest{
{{- range .Tests}}
{"{{.Name}}", {{.Location}}.{{.Name}}},
{{- end}}
}
var benchmarks = []testing.InternalBenchmark{
{{- range .Benchmarks}}
{"{{.Name}}", {{.Location}}.{{.Name}}},
{{- end}}
}
var fuzzTargets = []testing.InternalFuzzTarget{
{{- range .Fuzz}}
{"{{.Name}}", {{.Location}}.{{.Name}}},
{{- end}}
}
var examples = []testing.InternalExample{
{{- range .Examples }}
{{- if .Executable }}
{"{{.Name}}", {{.Location}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}},
{{- end }}
{{- end }}
}
func main() {
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples)
{{with .TestMain}}
{{.Location}}.{{.Name}}(m)
{{else}}
os.Exit(m.Run())
{{end -}}
}
`))