@@ -12,7 +12,7 @@ import (
12
12
13
13
const Doc = `check that tests use t.Parallel() method
14
14
It also checks that the t.Parallel is used if multiple tests cases are run as part of single test.
15
- As part of ensuring parallel tests works as expected it checks for reinitialising of the range value
15
+ As part of ensuring parallel tests works as expected it checks for reinitializing of the range value
16
16
over the test cases.(https://tinyurl.com/y6555cy6)`
17
17
18
18
func NewAnalyzer () * analysis.Analyzer {
@@ -46,138 +46,183 @@ func newParallelAnalyzer() *parallelAnalyzer {
46
46
return a
47
47
}
48
48
49
- func (a * parallelAnalyzer ) run (pass * analysis.Pass ) (interface {}, error ) {
50
- inspector := inspector .New (pass .Files )
49
+ type testFunctionAnalysis struct {
50
+ funcHasParallelMethod ,
51
+ funcCantParallelMethod ,
52
+ rangeStatementOverTestCasesExists ,
53
+ rangeStatementHasParallelMethod ,
54
+ rangeStatementCantParallelMethod bool
55
+ loopVariableUsedInRun * string
56
+ numberOfTestRun int
57
+ positionOfTestRunNode []ast.Node
58
+ rangeNode ast.Node
59
+ }
51
60
52
- nodeFilter := []ast.Node {
53
- (* ast .FuncDecl )(nil ),
54
- }
61
+ type testRunAnalysis struct {
62
+ hasParallel bool
63
+ cantParallel bool
64
+ numberOfTestRun int
65
+ positionOfTestRunNode []ast.Node
66
+ }
55
67
56
- inspector .Preorder (nodeFilter , func (node ast.Node ) {
57
- funcDecl := node .(* ast.FuncDecl )
58
- var funcHasParallelMethod ,
59
- funcCantParallelMethod ,
60
- rangeStatementOverTestCasesExists ,
61
- rangeStatementHasParallelMethod ,
62
- rangeStatementCantParallelMethod bool
63
- var loopVariableUsedInRun * string
64
- var numberOfTestRun int
65
- var positionOfTestRunNode []ast.Node
66
- var rangeNode ast.Node
67
-
68
- // Check runs for test functions only
69
- isTest , testVar := isTestFunction (funcDecl )
70
- if ! isTest {
71
- return
72
- }
68
+ func (a * parallelAnalyzer ) analyzeTestRun (pass * analysis.Pass , n ast.Node , testVar string ) testRunAnalysis {
69
+ var analysis testRunAnalysis
73
70
74
- for _ , l := range funcDecl .Body .List {
75
- switch v := l .(type ) {
71
+ if methodRunIsCalledInTestFunction (n , testVar ) {
72
+ innerTestVar := getRunCallbackParameterName (n )
73
+ analysis .numberOfTestRun ++
76
74
77
- case * ast.ExprStmt :
78
- ast . Inspect ( v , func ( n ast.Node ) bool {
79
- // Check if the test method is calling t.Parallel
80
- if ! funcHasParallelMethod {
81
- funcHasParallelMethod = methodParallelIsCalledInTestFunction (n , testVar )
75
+ if callExpr , ok := n .( * ast.CallExpr ); ok && len ( callExpr . Args ) > 1 {
76
+ if funcLit , ok := callExpr . Args [ 1 ].( * ast.FuncLit ); ok {
77
+ ast . Inspect ( funcLit , func ( p ast. Node ) bool {
78
+ if ! analysis . hasParallel {
79
+ analysis . hasParallel = methodParallelIsCalledInTestFunction (p , innerTestVar )
82
80
}
83
-
84
- // Check if the test calls t.Setenv, cannot be used in parallel tests or tests with parallel ancestors
85
- if ! funcCantParallelMethod {
86
- funcCantParallelMethod = methodSetenvIsCalledInTestFunction (n , testVar )
81
+ if ! analysis .cantParallel {
82
+ analysis .cantParallel = methodSetenvIsCalledInTestFunction (p , innerTestVar )
87
83
}
88
-
89
- // Check if the t.Run within the test function is calling t.Parallel
90
- if methodRunIsCalledInTestFunction (n , testVar ) {
91
- // n is a call to t.Run; find out the name of the subtest's *testing.T parameter.
92
- innerTestVar := getRunCallbackParameterName (n )
93
-
94
- hasParallel := false
95
- cantParallel := false
96
- numberOfTestRun ++
97
- ast .Inspect (v , func (p ast.Node ) bool {
98
- if ! hasParallel {
99
- hasParallel = methodParallelIsCalledInTestFunction (p , innerTestVar )
100
- }
101
- if ! cantParallel {
102
- cantParallel = methodSetenvIsCalledInTestFunction (p , innerTestVar )
84
+ return true
85
+ })
86
+ } else if ident , ok := callExpr .Args [1 ].(* ast.Ident ); ok {
87
+ foundFunc := false
88
+ for _ , file := range pass .Files {
89
+ for _ , decl := range file .Decls {
90
+ if funcDecl , ok := decl .(* ast.FuncDecl ); ok && funcDecl .Name .Name == ident .Name {
91
+ foundFunc = true
92
+ isReceivingTestContext , testParamName := isFunctionReceivingTestContext (funcDecl )
93
+ if isReceivingTestContext {
94
+ ast .Inspect (funcDecl , func (p ast.Node ) bool {
95
+ if ! analysis .hasParallel {
96
+ analysis .hasParallel = methodParallelIsCalledInTestFunction (p , testParamName )
97
+ }
98
+ return true
99
+ })
103
100
}
104
- return true
105
- })
106
- if ! hasParallel && ! cantParallel {
107
- positionOfTestRunNode = append (positionOfTestRunNode , n )
108
101
}
109
102
}
110
- return true
111
- })
103
+ }
104
+ if ! foundFunc {
105
+ analysis .hasParallel = false
106
+ }
107
+ }
108
+ }
112
109
113
- // Check if the range over testcases is calling t.Parallel
114
- case * ast.RangeStmt :
115
- rangeNode = v
110
+ if ! analysis .hasParallel && ! analysis .cantParallel {
111
+ analysis .positionOfTestRunNode = append (analysis .positionOfTestRunNode , n )
112
+ }
113
+ }
116
114
117
- var loopVars []types.Object
118
- for _ , expr := range []ast.Expr {v .Key , v .Value } {
119
- if id , ok := expr .(* ast.Ident ); ok {
120
- loopVars = append (loopVars , pass .TypesInfo .ObjectOf (id ))
121
- }
122
- }
115
+ return analysis
116
+ }
123
117
124
- ast .Inspect (v , func (n ast.Node ) bool {
125
- // nolint: gocritic
126
- switch r := n .(type ) {
127
- case * ast.ExprStmt :
128
- if methodRunIsCalledInRangeStatement (r .X , testVar ) {
129
- // r.X is a call to t.Run; find out the name of the subtest's *testing.T parameter.
130
- innerTestVar := getRunCallbackParameterName (r .X )
118
+ func (a * parallelAnalyzer ) analyzeTestFunction (pass * analysis.Pass , funcDecl * ast.FuncDecl ) {
119
+ var analysis testFunctionAnalysis
131
120
132
- rangeStatementOverTestCasesExists = true
121
+ // Check runs for test functions only
122
+ isTest , testVar := isTestFunction (funcDecl )
123
+ if ! isTest {
124
+ return
125
+ }
133
126
134
- if ! rangeStatementHasParallelMethod {
135
- rangeStatementHasParallelMethod = methodParallelIsCalledInMethodRun (r .X , innerTestVar )
136
- }
127
+ for _ , l := range funcDecl .Body .List {
128
+ switch v := l .(type ) {
129
+ case * ast.ExprStmt :
130
+ ast .Inspect (v , func (n ast.Node ) bool {
131
+ if ! analysis .funcHasParallelMethod {
132
+ analysis .funcHasParallelMethod = methodParallelIsCalledInTestFunction (n , testVar )
133
+ }
134
+ if ! analysis .funcCantParallelMethod {
135
+ analysis .funcCantParallelMethod = methodSetenvIsCalledInTestFunction (n , testVar )
136
+ }
137
+ runAnalysis := a .analyzeTestRun (pass , n , testVar )
138
+ analysis .numberOfTestRun += runAnalysis .numberOfTestRun
139
+ analysis .positionOfTestRunNode = append (analysis .positionOfTestRunNode , runAnalysis .positionOfTestRunNode ... )
140
+ return true
141
+ })
142
+
143
+ case * ast.RangeStmt :
144
+ analysis .rangeNode = v
145
+
146
+ var loopVars []types.Object
147
+ for _ , expr := range []ast.Expr {v .Key , v .Value } {
148
+ if id , ok := expr .(* ast.Ident ); ok {
149
+ loopVars = append (loopVars , pass .TypesInfo .ObjectOf (id ))
150
+ }
151
+ }
152
+
153
+ ast .Inspect (v , func (n ast.Node ) bool {
154
+ if r , ok := n .(* ast.ExprStmt ); ok {
155
+ if methodRunIsCalledInRangeStatement (r .X , testVar ) {
156
+ innerTestVar := getRunCallbackParameterName (r .X )
157
+ analysis .rangeStatementOverTestCasesExists = true
137
158
138
- if ! rangeStatementCantParallelMethod {
139
- rangeStatementCantParallelMethod = methodSetenvIsCalledInMethodRun (r .X , innerTestVar )
159
+ if ! analysis .rangeStatementHasParallelMethod {
160
+ analysis .rangeStatementHasParallelMethod = methodParallelIsCalledInMethodRun (r .X , innerTestVar )
161
+ }
162
+ if ! analysis .rangeStatementCantParallelMethod {
163
+ analysis .rangeStatementCantParallelMethod = methodSetenvIsCalledInMethodRun (r .X , innerTestVar )
164
+ }
165
+ if ! a .ignoreLoopVar && analysis .loopVariableUsedInRun == nil {
166
+ if run , ok := r .X .(* ast.CallExpr ); ok {
167
+ analysis .loopVariableUsedInRun = loopVarReferencedInRun (run , loopVars , pass .TypesInfo )
140
168
}
169
+ }
141
170
142
- if ! a .ignoreLoopVar && loopVariableUsedInRun == nil {
143
- if run , ok := r .X .(* ast.CallExpr ); ok {
144
- loopVariableUsedInRun = loopVarReferencedInRun (run , loopVars , pass .TypesInfo )
145
- }
171
+ // Check nested test runs
172
+ if callExpr , ok := r .X .(* ast.CallExpr ); ok && len (callExpr .Args ) > 1 {
173
+ if funcLit , ok := callExpr .Args [1 ].(* ast.FuncLit ); ok {
174
+ ast .Inspect (funcLit , func (p ast.Node ) bool {
175
+ runAnalysis := a .analyzeTestRun (pass , p , innerTestVar )
176
+ analysis .numberOfTestRun += runAnalysis .numberOfTestRun
177
+ analysis .positionOfTestRunNode = append (analysis .positionOfTestRunNode , runAnalysis .positionOfTestRunNode ... )
178
+ return true
179
+ })
146
180
}
147
181
}
148
182
}
149
- return true
150
- })
151
- }
183
+ }
184
+ return true
185
+ })
152
186
}
187
+ }
153
188
154
- // Descendents which call Setenv, also prevent tests from calling Parallel
155
- if rangeStatementCantParallelMethod {
156
- funcCantParallelMethod = true
157
- }
189
+ if analysis .rangeStatementCantParallelMethod {
190
+ analysis .funcCantParallelMethod = true
191
+ }
158
192
159
- if ! a .ignoreMissing && ! funcHasParallelMethod && ! funcCantParallelMethod {
160
- pass .Reportf (node .Pos (), "Function %s missing the call to method parallel\n " , funcDecl .Name .Name )
161
- }
193
+ if ! a .ignoreMissing && ! analysis . funcHasParallelMethod && ! analysis . funcCantParallelMethod {
194
+ pass .Reportf (funcDecl .Pos (), "Function %s missing the call to method parallel\n " , funcDecl .Name .Name )
195
+ }
162
196
163
- if rangeStatementOverTestCasesExists && rangeNode != nil {
164
- if ! rangeStatementHasParallelMethod && ! rangeStatementCantParallelMethod {
165
- if ! a .ignoreMissing && ! a .ignoreMissingSubtests {
166
- pass .Reportf (rangeNode .Pos (), "Range statement for test %s missing the call to method parallel in test Run\n " , funcDecl .Name .Name )
167
- }
168
- } else if loopVariableUsedInRun != nil {
169
- pass .Reportf (rangeNode .Pos (), "Range statement for test %s does not reinitialise the variable %s\n " , funcDecl .Name .Name , * loopVariableUsedInRun )
197
+ if analysis .rangeStatementOverTestCasesExists && analysis .rangeNode != nil {
198
+ if ! analysis .rangeStatementHasParallelMethod && ! analysis .rangeStatementCantParallelMethod {
199
+ if ! a .ignoreMissing && ! a .ignoreMissingSubtests {
200
+ pass .Reportf (analysis .rangeNode .Pos (), "Range statement for test %s missing the call to method parallel in test Run\n " , funcDecl .Name .Name )
170
201
}
202
+ } else if analysis .loopVariableUsedInRun != nil && ! a .ignoreLoopVar {
203
+ pass .Reportf (analysis .rangeNode .Pos (), "Range statement for test %s does not reinitialise the variable %s\n " , funcDecl .Name .Name , * analysis .loopVariableUsedInRun )
171
204
}
205
+ }
172
206
173
- // Check if the t.Run is more than one as there is no point making one test parallel
174
- if ! a .ignoreMissing && ! a .ignoreMissingSubtests {
175
- if numberOfTestRun > 1 && len (positionOfTestRunNode ) > 0 {
176
- for _ , n := range positionOfTestRunNode {
177
- pass .Reportf (n .Pos (), "Function %s missing the call to method parallel in the test run\n " , funcDecl .Name .Name )
178
- }
207
+ if ! a .ignoreMissing && ! a .ignoreMissingSubtests {
208
+ if analysis .numberOfTestRun > 1 && len (analysis .positionOfTestRunNode ) > 0 {
209
+ for _ , n := range analysis .positionOfTestRunNode {
210
+ pass .Reportf (n .Pos (), "Function %s missing the call to method parallel in the test run\n " , funcDecl .Name .Name )
179
211
}
180
212
}
213
+ }
214
+ }
215
+
216
+ func (a * parallelAnalyzer ) run (pass * analysis.Pass ) (interface {}, error ) {
217
+ inspector := inspector .New (pass .Files )
218
+
219
+ nodeFilter := []ast.Node {
220
+ (* ast .FuncDecl )(nil ),
221
+ }
222
+
223
+ inspector .Preorder (nodeFilter , func (node ast.Node ) {
224
+ funcDecl := node .(* ast.FuncDecl )
225
+ a .analyzeTestFunction (pass , funcDecl )
181
226
})
182
227
183
228
return nil , nil
@@ -267,8 +312,38 @@ func getRunCallbackParameterName(node ast.Node) string {
267
312
return ""
268
313
}
269
314
270
- // Checks if the function has the param type *testing.T; if it does, then the
271
- // parameter name is returned, too.
315
+ // isFunctionReceivingTestContext checks if a function declaration receives a *testing.T parameter
316
+ // Returns (true, paramName) if it does, (false, "") if it doesn't
317
+ func isFunctionReceivingTestContext (funcDecl * ast.FuncDecl ) (bool , string ) {
318
+ testMethodPackageType := "testing"
319
+ testMethodStruct := "T"
320
+
321
+ if funcDecl .Type .Params != nil && len (funcDecl .Type .Params .List ) != 1 {
322
+ return false , ""
323
+ }
324
+
325
+ param := funcDecl .Type .Params .List [0 ]
326
+ if starExp , ok := param .Type .(* ast.StarExpr ); ok {
327
+ if selectExpr , ok := starExp .X .(* ast.SelectorExpr ); ok {
328
+ if selectExpr .Sel .Name == testMethodStruct {
329
+ if s , ok := selectExpr .X .(* ast.Ident ); ok {
330
+ if len (param .Names ) > 0 {
331
+ return s .Name == testMethodPackageType , param .Names [0 ].Name
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ return false , ""
339
+ }
340
+
341
+ // isTestFunction checks if a function declaration is a test function
342
+ // A test function must:
343
+ // 1. Start with "Test"
344
+ // 2. Have exactly one parameter
345
+ // 3. Have that parameter be of type *testing.T
346
+ // Returns (true, paramName) if it is a test function, (false, "") if it isn't
272
347
func isTestFunction (funcDecl * ast.FuncDecl ) (bool , string ) {
273
348
testMethodPackageType := "testing"
274
349
testMethodStruct := "T"
@@ -298,6 +373,8 @@ func isTestFunction(funcDecl *ast.FuncDecl) (bool, string) {
298
373
return false , ""
299
374
}
300
375
376
+ // loopVarReferencedInRun checks if a loop variable is referenced within a test run
377
+ // This is important for detecting potential race conditions in parallel tests
301
378
func loopVarReferencedInRun (call * ast.CallExpr , vars []types.Object , typeInfo * types.Info ) (found * string ) {
302
379
if len (call .Args ) != 2 {
303
380
return
0 commit comments