Skip to content

Commit

Permalink
Merge pull request #10 from rkennedy/bugfix/false-positive-wrong-rece…
Browse files Browse the repository at this point in the history
…iver
  • Loading branch information
kunwardeep authored Sep 17, 2021
2 parents f99d07c + 8b9f74a commit f435dce
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 28 deletions.
85 changes: 61 additions & 24 deletions pkg/paralleltest/paralleltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ func run(pass *analysis.Pass) (interface{}, error) {
var rangeNode ast.Node

// Check runs for test functions only
if !isTestFunction(funcDecl) {
isTest, testVar := isTestFunction(funcDecl)
if !isTest {
return
}

Expand All @@ -53,16 +54,19 @@ func run(pass *analysis.Pass) (interface{}, error) {
ast.Inspect(v, func(n ast.Node) bool {
// Check if the test method is calling t.parallel
if !funcHasParallelMethod {
funcHasParallelMethod = methodParallelIsCalledInTestFunction(n)
funcHasParallelMethod = methodParallelIsCalledInTestFunction(n, testVar)
}

// Check if the t.Run within the test function is calling t.parallel
if methodRunIsCalledInTestFunction(n) {
if methodRunIsCalledInTestFunction(n, testVar) {
// n is a call to t.Run; find out the name of the subtest's *testing.T parameter.
innerTestVar := getRunCallbackParameterName(n)

hasParallel := false
numberOfTestRun++
ast.Inspect(v, func(p ast.Node) bool {
if !hasParallel {
hasParallel = methodParallelIsCalledInTestFunction(p)
hasParallel = methodParallelIsCalledInTestFunction(p, innerTestVar)
}
return true
})
Expand All @@ -81,12 +85,15 @@ func run(pass *analysis.Pass) (interface{}, error) {
// nolint: gocritic
switch r := n.(type) {
case *ast.ExprStmt:
if methodRunIsCalledInRangeStatement(r.X) {
if methodRunIsCalledInRangeStatement(r.X, testVar) {
// r.X is a call to t.Run; find out the name of the subtest's *testing.T parameter.
innerTestVar := getRunCallbackParameterName(r.X)

rangeStatementOverTestCasesExists = true
testRunLoopIdentifier = methodRunFirstArgumentObjectName(r.X)

if !rangeStatementHasParallelMethod {
rangeStatementHasParallelMethod = methodParallelIsCalledInMethodRun(r.X)
rangeStatementHasParallelMethod = methodParallelIsCalledInMethodRun(r.X, innerTestVar)
}
}
}
Expand Down Expand Up @@ -165,7 +172,7 @@ func getLeftAndRightIdentifier(s ast.Stmt) (string, string) {
return leftIdentifier, rightIdentifier
}

func methodParallelIsCalledInMethodRun(node ast.Node) bool {
func methodParallelIsCalledInMethodRun(node ast.Node, testVar string) bool {
var methodParallelCalled bool
// nolint: gocritic
switch callExp := node.(type) {
Expand All @@ -174,7 +181,7 @@ func methodParallelIsCalledInMethodRun(node ast.Node) bool {
if !methodParallelCalled {
ast.Inspect(arg, func(n ast.Node) bool {
if !methodParallelCalled {
methodParallelCalled = methodParallelIsCalledInRunMethod(n)
methodParallelCalled = methodParallelIsCalledInRunMethod(n, testVar)
return true
}
return false
Expand All @@ -185,32 +192,61 @@ func methodParallelIsCalledInMethodRun(node ast.Node) bool {
return methodParallelCalled
}

func methodParallelIsCalledInRunMethod(node ast.Node) bool {
return exprCallHasMethod(node, "Parallel")
func methodParallelIsCalledInRunMethod(node ast.Node, testVar string) bool {
return exprCallHasMethod(node, testVar, "Parallel")
}

func methodParallelIsCalledInTestFunction(node ast.Node) bool {
return exprCallHasMethod(node, "Parallel")
func methodParallelIsCalledInTestFunction(node ast.Node, testVar string) bool {
return exprCallHasMethod(node, testVar, "Parallel")
}

func methodRunIsCalledInRangeStatement(node ast.Node) bool {
return exprCallHasMethod(node, "Run")
func methodRunIsCalledInRangeStatement(node ast.Node, testVar string) bool {
return exprCallHasMethod(node, testVar, "Run")
}

func methodRunIsCalledInTestFunction(node ast.Node) bool {
return exprCallHasMethod(node, "Run")
func methodRunIsCalledInTestFunction(node ast.Node, testVar string) bool {
return exprCallHasMethod(node, testVar, "Run")
}
func exprCallHasMethod(node ast.Node, methodName string) bool {
func exprCallHasMethod(node ast.Node, receiverName, methodName string) bool {
// nolint: gocritic
switch n := node.(type) {
case *ast.CallExpr:
if fun, ok := n.Fun.(*ast.SelectorExpr); ok {
return fun.Sel.Name == methodName
if receiver, ok := fun.X.(*ast.Ident); ok {
return receiver.Name == receiverName && fun.Sel.Name == methodName
}
}
}
return false
}

// In an expression of the form t.Run(x, func(q *testing.T) {...}), return the
// value "q". In _most_ code, the name is probably t, but we shouldn't just
// assume.
func getRunCallbackParameterName(node ast.Node) string {
if n, ok := node.(*ast.CallExpr); ok {
if len(n.Args) < 2 {
// We want argument #2, but this call doesn't have two
// arguments. Maybe it's not really t.Run.
return ""
}
funcArg := n.Args[1]
if fun, ok := funcArg.(*ast.FuncLit); ok {
if len(fun.Type.Params.List) < 1 {
// Subtest function doesn't have any parameters.
return ""
}
firstArg := fun.Type.Params.List[0]
// We'll assume firstArg.Type is *testing.T.
if len(firstArg.Names) < 1 {
return ""
}
return firstArg.Names[0].Name
}
}
return ""
}

// Gets the object name `tc` from method t.Run(tc.Foo, func(t *testing.T)
func methodRunFirstArgumentObjectName(node ast.Node) string {
// nolint: gocritic
Expand All @@ -227,30 +263,31 @@ func methodRunFirstArgumentObjectName(node ast.Node) string {
return ""
}

// Checks if the function has the param type *testing.T)
func isTestFunction(funcDecl *ast.FuncDecl) bool {
// Checks if the function has the param type *testing.T; if it does, then the
// parameter name is returned, too.
func isTestFunction(funcDecl *ast.FuncDecl) (bool, string) {
testMethodPackageType := "testing"
testMethodStruct := "T"
testPrefix := "Test"

if !strings.HasPrefix(funcDecl.Name.Name, testPrefix) {
return false
return false, ""
}

if funcDecl.Type.Params != nil && len(funcDecl.Type.Params.List) != 1 {
return false
return false, ""
}

param := funcDecl.Type.Params.List[0]
if starExp, ok := param.Type.(*ast.StarExpr); ok {
if selectExpr, ok := starExp.X.(*ast.SelectorExpr); ok {
if selectExpr.Sel.Name == testMethodStruct {
if s, ok := selectExpr.X.(*ast.Ident); ok {
return s.Name == testMethodPackageType
return s.Name == testMethodPackageType, param.Names[0].Name
}
}
}
}

return false
return false, ""
}
32 changes: 28 additions & 4 deletions pkg/paralleltest/testdata/src/t/t_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ func TestFunctionSuccessfulRangeTest(t *testing.T) {
}{{name: "foo"}}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run(tc.name, func(x *testing.T) {
x.Parallel()
fmt.Println(tc.name)
})
}
Expand Down Expand Up @@ -129,11 +129,35 @@ func TestFunctionFirstOneTestRunMissingCallToParallel(t *testing.T) {
func TestFunctionSecondOneTestRunMissingCallToParallel(t *testing.T) {
t.Parallel()

t.Run("1", func(t *testing.T) {
t.Parallel()
t.Run("1", func(x *testing.T) {
x.Parallel()
fmt.Println("1")
})
t.Run("2", func(t *testing.T) { // want "Function TestFunctionSecondOneTestRunMissingCallToParallel has missing the call to method parallel in the test run"
fmt.Println("2")
})
}

type notATest int

func (notATest) Run(args ...interface{}) {}
func (notATest) Parallel() {}

func TestFunctionWithRunLookalike(t *testing.T) {
t.Parallel()

var other notATest
// These aren't t.Run, so they shouldn't be flagged as Run invocations missing calls to Parallel.
other.Run(1, 1)
other.Run(2, 2)
}

func TestFunctionWithParallelLookalike(t *testing.T) { // want "Function TestFunctionWithParallelLookalike missing the call to method parallel"
var other notATest
// This isn't t.Parallel, so it doesn't qualify as a call to Parallel.
other.Parallel()
}

func TestFunctionWithOtherTestingVar(q *testing.T) {
q.Parallel()
}

0 comments on commit f435dce

Please sign in to comment.