mirror of
https://github.com/golang/go
synced 2024-11-18 18:54:42 -07:00
analysis/passes: report testing.Fatal* FailNow Skip* misuse in goroutines
Adds an analyzer to report an error if any tests or benchmarks have any *Fatal, FailNow, Skip* misuses in goroutines which are forbidden by the package testing, since those functions terminate the entire benchmark/test yet ideally one goroutine exiting shouldn't affect the entire benchmark/test. This first pass only works for plain goroutines and doesn't yet work with b.RunParallel. That'll be added in a subsequent CL after this one is reviewed and merged. Updates golang/go#5746 Change-Id: Ia47e5c9fd96ceced1ae9834b94f529f6ae2edaaa Reviewed-on: https://go-review.googlesource.com/c/tools/+/212920 Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
parent
7be0a674c9
commit
53017a39ae
261
go/analysis/passes/testinggoroutine/testdata/src/a/a.go
vendored
Normal file
261
go/analysis/passes/testinggoroutine/testdata/src/a/a.go
vendored
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// 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 a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBadFatalf(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
t.Fatalf("TestFailed: id = %v\n", id) // want "call to .+T.+Fatalf from a non-test goroutine"
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOKErrorf(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
t.Errorf("TestFailed: id = %v\n", id)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBadFatalf(b *testing.B) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
b.Fatalf("TestFailed: id = %v\n", id) // want "call to .+B.+Fatalf from a non-test goroutine"
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadFatal(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
t.Fatal("TestFailed") // want "call to .+T.+Fatal from a non-test goroutine"
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBadFatal(b *testing.B) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
b.Fatal("TestFailed") // want "call to .+B.+Fatal from a non-test goroutine"
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkOKErrorf(b *testing.B) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
b.Errorf("TestFailed: %d", i)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBadFatalGoGo(b *testing.B) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
b.Fatal("TestFailed") // want "call to .+B.+Fatal from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if false {
|
||||||
|
defer b.Fatal("here")
|
||||||
|
}
|
||||||
|
|
||||||
|
if true {
|
||||||
|
go func() {
|
||||||
|
b.Fatal("in here") // want "call to .+B.+Fatal from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
func() {
|
||||||
|
func() {
|
||||||
|
func() {
|
||||||
|
go func() {
|
||||||
|
b.Fatal("Here") // want "call to .+B.+Fatal from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
|
||||||
|
_ = 10 * 10
|
||||||
|
_ = func() bool {
|
||||||
|
go b.Fatal("Failed") // want "call to .+B.+Fatal from a non-test goroutine"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
go b.Fatal("Here") // want "call to .+B.+Fatal from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBadSkip(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if i == 100 {
|
||||||
|
go b.Skip("Skipping") // want "call to .+B.+Skip from a non-test goroutine"
|
||||||
|
}
|
||||||
|
if i == 22 {
|
||||||
|
go func() {
|
||||||
|
go func() {
|
||||||
|
b.Skip("Skipping now") // want "call to .+B.+Skip from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadSkip(t *testing.T) {
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
if i == 100 {
|
||||||
|
go t.Skip("Skipping") // want "call to .+T.+Skip from a non-test goroutine"
|
||||||
|
}
|
||||||
|
if i == 22 {
|
||||||
|
go func() {
|
||||||
|
go func() {
|
||||||
|
t.Skip("Skipping now") // want "call to .+T.+Skip from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBadFailNow(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if i == 100 {
|
||||||
|
go b.FailNow() // want "call to .+B.+FailNow from a non-test goroutine"
|
||||||
|
}
|
||||||
|
if i == 22 {
|
||||||
|
go func() {
|
||||||
|
go func() {
|
||||||
|
b.FailNow() // want "call to .+B.+FailNow from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadFailNow(t *testing.T) {
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
if i == 100 {
|
||||||
|
go t.FailNow() // want "call to .+T.+FailNow from a non-test goroutine"
|
||||||
|
}
|
||||||
|
if i == 22 {
|
||||||
|
go func() {
|
||||||
|
go func() {
|
||||||
|
t.FailNow() // want "call to .+T.+FailNow from a non-test goroutine"
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadWithLoopCond(ty *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer ty.Fatalf("Why") // want "call to .+T.+Fatalf from a non-test goroutine"
|
||||||
|
go func() {
|
||||||
|
for j := 0; j < 2; ty.FailNow() { // want "call to .+T.+FailNow from"
|
||||||
|
j++
|
||||||
|
ty.Errorf("Done here")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type customType int
|
||||||
|
|
||||||
|
func (ct *customType) Fatalf(fmtSpec string, args ...interface{}) {
|
||||||
|
if fmtSpec == "" {
|
||||||
|
panic("empty format specifier")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *customType) FailNow() {}
|
||||||
|
func (ct *customType) Skip() {}
|
||||||
|
|
||||||
|
func TestWithLogFatalf(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
go func() {
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
log.Fatal("Done here")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCustomType(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
ct := new(customType)
|
||||||
|
defer ct.FailNow()
|
||||||
|
defer ct.Skip()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
go func() {
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
ct.Fatalf("Done here: %d", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
165
go/analysis/passes/testinggoroutine/testinggoroutine.go
Normal file
165
go/analysis/passes/testinggoroutine/testinggoroutine.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// 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"
|
||||||
|
"go/types"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/analysis"
|
||||||
|
"golang.org/x/tools/go/analysis/passes/inspect"
|
||||||
|
"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 !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 imports(pkg *types.Package, path string) bool {
|
||||||
|
// TODO: (@odeke-em) perhaps move this function into a
|
||||||
|
// a library since it is commonly used in x/tools/go/analysis.
|
||||||
|
for _, imp := range pkg.Imports() {
|
||||||
|
if imp.Path() == path {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
17
go/analysis/passes/testinggoroutine/testinggoroutine_test.go
Normal file
17
go/analysis/passes/testinggoroutine/testinggoroutine_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/analysis/analysistest"
|
||||||
|
"golang.org/x/tools/go/analysis/passes/testinggoroutine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(t *testing.T) {
|
||||||
|
testdata := analysistest.TestData()
|
||||||
|
analysistest.Run(t, testdata, testinggoroutine.Analyzer, "a")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user