| // Copyright 2018 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. |
| |
| // This file is largely based on the Go 1.10-era cmd/go/internal/test/test.go |
| // testmain generation code. |
| |
| package packages |
| |
| import ( |
| "errors" |
| "fmt" |
| "go/ast" |
| "go/doc" |
| "go/parser" |
| "go/token" |
| "os" |
| "sort" |
| "strings" |
| "text/template" |
| "unicode" |
| "unicode/utf8" |
| ) |
| |
| // TODO(matloob): Delete this file once Go 1.12 is released. |
| |
| // This file complements golist_fallback.go by providing |
| // support for generating testmains. |
| |
| func generateTestmain(out string, testPkg, xtestPkg *Package) (extraimports, extradeps []string, err error) { |
| testFuncs, err := loadTestFuncs(testPkg, xtestPkg) |
| if err != nil { |
| return nil, nil, err |
| } |
| extraimports = []string{"testing", "testing/internal/testdeps"} |
| if testFuncs.TestMain == nil { |
| extraimports = append(extraimports, "os") |
| } |
| // Transitive dependencies of ("testing", "testing/internal/testdeps"). |
| // os is part of the transitive closure so it and its transitive dependencies are |
| // included regardless of whether it's imported in the template below. |
| extradeps = []string{ |
| "errors", |
| "internal/cpu", |
| "unsafe", |
| "internal/bytealg", |
| "internal/race", |
| "runtime/internal/atomic", |
| "runtime/internal/sys", |
| "runtime", |
| "sync/atomic", |
| "sync", |
| "io", |
| "unicode", |
| "unicode/utf8", |
| "bytes", |
| "math", |
| "syscall", |
| "time", |
| "internal/poll", |
| "internal/syscall/unix", |
| "internal/testlog", |
| "os", |
| "math/bits", |
| "strconv", |
| "reflect", |
| "fmt", |
| "sort", |
| "strings", |
| "flag", |
| "runtime/debug", |
| "context", |
| "runtime/trace", |
| "testing", |
| "bufio", |
| "regexp/syntax", |
| "regexp", |
| "compress/flate", |
| "encoding/binary", |
| "hash", |
| "hash/crc32", |
| "compress/gzip", |
| "path/filepath", |
| "io/ioutil", |
| "text/tabwriter", |
| "runtime/pprof", |
| "testing/internal/testdeps", |
| } |
| return extraimports, extradeps, writeTestmain(out, testFuncs) |
| } |
| |
| // The following is adapted from the cmd/go testmain generation code. |
| |
| // isTestFunc tells whether fn has the type of a testing function. arg |
| // specifies the parameter type we look for: B, M or T. |
| func isTestFunc(fn *ast.FuncDecl, arg string) bool { |
| if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || |
| fn.Type.Params.List == 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. |
| // Same applies for B and T. |
| if name, ok := ptr.X.(*ast.Ident); ok && name.Name == arg { |
| return true |
| } |
| if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == arg { |
| 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) |
| } |
| |
| // loadTestFuncs returns the testFuncs describing the tests that will be run. |
| func loadTestFuncs(ptest, pxtest *Package) (*testFuncs, error) { |
| t := &testFuncs{ |
| TestPackage: ptest, |
| XTestPackage: pxtest, |
| } |
| for _, file := range ptest.GoFiles { |
| if !strings.HasSuffix(file, "_test.go") { |
| continue |
| } |
| if err := t.load(file, "_test", &t.ImportTest, &t.NeedTest); err != nil { |
| return nil, err |
| } |
| } |
| if pxtest != nil { |
| for _, file := range pxtest.GoFiles { |
| if err := t.load(file, "_xtest", &t.ImportXtest, &t.NeedXtest); err != nil { |
| return nil, err |
| } |
| } |
| } |
| return t, nil |
| } |
| |
| // writeTestmain writes the _testmain.go file for t to the file named out. |
| func writeTestmain(out string, t *testFuncs) error { |
| f, err := os.Create(out) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| if err := testmainTmpl.Execute(f, t); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| type testFuncs struct { |
| Tests []testFunc |
| Benchmarks []testFunc |
| Examples []testFunc |
| TestMain *testFunc |
| TestPackage *Package |
| XTestPackage *Package |
| ImportTest bool |
| NeedTest bool |
| ImportXtest bool |
| NeedXtest bool |
| } |
| |
| // Tested returns the name of the package being tested. |
| func (t *testFuncs) Tested() string { |
| return t.TestPackage.Name |
| } |
| |
| type testFunc struct { |
| Package string // imported package name (_test or _xtest) |
| Name string // function name |
| Output string // output, for examples |
| Unordered bool // output is allowed to be unordered. |
| } |
| |
| func (t *testFuncs) load(filename, pkg string, doImport, seen *bool) error { |
| var fset = token.NewFileSet() |
| |
| f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) |
| if err != nil { |
| return errors.New("failed to parse test file " + filename) |
| } |
| for _, d := range f.Decls { |
| n, ok := d.(*ast.FuncDecl) |
| if !ok { |
| continue |
| } |
| if n.Recv != nil { |
| continue |
| } |
| name := n.Name.String() |
| switch { |
| case name == "TestMain": |
| if isTestFunc(n, "T") { |
| t.Tests = append(t.Tests, testFunc{pkg, name, "", false}) |
| *doImport, *seen = true, true |
| continue |
| } |
| err := checkTestFunc(fset, n, "M") |
| if err != nil { |
| return err |
| } |
| if t.TestMain != nil { |
| return errors.New("multiple definitions of TestMain") |
| } |
| t.TestMain = &testFunc{pkg, name, "", false} |
| *doImport, *seen = true, true |
| case isTest(name, "Test"): |
| err := checkTestFunc(fset, n, "T") |
| if err != nil { |
| return err |
| } |
| t.Tests = append(t.Tests, testFunc{pkg, name, "", false}) |
| *doImport, *seen = true, true |
| case isTest(name, "Benchmark"): |
| err := checkTestFunc(fset, n, "B") |
| if err != nil { |
| return err |
| } |
| t.Benchmarks = append(t.Benchmarks, testFunc{pkg, name, "", false}) |
| *doImport, *seen = true, true |
| } |
| } |
| ex := doc.Examples(f) |
| sort.Slice(ex, func(i, j int) bool { return ex[i].Order < ex[j].Order }) |
| for _, e := range ex { |
| *doImport = true // import test file whether executed or not |
| if e.Output == "" && !e.EmptyOutput { |
| // Don't run examples with no output. |
| continue |
| } |
| t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output, e.Unordered}) |
| *seen = true |
| } |
| return nil |
| } |
| |
| func checkTestFunc(fset *token.FileSet, fn *ast.FuncDecl, arg string) error { |
| if !isTestFunc(fn, arg) { |
| name := fn.Name.String() |
| pos := fset.Position(fn.Pos()) |
| return fmt.Errorf("%s: wrong signature for %s, must be: func %s(%s *testing.%s)", pos, name, name, strings.ToLower(arg), arg) |
| } |
| return nil |
| } |
| |
| var testmainTmpl = template.Must(template.New("main").Parse(` |
| package main |
| |
| import ( |
| {{if not .TestMain}} |
| "os" |
| {{end}} |
| "testing" |
| "testing/internal/testdeps" |
| |
| {{if .ImportTest}} |
| {{if .NeedTest}}_test{{else}}_{{end}} {{.TestPackage.PkgPath | printf "%q"}} |
| {{end}} |
| {{if .ImportXtest}} |
| {{if .NeedXtest}}_xtest{{else}}_{{end}} {{.XTestPackage.PkgPath | printf "%q"}} |
| {{end}} |
| ) |
| |
| var tests = []testing.InternalTest{ |
| {{range .Tests}} |
| {"{{.Name}}", {{.Package}}.{{.Name}}}, |
| {{end}} |
| } |
| |
| var benchmarks = []testing.InternalBenchmark{ |
| {{range .Benchmarks}} |
| {"{{.Name}}", {{.Package}}.{{.Name}}}, |
| {{end}} |
| } |
| |
| var examples = []testing.InternalExample{ |
| {{range .Examples}} |
| {"{{.Name}}", {{.Package}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}}, |
| {{end}} |
| } |
| |
| func init() { |
| testdeps.ImportPath = {{.TestPackage.PkgPath | printf "%q"}} |
| } |
| |
| func main() { |
| m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, examples) |
| {{with .TestMain}} |
| {{.Package}}.{{.Name}}(m) |
| {{else}} |
| os.Exit(m.Run()) |
| {{end}} |
| } |
| |
| `)) |