Skip to content

Commit 5bd86f2

Browse files
authored
Merge pull request #19 from turbolent/add-ignore-missing-flag
2 parents c61ab88 + e1467c5 commit 5bd86f2

File tree

5 files changed

+220
-18
lines changed

5 files changed

+220
-18
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ The Go linter `paralleltest` checks that the t.Parallel gets called for the test
1010
paralleltest ./...
1111
```
1212

13+
To ignore missing calls to `t.Parallel` and only report incorrect uses of it, pass the flag `-i`.
14+
1315
## Examples
1416

1517
### Missing `t.Parallel()` in the test method

main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ import (
77
)
88

99
func main() {
10-
singlechecker.Main(paralleltest.NewAnalyzer())
10+
singlechecker.Main(paralleltest.Analyzer)
1111
}

pkg/paralleltest/paralleltest.go

+32-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package paralleltest
22

33
import (
4+
"flag"
45
"go/ast"
56
"go/types"
67
"strings"
@@ -15,16 +16,28 @@ It also checks that the t.Parallel is used if multiple tests cases are run as pa
1516
As part of ensuring parallel tests works as expected it checks for reinitialising of the range value
1617
over the test cases.(https://tinyurl.com/y6555cy6)`
1718

18-
func NewAnalyzer() *analysis.Analyzer {
19-
return &analysis.Analyzer{
20-
Name: "paralleltest",
21-
Doc: Doc,
22-
Run: run,
23-
Requires: []*analysis.Analyzer{inspect.Analyzer},
24-
}
19+
var Analyzer = &analysis.Analyzer{
20+
Name: "paralleltest",
21+
Doc: Doc,
22+
Run: run,
23+
Flags: flags(),
24+
Requires: []*analysis.Analyzer{inspect.Analyzer},
2525
}
2626

27+
const ignoreMissingFlag = "i"
28+
29+
func flags() flag.FlagSet {
30+
options := flag.NewFlagSet("", flag.ExitOnError)
31+
options.Bool(ignoreMissingFlag, false, "ignore missing calls to t.Parallel")
32+
return *options
33+
}
34+
35+
type boolValue bool
36+
2737
func run(pass *analysis.Pass) (interface{}, error) {
38+
39+
ignoreMissing := pass.Analyzer.Flags.Lookup(ignoreMissingFlag).Value.(flag.Getter).Get().(bool)
40+
2841
inspector := inspector.New(pass.Files)
2942

3043
nodeFilter := []ast.Node{
@@ -52,12 +65,12 @@ func run(pass *analysis.Pass) (interface{}, error) {
5265

5366
case *ast.ExprStmt:
5467
ast.Inspect(v, func(n ast.Node) bool {
55-
// Check if the test method is calling t.parallel
68+
// Check if the test method is calling t.Parallel
5669
if !funcHasParallelMethod {
5770
funcHasParallelMethod = methodParallelIsCalledInTestFunction(n, testVar)
5871
}
5972

60-
// Check if the t.Run within the test function is calling t.parallel
73+
// Check if the t.Run within the test function is calling t.Parallel
6174
if methodRunIsCalledInTestFunction(n, testVar) {
6275
// n is a call to t.Run; find out the name of the subtest's *testing.T parameter.
6376
innerTestVar := getRunCallbackParameterName(n)
@@ -77,7 +90,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
7790
return true
7891
})
7992

80-
// Check if the range over testcases is calling t.parallel
93+
// Check if the range over testcases is calling t.Parallel
8194
case *ast.RangeStmt:
8295
rangeNode = v
8396

@@ -114,22 +127,26 @@ func run(pass *analysis.Pass) (interface{}, error) {
114127
}
115128
}
116129

117-
if !funcHasParallelMethod {
130+
if !ignoreMissing && !funcHasParallelMethod {
118131
pass.Reportf(node.Pos(), "Function %s missing the call to method parallel\n", funcDecl.Name.Name)
119132
}
120133

121134
if rangeStatementOverTestCasesExists && rangeNode != nil {
122135
if !rangeStatementHasParallelMethod {
123-
pass.Reportf(rangeNode.Pos(), "Range statement for test %s missing the call to method parallel in test Run\n", funcDecl.Name.Name)
136+
if !ignoreMissing {
137+
pass.Reportf(rangeNode.Pos(), "Range statement for test %s missing the call to method parallel in test Run\n", funcDecl.Name.Name)
138+
}
124139
} else if loopVariableUsedInRun != nil {
125140
pass.Reportf(rangeNode.Pos(), "Range statement for test %s does not reinitialise the variable %s\n", funcDecl.Name.Name, *loopVariableUsedInRun)
126141
}
127142
}
128143

129144
// Check if the t.Run is more than one as there is no point making one test parallel
130-
if numberOfTestRun > 1 && len(positionOfTestRunNode) > 0 {
131-
for _, n := range positionOfTestRunNode {
132-
pass.Reportf(n.Pos(), "Function %s has missing the call to method parallel in the test run\n", funcDecl.Name.Name)
145+
if !ignoreMissing {
146+
if numberOfTestRun > 1 && len(positionOfTestRunNode) > 0 {
147+
for _, n := range positionOfTestRunNode {
148+
pass.Reportf(n.Pos(), "Function %s has missing the call to method parallel in the test run\n", funcDecl.Name.Name)
149+
}
133150
}
134151
}
135152
})

pkg/paralleltest/paralleltest_test.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
11
package paralleltest
22

33
import (
4+
"flag"
45
"os"
56
"path/filepath"
67
"testing"
78

89
"golang.org/x/tools/go/analysis/analysistest"
910
)
1011

11-
func TestAll(t *testing.T) {
12+
func TestMissing(t *testing.T) {
1213
t.Parallel()
14+
1315
wd, err := os.Getwd()
1416
if err != nil {
1517
t.Fatalf("Failed to get wd: %s", err)
1618
}
1719

1820
testdata := filepath.Join(filepath.Dir(wd), "paralleltest", "testdata")
19-
analysistest.Run(t, testdata, NewAnalyzer(), "t")
21+
analysistest.Run(t, testdata, Analyzer, "t")
22+
}
23+
24+
func TestIgnoreMissing(t *testing.T) {
25+
t.Parallel()
26+
27+
wd, err := os.Getwd()
28+
if err != nil {
29+
t.Fatalf("Failed to get wd: %s", err)
30+
}
31+
32+
options := flag.NewFlagSet("", flag.ExitOnError)
33+
options.Bool("i", true, "")
34+
35+
analyzer := *Analyzer
36+
analyzer.Flags = *options
37+
38+
testdata := filepath.Join(filepath.Dir(wd), "paralleltest", "testdata")
39+
analysistest.Run(t, testdata, &analyzer, "i")
2040
}
+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package t
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func NoATestFunction() {}
9+
func TestingFunctionLooksLikeATestButIsNotWithParam() {}
10+
func TestingFunctionLooksLikeATestButIsWithParam(i int) {}
11+
func AbcFunctionSuccessful(t *testing.T) {}
12+
13+
func TestFunctionSuccessfulRangeTest(t *testing.T) {
14+
t.Parallel()
15+
16+
testCases := []struct {
17+
name string
18+
}{{name: "foo"}}
19+
for _, tc := range testCases {
20+
tc := tc
21+
t.Run(tc.name, func(x *testing.T) {
22+
x.Parallel()
23+
fmt.Println(tc.name)
24+
})
25+
}
26+
}
27+
28+
func TestFunctionSuccessfulNoRangeTest(t *testing.T) {
29+
t.Parallel()
30+
31+
testCases := []struct {
32+
name string
33+
}{{name: "foo"}, {name: "bar"}}
34+
35+
t.Run(testCases[0].name, func(t *testing.T) {
36+
t.Parallel()
37+
fmt.Println(testCases[0].name)
38+
})
39+
t.Run(testCases[1].name, func(t *testing.T) {
40+
t.Parallel()
41+
fmt.Println(testCases[1].name)
42+
})
43+
44+
}
45+
46+
func TestFunctionMissingCallToParallel(t *testing.T) {}
47+
func TestFunctionRangeMissingCallToParallel(t *testing.T) {
48+
t.Parallel()
49+
50+
testCases := []struct {
51+
name string
52+
}{{name: "foo"}}
53+
54+
// this range loop should be okay as it does not have test Run
55+
for _, tc := range testCases {
56+
fmt.Println(tc.name)
57+
}
58+
59+
for _, tc := range testCases {
60+
t.Run(tc.name, func(t *testing.T) {
61+
fmt.Println(tc.name)
62+
})
63+
}
64+
}
65+
66+
func TestFunctionMissingCallToParallelAndRangeNotUsingRangeValueInTDotRun(t *testing.T) {
67+
testCases := []struct {
68+
name string
69+
}{{name: "foo"}}
70+
71+
for _, tc := range testCases {
72+
t.Run(tc.name, func(t *testing.T) {
73+
fmt.Println(tc.name)
74+
})
75+
}
76+
}
77+
78+
func TestFunctionRangeNotUsingRangeValueInTDotRun(t *testing.T) {
79+
t.Parallel()
80+
81+
testCases := []struct {
82+
name string
83+
}{{name: "foo"}}
84+
for _, tc := range testCases { // want "Range statement for test TestFunctionRangeNotUsingRangeValueInTDotRun does not reinitialise the variable tc"
85+
t.Run("tc.name", func(t *testing.T) {
86+
t.Parallel()
87+
fmt.Println(tc.name)
88+
})
89+
}
90+
}
91+
92+
func TestFunctionRangeNotReInitialisingVariable(t *testing.T) {
93+
t.Parallel()
94+
95+
testCases := []struct {
96+
name string
97+
}{{name: "foo"}}
98+
for _, tc := range testCases { // want "Range statement for test TestFunctionRangeNotReInitialisingVariable does not reinitialise the variable tc"
99+
t.Run(tc.name, func(t *testing.T) {
100+
t.Parallel()
101+
fmt.Println(tc.name)
102+
})
103+
}
104+
}
105+
106+
func TestFunctionTwoTestRunMissingCallToParallel(t *testing.T) {
107+
t.Parallel()
108+
109+
t.Run("1", func(t *testing.T) {
110+
fmt.Println("1")
111+
})
112+
t.Run("2", func(t *testing.T) {
113+
fmt.Println("2")
114+
})
115+
}
116+
117+
func TestFunctionFirstOneTestRunMissingCallToParallel(t *testing.T) {
118+
t.Parallel()
119+
120+
t.Run("1", func(t *testing.T) {
121+
fmt.Println("1")
122+
})
123+
t.Run("2", func(t *testing.T) {
124+
t.Parallel()
125+
fmt.Println("2")
126+
})
127+
}
128+
129+
func TestFunctionSecondOneTestRunMissingCallToParallel(t *testing.T) {
130+
t.Parallel()
131+
132+
t.Run("1", func(x *testing.T) {
133+
x.Parallel()
134+
fmt.Println("1")
135+
})
136+
t.Run("2", func(t *testing.T) {
137+
fmt.Println("2")
138+
})
139+
}
140+
141+
type notATest int
142+
143+
func (notATest) Run(args ...interface{}) {}
144+
func (notATest) Parallel() {}
145+
146+
func TestFunctionWithRunLookalike(t *testing.T) {
147+
t.Parallel()
148+
149+
var other notATest
150+
// These aren't t.Run, so they shouldn't be flagged as Run invocations missing calls to Parallel.
151+
other.Run(1, 1)
152+
other.Run(2, 2)
153+
}
154+
155+
func TestFunctionWithParallelLookalike(t *testing.T) {
156+
var other notATest
157+
// This isn't t.Parallel, so it doesn't qualify as a call to Parallel.
158+
other.Parallel()
159+
}
160+
161+
func TestFunctionWithOtherTestingVar(q *testing.T) {
162+
q.Parallel()
163+
}

0 commit comments

Comments
 (0)