1
0
mirror of https://github.com/golang/go synced 2024-11-26 20:01:19 -07:00

cmd/vet: bring in pass to catch invalid uses of testing.T in goroutines

Add "go/analysis/passes/testinggoroutine" from x/tools and vendor its source in.
This pass will catch misuses of:
* testing.T.Fail*
* testing.T.Fatal*
* testing.T.Skip*
inside goroutines explicitly started by the go keyword.

The pass was implemented in CL 212920.

While here, found 2 misuses in:
* database/sql/sql_test.go
* runtime/syscall_windows_test.go
and fixed them in CL 235527.

Fixes #5746

Change-Id: I1740ad3f1d677bb5d78dc5d8d66bac6ec287a2b1
Reviewed-on: https://go-review.googlesource.com/c/go/+/235677
Run-TryBot: Emmanuel Odeke <emmanuel@orijtech.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Trust: Emmanuel Odeke <emmanuel@orijtech.com>
This commit is contained in:
Emmanuel T Odeke 2020-05-29 02:17:38 -07:00 committed by Emmanuel Odeke
parent 063a91c0ab
commit 5a267c840a
2 changed files with 156 additions and 0 deletions

View File

@ -0,0 +1,154 @@
// Copyright 2020 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 testinggoroutine
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
)
const Doc = `report calls to (*testing.T).Fatal from goroutines started by a test.
Functions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and
Skip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.
This checker detects calls to these functions that occur within a goroutine
started by the test. For example:
func TestFoo(t *testing.T) {
go func() {
t.Fatal("oops") // error: (*T).Fatal called from non-test goroutine
}()
}
`
var Analyzer = &analysis.Analyzer{
Name: "testinggoroutine",
Doc: Doc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
var forbidden = map[string]bool{
"FailNow": true,
"Fatal": true,
"Fatalf": true,
"Skip": true,
"Skipf": true,
"SkipNow": true,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
if !analysisutil.Imports(pass.Pkg, "testing") {
return nil, nil
}
// Filter out anything that isn't a function declaration.
onlyFuncs := []ast.Node{
(*ast.FuncDecl)(nil),
}
inspect.Nodes(onlyFuncs, func(node ast.Node, push bool) bool {
fnDecl, ok := node.(*ast.FuncDecl)
if !ok {
return false
}
if !hasBenchmarkOrTestParams(fnDecl) {
return false
}
// Now traverse the benchmark/test's body and check that none of the
// forbidden methods are invoked in the goroutines within the body.
ast.Inspect(fnDecl, func(n ast.Node) bool {
goStmt, ok := n.(*ast.GoStmt)
if !ok {
return true
}
checkGoStmt(pass, goStmt)
// No need to further traverse the GoStmt since right
// above we manually traversed it in the ast.Inspect(goStmt, ...)
return false
})
return false
})
return nil, nil
}
func hasBenchmarkOrTestParams(fnDecl *ast.FuncDecl) bool {
// Check that the function's arguments include "*testing.T" or "*testing.B".
params := fnDecl.Type.Params.List
for _, param := range params {
if _, ok := typeIsTestingDotTOrB(param.Type); ok {
return true
}
}
return false
}
func typeIsTestingDotTOrB(expr ast.Expr) (string, bool) {
starExpr, ok := expr.(*ast.StarExpr)
if !ok {
return "", false
}
selExpr, ok := starExpr.X.(*ast.SelectorExpr)
if !ok {
return "", false
}
varPkg := selExpr.X.(*ast.Ident)
if varPkg.Name != "testing" {
return "", false
}
varTypeName := selExpr.Sel.Name
ok = varTypeName == "B" || varTypeName == "T"
return varTypeName, ok
}
// checkGoStmt traverses the goroutine and checks for the
// use of the forbidden *testing.(B, T) methods.
func checkGoStmt(pass *analysis.Pass, goStmt *ast.GoStmt) {
// Otherwise examine the goroutine to check for the forbidden methods.
ast.Inspect(goStmt, func(n ast.Node) bool {
selExpr, ok := n.(*ast.SelectorExpr)
if !ok {
return true
}
_, bad := forbidden[selExpr.Sel.Name]
if !bad {
return true
}
// Now filter out false positives by the import-path/type.
ident, ok := selExpr.X.(*ast.Ident)
if !ok {
return true
}
if ident.Obj == nil || ident.Obj.Decl == nil {
return true
}
field, ok := ident.Obj.Decl.(*ast.Field)
if !ok {
return true
}
if typeName, ok := typeIsTestingDotTOrB(field.Type); ok {
pass.ReportRangef(selExpr, "call to (*%s).%s from a non-test goroutine", typeName, selExpr.Sel)
}
return true
})
}

View File

@ -24,6 +24,7 @@ import (
"golang.org/x/tools/go/analysis/passes/stdmethods"
"golang.org/x/tools/go/analysis/passes/stringintconv"
"golang.org/x/tools/go/analysis/passes/structtag"
"golang.org/x/tools/go/analysis/passes/testinggoroutine"
"golang.org/x/tools/go/analysis/passes/tests"
"golang.org/x/tools/go/analysis/passes/unmarshal"
"golang.org/x/tools/go/analysis/passes/unreachable"
@ -55,6 +56,7 @@ func main() {
stringintconv.Analyzer,
structtag.Analyzer,
tests.Analyzer,
testinggoroutine.Analyzer,
unmarshal.Analyzer,
unreachable.Analyzer,
unsafeptr.Analyzer,