A Go static analysis tool that warns when contexts with a deadline are used in parallel subtests.
See our blog post for an in-depth explanation of the problem.
go run github.com/coder/paralleltestctx/cmd/paralleltestctx@latest ./...By default, the linter detects context.WithTimeout and context.WithDeadline
as producing contexts with timeouts or deadlines. Additional functions that
create a context with a deadline or timeout can be specified using the
-custom-funcs flag.
go run github.com/coder/paralleltestctx/cmd/paralleltestctx@latest -custom-funcs="testutil.Context" ./...Sibling parallel subtests can race on a variable declared in an enclosing
scope. The fix is almost always to shadow with :=.
func TestBad(t *testing.T) {
t.Parallel()
var err error
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// WARNING: `err` is shared across parallel subtests.
_, err = doThing(tc)
require.NoError(t, err)
})
}
}func TestGood(t *testing.T) {
t.Parallel()
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := doThing(tc) // shadowed
require.NoError(t, err)
})
}
}func TestBad(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// WARNING: do not do this!
t.Parallel()
uut := newUnitUnderTest()
// Danger! Context may have timed out by this point
writeCtx(ctx, t, uut.Input, 5)
output := readCtx(ctx, t, uut.Output)
if output != 25 {
t.Errorf("expected 25 got %d", output)
}
}func TestBad(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
t.Cleanup(cancel)
t.Run("sub", func(t *testing.T) {
t.Parallel()
// Danger! Context may have timed out by this point
doSomething(ctx) // Warning: timeout context used after t.Parallel call
})
}func TestGood(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
t.Cleanup(cancel)
t.Run("sub", func(t *testing.T) {
t.Parallel()
// We're shadowing the parent context with a new deadline, so this is fine
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
t.Cleanup(cancel)
doSomething(ctx)
})
}This project is licensed under the MIT License - see the LICENSE file for details.