Commit 296e6765 authored by Russ Cox's avatar Russ Cox

cmd/go: add go list -test to describe test binaries

Tools should be able to ask cmd/go about the dependency
graph for test binaries instead of reinventing it themselves.
Allow them to do so, with the new list -test flag.

This also fixes and tests for a bug introduced in CL 104315
that was not properly splitting dependencies on the path
between package main and the package being tested.

Change-Id: I29eb454c82893f5ee70252aaaecd9fa376eaf3c8
Reviewed-on: https://go-review.googlesource.com/107916
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: 's avatarBryan C. Mills <bcmills@google.com>
parent c5f0104d
...@@ -697,6 +697,18 @@ ...@@ -697,6 +697,18 @@
// a non-nil Error field; other information may or may not be missing // a non-nil Error field; other information may or may not be missing
// (zeroed). // (zeroed).
// //
// The -test flag causes list to add to its output test binaries for the
// named packages that have tests, to make information about test
// binary construction available to source code analysis tools.
// The reported import path for a test binary is the import path of
// the package followed by a ".test" suffix, as in "math/rand.test".
// When building a test, it is sometimes necessary to rebuild certain
// dependencies specially for that test (most commonly the tested
// package itself). The reported import path of a package recompiled
// for a particular test binary is followed by a space and the name of
// the test binary in brackets, as in "math/rand [math/rand.test]"
// or "regexp [sort.test]".
//
// For more about build flags, see 'go help build'. // For more about build flags, see 'go help build'.
// //
// For more about specifying packages, see 'go help packages'. // For more about specifying packages, see 'go help packages'.
......
...@@ -1919,6 +1919,35 @@ func TestGoListDeps(t *testing.T) { ...@@ -1919,6 +1919,35 @@ func TestGoListDeps(t *testing.T) {
} }
} }
func TestGoListTest(t *testing.T) {
tg := testgo(t)
defer tg.cleanup()
tg.parallel()
tg.makeTempdir()
tg.setenv("GOCACHE", tg.tempdir)
tg.run("list", "-test", "-deps", "sort")
tg.grepStdout(`^sort.test$`, "missing test main")
tg.grepStdout(`^sort$`, "missing real sort")
tg.grepStdout(`^sort \[sort.test\]$`, "missing test copy of sort")
tg.grepStdout(`^testing \[sort.test\]$`, "missing test copy of testing")
tg.grepStdoutNot(`^testing$`, "unexpected real copy of testing")
tg.run("list", "-test", "sort")
tg.grepStdout(`^sort.test$`, "missing test main")
tg.grepStdout(`^sort$`, "missing real sort")
tg.grepStdoutNot(`^sort \[sort.test\]$`, "unexpected test copy of sort")
tg.grepStdoutNot(`^testing \[sort.test\]$`, "unexpected test copy of testing")
tg.grepStdoutNot(`^testing$`, "unexpected real copy of testing")
tg.run("list", "-test", "cmd/dist", "cmd/doc")
tg.grepStdout(`^cmd/dist$`, "missing cmd/dist")
tg.grepStdout(`^cmd/doc$`, "missing cmd/doc")
tg.grepStdout(`^cmd/doc\.test$`, "missing cmd/doc test")
tg.grepStdoutNot(`^cmd/dist\.test$`, "unexpected cmd/dist test")
tg.grepStdoutNot(`^testing`, "unexpected testing")
}
// Issue 4096. Validate the output of unsuccessful go install foo/quxx. // Issue 4096. Validate the output of unsuccessful go install foo/quxx.
func TestUnsuccessfulGoInstallShouldMentionMissingPackage(t *testing.T) { func TestUnsuccessfulGoInstallShouldMentionMissingPackage(t *testing.T) {
tg := testgo(t) tg := testgo(t)
......
...@@ -7,13 +7,16 @@ package list ...@@ -7,13 +7,16 @@ package list
import ( import (
"bufio" "bufio"
"bytes"
"encoding/json" "encoding/json"
"io" "io"
"os" "os"
"sort"
"strings" "strings"
"text/template" "text/template"
"cmd/go/internal/base" "cmd/go/internal/base"
"cmd/go/internal/cache"
"cmd/go/internal/cfg" "cmd/go/internal/cfg"
"cmd/go/internal/load" "cmd/go/internal/load"
"cmd/go/internal/work" "cmd/go/internal/work"
...@@ -139,6 +142,18 @@ printing. Erroneous packages will have a non-empty ImportPath and ...@@ -139,6 +142,18 @@ printing. Erroneous packages will have a non-empty ImportPath and
a non-nil Error field; other information may or may not be missing a non-nil Error field; other information may or may not be missing
(zeroed). (zeroed).
The -test flag causes list to report not only the named packages
but also their test binaries (for packages with tests), to convey to
source code analysis tools exactly how test binaries are constructed.
The reported import path for a test binary is the import path of
the package followed by a ".test" suffix, as in "math/rand.test".
When building a test, it is sometimes necessary to rebuild certain
dependencies specially for that test (most commonly the tested
package itself). The reported import path of a package recompiled
for a particular test binary is followed by a space and the name of
the test binary in brackets, as in "math/rand [math/rand.test]"
or "regexp [sort.test]".
For more about build flags, see 'go help build'. For more about build flags, see 'go help build'.
For more about specifying packages, see 'go help packages'. For more about specifying packages, see 'go help packages'.
...@@ -154,6 +169,7 @@ var listDeps = CmdList.Flag.Bool("deps", false, "") ...@@ -154,6 +169,7 @@ var listDeps = CmdList.Flag.Bool("deps", false, "")
var listE = CmdList.Flag.Bool("e", false, "") var listE = CmdList.Flag.Bool("e", false, "")
var listFmt = CmdList.Flag.String("f", "{{.ImportPath}}", "") var listFmt = CmdList.Flag.String("f", "{{.ImportPath}}", "")
var listJson = CmdList.Flag.Bool("json", false, "") var listJson = CmdList.Flag.Bool("json", false, "")
var listTest = CmdList.Flag.Bool("test", false, "")
var nl = []byte{'\n'} var nl = []byte{'\n'}
func runList(cmd *base.Command, args []string) { func runList(cmd *base.Command, args []string) {
...@@ -206,12 +222,60 @@ func runList(cmd *base.Command, args []string) { ...@@ -206,12 +222,60 @@ func runList(cmd *base.Command, args []string) {
pkgs = load.Packages(args) pkgs = load.Packages(args)
} }
if *listTest {
c := cache.Default()
if c == nil {
base.Fatalf("go list -test requires build cache")
}
// Add test binaries to packages to be listed.
for _, p := range pkgs {
if p.Error != nil {
continue
}
if len(p.TestGoFiles)+len(p.XTestGoFiles) > 0 {
pmain, _, _, err := load.TestPackagesFor(p, nil)
if err != nil {
if !*listE {
base.Errorf("can't load test package: %s", err)
continue
}
pmain = &load.Package{
PackagePublic: load.PackagePublic{
ImportPath: p.ImportPath + ".test",
Error: &load.PackageError{Err: err.Error()},
},
}
}
pkgs = append(pkgs, pmain)
data := *pmain.Internal.TestmainGo
h := cache.NewHash("testmain")
h.Write([]byte("testmain\n"))
h.Write(data)
out, _, err := c.Put(h.Sum(), bytes.NewReader(data))
if err != nil {
base.Fatalf("%s", err)
}
pmain.GoFiles[0] = c.OutputFile(out)
}
}
}
// Remember which packages are named on the command line.
cmdline := make(map[*load.Package]bool)
for _, p := range pkgs {
cmdline[p] = true
}
if *listDeps { if *listDeps {
// Note: This changes the order of the listed packages // Note: This changes the order of the listed packages
// from "as written on the command line" to // from "as written on the command line" to
// "a depth-first post-order traversal". // "a depth-first post-order traversal".
// (The dependency exploration order for a given node // (The dependency exploration order for a given node
// is alphabetical, same as listed in .Deps.) // is alphabetical, same as listed in .Deps.)
// Note that -deps is applied after -test,
// so that you only get descriptions of tests for the things named
// explicitly on the command line, not for all dependencies.
pkgs = load.PackageList(pkgs) pkgs = load.PackageList(pkgs)
} }
...@@ -230,12 +294,53 @@ func runList(cmd *base.Command, args []string) { ...@@ -230,12 +294,53 @@ func runList(cmd *base.Command, args []string) {
b.Do(a) b.Do(a)
} }
for _, pkg := range pkgs { for _, p := range pkgs {
// Show vendor-expanded paths in listing // Show vendor-expanded paths in listing
pkg.TestImports = pkg.Vendored(pkg.TestImports) p.TestImports = p.Vendored(p.TestImports)
pkg.XTestImports = pkg.Vendored(pkg.XTestImports) p.XTestImports = p.Vendored(p.XTestImports)
}
if *listTest {
all := pkgs
if !*listDeps {
all = load.PackageList(pkgs)
}
// Update import paths to distinguish the real package p
// from p recompiled for q.test.
// This must happen only once the build code is done
// looking at import paths, because it will get very confused
// if it sees these.
for _, p := range all {
if p.ForTest != "" {
p.ImportPath += " [" + p.ForTest + ".test]"
}
p.DepOnly = !cmdline[p]
}
// Update import path lists to use new strings.
for _, p := range all {
for i := range p.Imports {
p.Imports[i] = p.Internal.Imports[i].ImportPath
}
}
// Recompute deps lists using new strings, from the leaves up.
for _, p := range all {
deps := make(map[string]bool)
for _, p1 := range p.Internal.Imports {
deps[p1.ImportPath] = true
for _, d := range p1.Deps {
deps[d] = true
}
}
p.Deps = make([]string, 0, len(deps))
for d := range deps {
p.Deps = append(p.Deps, d)
}
sort.Strings(p.Deps)
}
}
do(&pkg.PackagePublic) for _, p := range pkgs {
do(&p.PackagePublic)
} }
} }
......
...@@ -47,6 +47,8 @@ type PackagePublic struct { ...@@ -47,6 +47,8 @@ type PackagePublic struct {
Root string `json:",omitempty"` // Go root or Go path dir containing this package Root string `json:",omitempty"` // Go root or Go path dir containing this package
ConflictDir string `json:",omitempty"` // Dir is hidden by this other directory ConflictDir string `json:",omitempty"` // Dir is hidden by this other directory
BinaryOnly bool `json:",omitempty"` // package cannot be recompiled BinaryOnly bool `json:",omitempty"` // package cannot be recompiled
ForTest string `json:",omitempty"` // package is only for use in named test
DepOnly bool `json:",omitempty"` // package is only as a dependency, not explicitly listed
// Stale and StaleReason remain here *only* for the list command. // Stale and StaleReason remain here *only* for the list command.
// They are only initialized in preparation for list execution. // They are only initialized in preparation for list execution.
...@@ -135,6 +137,7 @@ type PackageInternal struct { ...@@ -135,6 +137,7 @@ type PackageInternal struct {
CoverVars map[string]*CoverVar // variables created by coverage analysis CoverVars map[string]*CoverVar // variables created by coverage analysis
OmitDebug bool // tell linker not to write debug information OmitDebug bool // tell linker not to write debug information
GobinSubdir bool // install target would be subdir of GOBIN GobinSubdir bool // install target would be subdir of GOBIN
TestmainGo *[]byte // content for _testmain.go
Asmflags []string // -asmflags for this package Asmflags []string // -asmflags for this package
Gcflags []string // -gcflags for this package Gcflags []string // -gcflags for this package
......
...@@ -5,16 +5,54 @@ ...@@ -5,16 +5,54 @@
package load package load
import ( import (
"bytes"
"cmd/go/internal/base"
"cmd/go/internal/str" "cmd/go/internal/str"
"errors"
"fmt"
"go/ast"
"go/build" "go/build"
"go/doc"
"go/parser"
"go/token" "go/token"
"path/filepath"
"sort"
"strings"
"text/template"
"unicode"
"unicode/utf8"
) )
// TestPackagesFor returns package structs ptest, the package p plus var TestMainDeps = []string{
// its test files, and pxtest, the external tests of package p. // Dependencies for testmain.
// pxtest may be nil. If there are no test files, forceTest decides "os",
// whether this returns a new package struct or just returns p. "testing",
func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err error) { "testing/internal/testdeps",
}
type TestCover struct {
Mode string
Local bool
Pkgs []*Package
Paths []string
Vars []coverInfo
DeclVars func(string, ...string) map[string]*CoverVar
}
// TestPackagesFor returns three packages:
// - ptest, the package p compiled with added "package p" test files.
// - pxtest, the result of compiling any "package p_test" (external) test files.
// - pmain, the package main corresponding to the test binary (running tests in ptest and pxtest).
//
// If the package has no "package p_test" test files, pxtest will be nil.
// If the non-test compilation of package p can be reused
// (for example, if there are no "package p" test files and
// package p need not be instrumented for coverage or any other reason),
// then the returned ptest == p.
//
// The caller is expected to have checked that len(p.TestGoFiles)+len(p.XTestGoFiles) > 0,
// or else there's no point in any of this.
func TestPackagesFor(p *Package, cover *TestCover) (pmain, ptest, pxtest *Package, err error) {
var imports, ximports []*Package var imports, ximports []*Package
var stk ImportStack var stk ImportStack
stk.Push(p.ImportPath + " (test)") stk.Push(p.ImportPath + " (test)")
...@@ -22,12 +60,12 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er ...@@ -22,12 +60,12 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er
for i, path := range p.TestImports { for i, path := range p.TestImports {
p1 := LoadImport(path, p.Dir, p, &stk, p.Internal.Build.TestImportPos[path], UseVendor) p1 := LoadImport(path, p.Dir, p, &stk, p.Internal.Build.TestImportPos[path], UseVendor)
if p1.Error != nil { if p1.Error != nil {
return nil, nil, p1.Error return nil, nil, nil, p1.Error
} }
if len(p1.DepsErrors) > 0 { if len(p1.DepsErrors) > 0 {
err := p1.DepsErrors[0] err := p1.DepsErrors[0]
err.Pos = "" // show full import stack err.Pos = "" // show full import stack
return nil, nil, err return nil, nil, nil, err
} }
if str.Contains(p1.Deps, p.ImportPath) || p1.ImportPath == p.ImportPath { if str.Contains(p1.Deps, p.ImportPath) || p1.ImportPath == p.ImportPath {
// Same error that loadPackage returns (via reusePackage) in pkg.go. // Same error that loadPackage returns (via reusePackage) in pkg.go.
...@@ -38,7 +76,7 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er ...@@ -38,7 +76,7 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er
Err: "import cycle not allowed in test", Err: "import cycle not allowed in test",
IsImportCycle: true, IsImportCycle: true,
} }
return nil, nil, err return nil, nil, nil, err
} }
p.TestImports[i] = p1.ImportPath p.TestImports[i] = p1.ImportPath
imports = append(imports, p1) imports = append(imports, p1)
...@@ -50,12 +88,12 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er ...@@ -50,12 +88,12 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er
for i, path := range p.XTestImports { for i, path := range p.XTestImports {
p1 := LoadImport(path, p.Dir, p, &stk, p.Internal.Build.XTestImportPos[path], UseVendor) p1 := LoadImport(path, p.Dir, p, &stk, p.Internal.Build.XTestImportPos[path], UseVendor)
if p1.Error != nil { if p1.Error != nil {
return nil, nil, p1.Error return nil, nil, nil, p1.Error
} }
if len(p1.DepsErrors) > 0 { if len(p1.DepsErrors) > 0 {
err := p1.DepsErrors[0] err := p1.DepsErrors[0]
err.Pos = "" // show full import stack err.Pos = "" // show full import stack
return nil, nil, err return nil, nil, nil, err
} }
if p1.ImportPath == p.ImportPath { if p1.ImportPath == p.ImportPath {
pxtestNeedsPtest = true pxtestNeedsPtest = true
...@@ -67,9 +105,10 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er ...@@ -67,9 +105,10 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er
stk.Pop() stk.Pop()
// Test package. // Test package.
if len(p.TestGoFiles) > 0 || forceTest { if len(p.TestGoFiles) > 0 || p.Name == "main" || cover != nil && cover.Local {
ptest = new(Package) ptest = new(Package)
*ptest = *p *ptest = *p
ptest.ForTest = p.ImportPath
ptest.GoFiles = nil ptest.GoFiles = nil
ptest.GoFiles = append(ptest.GoFiles, p.GoFiles...) ptest.GoFiles = append(ptest.GoFiles, p.GoFiles...)
ptest.GoFiles = append(ptest.GoFiles, p.TestGoFiles...) ptest.GoFiles = append(ptest.GoFiles, p.TestGoFiles...)
...@@ -113,6 +152,7 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er ...@@ -113,6 +152,7 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er
Dir: p.Dir, Dir: p.Dir,
GoFiles: p.XTestGoFiles, GoFiles: p.XTestGoFiles,
Imports: p.XTestImports, Imports: p.XTestImports,
ForTest: p.ImportPath,
}, },
Internal: PackageInternal{ Internal: PackageInternal{
LocalPrefix: p.Internal.LocalPrefix, LocalPrefix: p.Internal.LocalPrefix,
...@@ -133,19 +173,113 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er ...@@ -133,19 +173,113 @@ func TestPackagesFor(p *Package, forceTest bool) (ptest, pxtest *Package, err er
} }
} }
if p != ptest && pxtest != nil { // Build main package.
pmain = &Package{
PackagePublic: PackagePublic{
Name: "main",
Dir: p.Dir,
GoFiles: []string{"_testmain.go"},
ImportPath: p.ImportPath + ".test",
Root: p.Root,
},
Internal: PackageInternal{
Build: &build.Package{Name: "main"},
Asmflags: p.Internal.Asmflags,
Gcflags: p.Internal.Gcflags,
Ldflags: p.Internal.Ldflags,
Gccgoflags: p.Internal.Gccgoflags,
},
}
// The generated main also imports testing, regexp, and os.
// Also the linker introduces implicit dependencies reported by LinkerDeps.
stk.Push("testmain")
deps := TestMainDeps // cap==len, so safe for append
for _, d := range LinkerDeps(p) {
deps = append(deps, d)
}
for _, dep := range deps {
if dep == ptest.ImportPath {
pmain.Internal.Imports = append(pmain.Internal.Imports, ptest)
} else {
p1 := LoadImport(dep, "", nil, &stk, nil, 0)
if p1.Error != nil {
return nil, nil, nil, p1.Error
}
pmain.Internal.Imports = append(pmain.Internal.Imports, p1)
}
}
stk.Pop()
if cover != nil && cover.Pkgs != nil {
// Add imports, but avoid duplicates.
seen := map[*Package]bool{p: true, ptest: true}
for _, p1 := range pmain.Internal.Imports {
seen[p1] = true
}
for _, p1 := range cover.Pkgs {
if !seen[p1] {
seen[p1] = true
pmain.Internal.Imports = append(pmain.Internal.Imports, p1)
}
}
}
// Do initial scan for metadata needed for writing _testmain.go
// Use that metadata to update the list of imports for package main.
// The list of imports is used by recompileForTest and by the loop
// afterward that gathers t.Cover information.
t, err := loadTestFuncs(ptest)
if err != nil {
return nil, nil, nil, err
}
t.Cover = cover
if len(ptest.GoFiles)+len(ptest.CgoFiles) > 0 {
pmain.Internal.Imports = append(pmain.Internal.Imports, ptest)
t.ImportTest = true
}
if pxtest != nil {
pmain.Internal.Imports = append(pmain.Internal.Imports, pxtest)
t.ImportXtest = true
}
if ptest != p {
// We have made modifications to the package p being tested // We have made modifications to the package p being tested
// and are rebuilding p (as ptest). // and are rebuilding p (as ptest).
// Arrange to rebuild all packages q such that // Arrange to rebuild all packages q such that
// pxtest depends on q and q depends on p. // the test depends on q and q depends on p.
// This makes sure that q sees the modifications to p. // This makes sure that q sees the modifications to p.
// Strictly speaking, the rebuild is only necessary if the // Strictly speaking, the rebuild is only necessary if the
// modifications to p change its export metadata, but // modifications to p change its export metadata, but
// determining that is a bit tricky, so we rebuild always. // determining that is a bit tricky, so we rebuild always.
recompileForTest(p, ptest, pxtest) recompileForTest(pmain, p, ptest, pxtest)
}
// Should we apply coverage analysis locally,
// only for this package and only for this test?
// Yes, if -cover is on but -coverpkg has not specified
// a list of packages for global coverage.
if cover != nil && cover.Local {
ptest.Internal.CoverMode = cover.Mode
var coverFiles []string
coverFiles = append(coverFiles, ptest.GoFiles...)
coverFiles = append(coverFiles, ptest.CgoFiles...)
ptest.Internal.CoverVars = cover.DeclVars(ptest.ImportPath, coverFiles...)
}
for _, cp := range pmain.Internal.Imports {
if len(cp.Internal.CoverVars) > 0 {
t.Cover.Vars = append(t.Cover.Vars, coverInfo{cp, cp.Internal.CoverVars})
}
}
data, err := formatTestmain(t)
if err != nil {
return nil, nil, nil, err
} }
pmain.Internal.TestmainGo = &data
return ptest, pxtest, nil return pmain, ptest, pxtest, nil
} }
func testImportStack(top string, p *Package, target string) []string { func testImportStack(top string, p *Package, target string) []string {
...@@ -166,20 +300,17 @@ Search: ...@@ -166,20 +300,17 @@ Search:
return stk return stk
} }
func recompileForTest(preal, ptest, pxtest *Package) { func recompileForTest(pmain, preal, ptest, pxtest *Package) {
// The "test copy" of preal is ptest. // The "test copy" of preal is ptest.
// For each package that depends on preal, make a "test copy" // For each package that depends on preal, make a "test copy"
// that depends on ptest. And so on, up the dependency tree. // that depends on ptest. And so on, up the dependency tree.
testCopy := map[*Package]*Package{preal: ptest} testCopy := map[*Package]*Package{preal: ptest}
// Only pxtest and its dependencies can legally depend on p. for _, p := range PackageList([]*Package{pmain}) {
// If ptest or its dependencies depended on p, the dependency
// would be circular.
for _, p := range PackageList([]*Package{pxtest}) {
if p == preal { if p == preal {
continue continue
} }
// Copy on write. // Copy on write.
didSplit := p == pxtest didSplit := p == pmain || p == pxtest
split := func() { split := func() {
if didSplit { if didSplit {
return return
...@@ -191,6 +322,7 @@ func recompileForTest(preal, ptest, pxtest *Package) { ...@@ -191,6 +322,7 @@ func recompileForTest(preal, ptest, pxtest *Package) {
p1 := new(Package) p1 := new(Package)
testCopy[p] = p1 testCopy[p] = p1
*p1 = *p *p1 = *p
p1.ForTest = preal.ImportPath
p1.Internal.Imports = make([]*Package, len(p.Internal.Imports)) p1.Internal.Imports = make([]*Package, len(p.Internal.Imports))
copy(p1.Internal.Imports, p.Internal.Imports) copy(p1.Internal.Imports, p.Internal.Imports)
p = p1 p = p1
...@@ -206,3 +338,298 @@ func recompileForTest(preal, ptest, pxtest *Package) { ...@@ -206,3 +338,298 @@ func recompileForTest(preal, ptest, pxtest *Package) {
} }
} }
} }
// 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)
}
type coverInfo struct {
Package *Package
Vars map[string]*CoverVar
}
// loadTestFuncs returns the testFuncs describing the tests that will be run.
func loadTestFuncs(ptest *Package) (*testFuncs, error) {
t := &testFuncs{
Package: ptest,
}
for _, file := range ptest.TestGoFiles {
if err := t.load(filepath.Join(ptest.Dir, file), "_test", &t.ImportTest, &t.NeedTest); err != nil {
return nil, err
}
}
for _, file := range ptest.XTestGoFiles {
if err := t.load(filepath.Join(ptest.Dir, file), "_xtest", &t.ImportXtest, &t.NeedXtest); err != nil {
return nil, err
}
}
return t, nil
}
// formatTestmain returns the content of the _testmain.go file for t.
func formatTestmain(t *testFuncs) ([]byte, error) {
var buf bytes.Buffer
if err := testmainTmpl.Execute(&buf, t); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type testFuncs struct {
Tests []testFunc
Benchmarks []testFunc
Examples []testFunc
TestMain *testFunc
Package *Package
ImportTest bool
NeedTest bool
ImportXtest bool
NeedXtest bool
Cover *TestCover
}
// ImportPath returns the import path of the package being tested, if it is within GOPATH.
// This is printed by the testing package when running benchmarks.
func (t *testFuncs) ImportPath() string {
pkg := t.Package.ImportPath
if strings.HasPrefix(pkg, "_/") {
return ""
}
if pkg == "command-line-arguments" {
return ""
}
return pkg
}
// Covered returns a string describing which packages are being tested for coverage.
// If the covered package is the same as the tested package, it returns the empty string.
// Otherwise it is a comma-separated human-readable list of packages beginning with
// " in", ready for use in the coverage message.
func (t *testFuncs) Covered() string {
if t.Cover == nil || t.Cover.Paths == nil {
return ""
}
return " in " + strings.Join(t.Cover.Paths, ", ")
}
// Tested returns the name of the package being tested.
func (t *testFuncs) Tested() string {
return t.Package.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.
}
var testFileSet = token.NewFileSet()
func (t *testFuncs) load(filename, pkg string, doImport, seen *bool) error {
f, err := parser.ParseFile(testFileSet, filename, nil, parser.ParseComments)
if err != nil {
return base.ExpandScanner(err)
}
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(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(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(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(fn *ast.FuncDecl, arg string) error {
if !isTestFunc(fn, arg) {
name := fn.Name.String()
pos := testFileSet.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}} {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .ImportXtest}}
{{if .NeedXtest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
{{if .Cover}}
{{range $i, $p := .Cover.Vars}}
_cover{{$i}} {{$p.Package.ImportPath | printf "%q"}}
{{end}}
{{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 = {{.ImportPath | printf "%q"}}
}
{{if .Cover}}
// Only updated by init functions, so no need for atomicity.
var (
coverCounters = make(map[string][]uint32)
coverBlocks = make(map[string][]testing.CoverBlock)
)
func init() {
{{range $i, $p := .Cover.Vars}}
{{range $file, $cover := $p.Vars}}
coverRegisterFile({{printf "%q" $cover.File}}, _cover{{$i}}.{{$cover.Var}}.Count[:], _cover{{$i}}.{{$cover.Var}}.Pos[:], _cover{{$i}}.{{$cover.Var}}.NumStmt[:])
{{end}}
{{end}}
}
func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) {
if 3*len(counter) != len(pos) || len(counter) != len(numStmts) {
panic("coverage: mismatched sizes")
}
if coverCounters[fileName] != nil {
// Already registered.
return
}
coverCounters[fileName] = counter
block := make([]testing.CoverBlock, len(counter))
for i := range counter {
block[i] = testing.CoverBlock{
Line0: pos[3*i+0],
Col0: uint16(pos[3*i+2]),
Line1: pos[3*i+1],
Col1: uint16(pos[3*i+2]>>16),
Stmts: numStmts[i],
}
}
coverBlocks[fileName] = block
}
{{end}}
func main() {
{{if .Cover}}
testing.RegisterCover(testing.Cover{
Mode: {{printf "%q" .Cover.Mode}},
Counters: coverCounters,
Blocks: coverBlocks,
CoveredPackages: {{printf "%q" .Covered}},
})
{{end}}
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, examples)
{{with .TestMain}}
{{.Package}}.{{.Name}}(m)
{{else}}
os.Exit(m.Run())
{{end}}
}
`))
...@@ -9,11 +9,7 @@ import ( ...@@ -9,11 +9,7 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"go/ast"
"go/build" "go/build"
"go/doc"
"go/parser"
"go/token"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
...@@ -25,10 +21,7 @@ import ( ...@@ -25,10 +21,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"text/template"
"time" "time"
"unicode"
"unicode/utf8"
"cmd/go/internal/base" "cmd/go/internal/base"
"cmd/go/internal/cache" "cmd/go/internal/cache"
...@@ -507,13 +500,6 @@ var ( ...@@ -507,13 +500,6 @@ var (
testCacheExpire time.Time // ignore cached test results before this time testCacheExpire time.Time // ignore cached test results before this time
) )
var testMainDeps = []string{
// Dependencies for testmain.
"os",
"testing",
"testing/internal/testdeps",
}
// testVetFlags is the list of flags to pass to vet when invoked automatically during go test. // testVetFlags is the list of flags to pass to vet when invoked automatically during go test.
var testVetFlags = []string{ var testVetFlags = []string{
// TODO(rsc): Decide which tests are enabled by default. // TODO(rsc): Decide which tests are enabled by default.
...@@ -605,7 +591,7 @@ func runTest(cmd *base.Command, args []string) { ...@@ -605,7 +591,7 @@ func runTest(cmd *base.Command, args []string) {
cfg.BuildV = testV cfg.BuildV = testV
deps := make(map[string]bool) deps := make(map[string]bool)
for _, dep := range testMainDeps { for _, dep := range load.TestMainDeps {
deps[dep] = true deps[dep] = true
} }
...@@ -799,14 +785,20 @@ func builderTest(b *work.Builder, p *load.Package) (buildAction, runAction, prin ...@@ -799,14 +785,20 @@ func builderTest(b *work.Builder, p *load.Package) (buildAction, runAction, prin
} }
// Build Package structs describing: // Build Package structs describing:
// pmain - pkg.test binary
// ptest - package + test files // ptest - package + test files
// pxtest - package of external test files // pxtest - package of external test files
// pmain - pkg.test binary var cover *load.TestCover
var ptest, pxtest, pmain *load.Package if testCover {
cover = &load.TestCover{
localCover := testCover && testCoverPaths == nil Mode: testCoverMode,
Local: testCover && testCoverPaths == nil,
ptest, pxtest, err = load.TestPackagesFor(p, localCover || p.Name == "main") Pkgs: testCoverPkgs,
Paths: testCoverPaths,
DeclVars: declareCoverVars,
}
}
pmain, ptest, pxtest, err := load.TestPackagesFor(p, cover)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
...@@ -823,104 +815,18 @@ func builderTest(b *work.Builder, p *load.Package) (buildAction, runAction, prin ...@@ -823,104 +815,18 @@ func builderTest(b *work.Builder, p *load.Package) (buildAction, runAction, prin
} }
testBinary := elem + ".test" testBinary := elem + ".test"
// Should we apply coverage analysis locally,
// only for this package and only for this test?
// Yes, if -cover is on but -coverpkg has not specified
// a list of packages for global coverage.
if localCover {
ptest.Internal.CoverMode = testCoverMode
var coverFiles []string
coverFiles = append(coverFiles, ptest.GoFiles...)
coverFiles = append(coverFiles, ptest.CgoFiles...)
ptest.Internal.CoverVars = declareCoverVars(ptest.ImportPath, coverFiles...)
}
testDir := b.NewObjdir() testDir := b.NewObjdir()
if err := b.Mkdir(testDir); err != nil { if err := b.Mkdir(testDir); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
// Action for building pkg.test. pmain.Dir = testDir
pmain = &load.Package{ pmain.Internal.OmitDebug = !testC && !testNeedBinary
PackagePublic: load.PackagePublic{
Name: "main",
Dir: testDir,
GoFiles: []string{"_testmain.go"},
ImportPath: p.ImportPath + " (testmain)",
Root: p.Root,
},
Internal: load.PackageInternal{
Build: &build.Package{Name: "main"},
OmitDebug: !testC && !testNeedBinary,
Asmflags: p.Internal.Asmflags,
Gcflags: p.Internal.Gcflags,
Ldflags: p.Internal.Ldflags,
Gccgoflags: p.Internal.Gccgoflags,
},
}
// The generated main also imports testing, regexp, and os.
// Also the linker introduces implicit dependencies reported by LinkerDeps.
var stk load.ImportStack
stk.Push("testmain")
deps := testMainDeps // cap==len, so safe for append
for _, d := range load.LinkerDeps(p) {
deps = append(deps, d)
}
for _, dep := range deps {
if dep == ptest.ImportPath {
pmain.Internal.Imports = append(pmain.Internal.Imports, ptest)
} else {
p1 := load.LoadImport(dep, "", nil, &stk, nil, 0)
if p1.Error != nil {
return nil, nil, nil, p1.Error
}
pmain.Internal.Imports = append(pmain.Internal.Imports, p1)
}
}
if testCoverPkgs != nil {
// Add imports, but avoid duplicates.
seen := map[*load.Package]bool{p: true, ptest: true}
for _, p1 := range pmain.Internal.Imports {
seen[p1] = true
}
for _, p1 := range testCoverPkgs {
if !seen[p1] {
seen[p1] = true
pmain.Internal.Imports = append(pmain.Internal.Imports, p1)
}
}
}
// Do initial scan for metadata needed for writing _testmain.go
// Use that metadata to update the list of imports for package main.
// The list of imports is used by recompileForTest and by the loop
// afterward that gathers t.Cover information.
t, err := loadTestFuncs(ptest)
if err != nil {
return nil, nil, nil, err
}
if len(ptest.GoFiles)+len(ptest.CgoFiles) > 0 {
pmain.Internal.Imports = append(pmain.Internal.Imports, ptest)
t.ImportTest = true
}
if pxtest != nil {
pmain.Internal.Imports = append(pmain.Internal.Imports, pxtest)
t.ImportXtest = true
}
for _, cp := range pmain.Internal.Imports {
if len(cp.Internal.CoverVars) > 0 {
t.Cover = append(t.Cover, coverInfo{cp, cp.Internal.CoverVars})
}
}
if !cfg.BuildN { if !cfg.BuildN {
// writeTestmain writes _testmain.go, // writeTestmain writes _testmain.go,
// using the test description gathered in t. // using the test description gathered in t.
if err := writeTestmain(testDir+"_testmain.go", t); err != nil { if err := ioutil.WriteFile(testDir+"_testmain.go", *pmain.Internal.TestmainGo, 0666); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
} }
...@@ -1697,306 +1603,3 @@ func builderNoTest(b *work.Builder, a *work.Action) error { ...@@ -1697,306 +1603,3 @@ func builderNoTest(b *work.Builder, a *work.Action) error {
fmt.Fprintf(stdout, "? \t%s\t[no test files]\n", a.Package.ImportPath) fmt.Fprintf(stdout, "? \t%s\t[no test files]\n", a.Package.ImportPath)
return nil return nil
} }
// 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)
}
type coverInfo struct {
Package *load.Package
Vars map[string]*load.CoverVar
}
// loadTestFuncs returns the testFuncs describing the tests that will be run.
func loadTestFuncs(ptest *load.Package) (*testFuncs, error) {
t := &testFuncs{
Package: ptest,
}
for _, file := range ptest.TestGoFiles {
if err := t.load(filepath.Join(ptest.Dir, file), "_test", &t.ImportTest, &t.NeedTest); err != nil {
return nil, err
}
}
for _, file := range ptest.XTestGoFiles {
if err := t.load(filepath.Join(ptest.Dir, 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()
return testmainTmpl.Execute(f, t)
}
type testFuncs struct {
Tests []testFunc
Benchmarks []testFunc
Examples []testFunc
TestMain *testFunc
Package *load.Package
ImportTest bool
NeedTest bool
ImportXtest bool
NeedXtest bool
Cover []coverInfo
}
func (t *testFuncs) CoverMode() string {
return testCoverMode
}
func (t *testFuncs) CoverEnabled() bool {
return testCover
}
// ImportPath returns the import path of the package being tested, if it is within GOPATH.
// This is printed by the testing package when running benchmarks.
func (t *testFuncs) ImportPath() string {
pkg := t.Package.ImportPath
if strings.HasPrefix(pkg, "_/") {
return ""
}
if pkg == "command-line-arguments" {
return ""
}
return pkg
}
// Covered returns a string describing which packages are being tested for coverage.
// If the covered package is the same as the tested package, it returns the empty string.
// Otherwise it is a comma-separated human-readable list of packages beginning with
// " in", ready for use in the coverage message.
func (t *testFuncs) Covered() string {
if testCoverPaths == nil {
return ""
}
return " in " + strings.Join(testCoverPaths, ", ")
}
// Tested returns the name of the package being tested.
func (t *testFuncs) Tested() string {
return t.Package.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.
}
var testFileSet = token.NewFileSet()
func (t *testFuncs) load(filename, pkg string, doImport, seen *bool) error {
f, err := parser.ParseFile(testFileSet, filename, nil, parser.ParseComments)
if err != nil {
return base.ExpandScanner(err)
}
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(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(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(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(fn *ast.FuncDecl, arg string) error {
if !isTestFunc(fn, arg) {
name := fn.Name.String()
pos := testFileSet.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}} {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .ImportXtest}}
{{if .NeedXtest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
{{range $i, $p := .Cover}}
_cover{{$i}} {{$p.Package.ImportPath | 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 = {{.ImportPath | printf "%q"}}
}
{{if .CoverEnabled}}
// Only updated by init functions, so no need for atomicity.
var (
coverCounters = make(map[string][]uint32)
coverBlocks = make(map[string][]testing.CoverBlock)
)
func init() {
{{range $i, $p := .Cover}}
{{range $file, $cover := $p.Vars}}
coverRegisterFile({{printf "%q" $cover.File}}, _cover{{$i}}.{{$cover.Var}}.Count[:], _cover{{$i}}.{{$cover.Var}}.Pos[:], _cover{{$i}}.{{$cover.Var}}.NumStmt[:])
{{end}}
{{end}}
}
func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) {
if 3*len(counter) != len(pos) || len(counter) != len(numStmts) {
panic("coverage: mismatched sizes")
}
if coverCounters[fileName] != nil {
// Already registered.
return
}
coverCounters[fileName] = counter
block := make([]testing.CoverBlock, len(counter))
for i := range counter {
block[i] = testing.CoverBlock{
Line0: pos[3*i+0],
Col0: uint16(pos[3*i+2]),
Line1: pos[3*i+1],
Col1: uint16(pos[3*i+2]>>16),
Stmts: numStmts[i],
}
}
coverBlocks[fileName] = block
}
{{end}}
func main() {
{{if .CoverEnabled}}
testing.RegisterCover(testing.Cover{
Mode: {{printf "%q" .CoverMode}},
Counters: coverCounters,
Blocks: coverBlocks,
CoveredPackages: {{printf "%q" .Covered}},
})
{{end}}
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, examples)
{{with .TestMain}}
{{.Package}}.{{.Name}}(m)
{{else}}
os.Exit(m.Run())
{{end}}
}
`))
...@@ -57,7 +57,7 @@ func runVet(cmd *base.Command, args []string) { ...@@ -57,7 +57,7 @@ func runVet(cmd *base.Command, args []string) {
root := &work.Action{Mode: "go vet"} root := &work.Action{Mode: "go vet"}
for _, p := range pkgs { for _, p := range pkgs {
ptest, pxtest, err := load.TestPackagesFor(p, false) _, ptest, pxtest, err := load.TestPackagesFor(p, nil)
if err != nil { if err != nil {
base.Errorf("%v", err) base.Errorf("%v", err)
continue continue
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment