diff --git a/README.md b/README.md
index 3aa172c3..30542afc 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ $ testifylint --enable=suite-extra-assert-call --suite-extra-assert-call.mode=re
| [error-nil](#error-nil) | ✅ | ✅ |
| [expected-actual](#expected-actual) | ✅ | ✅ |
| [float-compare](#float-compare) | ✅ | ❌ |
+| [go-require](#go-require) | ✅ | ❌ |
| [len](#len) | ✅ | ✅ |
| [nil-compare](#nil-compare) | ✅ | ✅ |
| [require-error](#require-error) | ✅ | ❌ |
@@ -156,7 +157,7 @@ $ testifylint --enable=suite-extra-assert-call --suite-extra-assert-call.mode=re
**Reason**: In the first two cases, a common mistake that leads to hiding the incorrect wrapping of sentinel errors.
In the rest cases – more appropriate `testify` API with clearer failure message.
-Also `error-is-as` repeats go vet's [errorsas check](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/errorsas/errorsas.go)
+Also `error-is-as` repeats `go vet`'s [errorsas check](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/errorsas/errorsas.go)
logic, but without autofix.
---
@@ -226,6 +227,48 @@ This checker is similar to the [floatcompare](https://github.com/golangci/golang
---
+### go-require
+
+```go
+go func() {
+ conn, err = lis.Accept()
+ require.NoError(t, err) ❌
+
+ if assert.Error(err) {
+ assert.FailNow(t, msg) ❌
+ }
+}()
+```
+
+**Autofix**: false.
+**Enabled by default**: true.
+**Reason**: Incorrect use of functions.
+
+This checker is a radically improved analogue of `go vet`'s
+[testinggoroutine](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/testinggoroutine/doc.go) check.
+
+The point of the check is that, according to the [documentation](https://pkg.go.dev/testing#T),
+functions leading to `t.FailNow` (essentially to `runtime.GoExit`) must only be used in the goroutine that runs the test.
+Otherwise, they will not work as declared, namely, finish the test function.
+
+You can disable the `go-require` checker and continue to use `require` as the current goroutine finisher, but this could lead
+1. to possible resource leaks in tests;
+2. to increased confusion, because functions will be not used as intended.
+
+Typically, any assertions inside goroutines are a marker of poor test architecture.
+Try to execute them in the main goroutine and distribute the data necessary for this into it
+([example](https://github.com/ipfs/kubo/issues/2043#issuecomment-164136026)).
+
+Also a bad solution would be to simply replace all `require` in goroutines with `assert`
+(like [here](https://github.com/gravitational/teleport/pull/22567/files#diff-9f5fd20913c5fe80c85263153fa9a0b28dbd1407e53da4ab5d09e13d2774c5dbR7377))
+– this will only mask the problem.
+
+The checker is enabled by default, because `testinggoroutine` is enabled by default in `go vet`.
+
+P.S. Related `testify`'s [thread](https://github.com/stretchr/testify/issues/772).
+
+---
+
### len
```go
@@ -274,7 +317,8 @@ This checker is similar to the [floatcompare](https://github.com/golangci/golang
**Enabled by default**: true.
**Reason**: Such "ignoring" of errors leads to further panics, making the test harder to debug.
-`testify/require` allows to stop test execution when a test fails.
+[testify/require](https://pkg.go.dev/github.com/stretchr/testify@master/require#hdr-Assertions) allows
+to stop test execution when a test fails.
To minimize the number of false positives, `require-error` ignores:
- assertion in the `if` condition;
diff --git a/analyzer/checkers_factory_test.go b/analyzer/checkers_factory_test.go
index ea7d8094..ea252431 100644
--- a/analyzer/checkers_factory_test.go
+++ b/analyzer/checkers_factory_test.go
@@ -40,9 +40,11 @@ func Test_newCheckers(t *testing.T) {
}
enabledByDefaultAdvancedCheckers := []checkers.AdvancedChecker{
+ checkers.NewGoRequire(),
checkers.NewRequireError(),
}
allAdvancedCheckers := []checkers.AdvancedChecker{
+ checkers.NewGoRequire(),
checkers.NewRequireError(),
checkers.NewSuiteTHelper(),
}
diff --git a/analyzer/testdata/src/checkers-default/go-require/go_require_test.go b/analyzer/testdata/src/checkers-default/go-require/go_require_test.go
new file mode 100644
index 00000000..7b037e6a
--- /dev/null
+++ b/analyzer/testdata/src/checkers-default/go-require/go_require_test.go
@@ -0,0 +1,681 @@
+// Code generated by testifylint/internal/testgen. DO NOT EDIT.
+
+package gorequire
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+)
+
+func TestGoRequireChecker_Smoke(t *testing.T) {
+ var err error
+ var b bool
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+
+ assert.Fail(t, "boom!")
+ assert.Failf(t, "boom!", "msg with args %d %s", 42, "42")
+ assert.FailNow(t, "boom!")
+ assert.FailNowf(t, "boom!", "msg with args %d %s", 42, "42")
+ assert.NoError(t, err)
+ assert.NoErrorf(t, err, "msg with args %d %s", 42, "42")
+ assert.True(t, b)
+ assert.Truef(t, b, "msg with args %d %s", 42, "42")
+
+ require.Fail(t, "boom!")
+ require.Failf(t, "boom!", "msg with args %d %s", 42, "42")
+ require.FailNow(t, "boom!")
+ require.FailNowf(t, "boom!", "msg with args %d %s", 42, "42")
+ require.NoError(t, err)
+ require.NoErrorf(t, err, "msg with args %d %s", 42, "42")
+ require.True(t, b)
+ require.Truef(t, b, "msg with args %d %s", 42, "42")
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ for i := 0; i < 2; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+
+ assert.Fail(t, "boom!")
+ assert.Failf(t, "boom!", "msg with args %d %s", 42, "42")
+ assert.FailNow(t, "boom!") // want "go-require: assert\\.FailNow must only be used in the goroutine running the test function"
+ assert.FailNowf(t, "boom!", "msg with args %d %s", 42, "42") // want "go-require: assert\\.FailNowf must only be used in the goroutine running the test function"
+ assert.NoError(t, err)
+ assert.NoErrorf(t, err, "msg with args %d %s", 42, "42")
+ assert.True(t, b)
+ assert.Truef(t, b, "msg with args %d %s", 42, "42")
+
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ require.Failf(t, "boom!", "msg with args %d %s", 42, "42") // want "go-require: require must only be used in the goroutine running the test function"
+ require.FailNow(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ require.FailNowf(t, "boom!", "msg with args %d %s", 42, "42") // want "go-require: require must only be used in the goroutine running the test function"
+ require.NoError(t, err) // want "go-require: require must only be used in the goroutine running the test function"
+ require.NoErrorf(t, err, "msg with args %d %s", 42, "42") // want "go-require: require must only be used in the goroutine running the test function"
+ require.True(t, b) // want "go-require: require must only be used in the goroutine running the test function"
+ require.Truef(t, b, "msg with args %d %s", 42, "42") // want "go-require: require must only be used in the goroutine running the test function"
+
+ if assert.Error(t, err) {
+ assert.FailNow(t, "boom!") // want "go-require: assert\\.FailNow must only be used in the goroutine running the test function"
+ assert.FailNowf(t, "boom!", "msg with args %d %s", 42, "42") // want "go-require: assert\\.FailNowf must only be used in the goroutine running the test function"
+ }
+ }(i)
+ }
+}
+
+func TestGoRequireChecker(t *testing.T) {
+ defer func() {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ func() {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ }()
+ }()
+
+ defer func() {
+ go func() {
+
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+ }()
+
+ defer run()
+ defer assertSomething(t)
+ defer requireSomething(t)
+
+ t.Cleanup(func() {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ func() {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ }()
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ func() {
+ func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }(t)
+ }()
+ }()
+ })
+
+ func() {
+ func() {
+ func() {
+ func() {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ }()
+
+ func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+ })
+ }(t)
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+ }()
+ }()
+ }()
+
+ if false {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ }
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ go func() {
+ go genericHelper[*testing.T](t) // want "go-require: genericHelper\\[\\*testing\\.T\\] contains assertions that must only be used in the goroutine running the test function"
+ go superGenericHelper[*testing.T, int](t) // want "go-require: superGenericHelper\\[\\*testing\\.T, int\\] contains assertions that must only be used in the goroutine running the test function"
+ }()
+ }()
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ go func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ if true {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }
+ }(t)
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+
+ cases := []struct{}{}
+ for _, tt := range cases {
+ tt := tt
+ t.Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ _ = tt
+ }()
+ })
+ }
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+
+ go run()
+ go assertSomething(t)
+ go requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+
+ go func() {
+ run()
+ }()
+ go func() {
+ assertSomething(t)
+ }()
+ go func() {
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ }()
+
+ var err error
+ var b bool
+
+ go assert.Fail(t, "boom!")
+ go assert.FailNow(t, "boom!") // want "go-require: assert\\.FailNow must only be used in the goroutine running the test function"
+ go assert.NoError(t, err)
+ go assert.True(t, b)
+
+ go require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ go require.FailNow(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ go require.NoError(t, err) // want "go-require: require must only be used in the goroutine running the test function"
+ go require.True(t, b) // want "go-require: require must only be used in the goroutine running the test function"
+
+ go requireSomethingInGo(t)
+ go func() {
+ requireSomethingInGo(t)
+ }()
+
+ go proxy(t) // want "go-require: proxy contains assertions that must only be used in the goroutine running the test function"
+ go func() {
+ proxy(t) // want "go-require: proxy contains assertions that must only be used in the goroutine running the test function"
+ }()
+
+ t.Run("", assertSomething)
+ t.Run("", requireSomething)
+
+ go t.Run("", assertSomething)
+ go t.Run("", requireSomething)
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+ for i := 1; i <= 2; i++ {
+ i := i
+ go t.Run(fmt.Sprintf("uncaught-%d", i), func(t *testing.T) {
+ defer wg.Done()
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+ }
+ wg.Wait()
+
+ genericHelper(t)
+ genericHelper[*testing.T](t)
+ superGenericHelper[*testing.T, int](t)
+
+ go func() {
+ genericHelper(t) // want "go-require: genericHelper contains assertions that must only be used in the goroutine running the test function"
+ genericHelper[*testing.T](t) // want "go-require: genericHelper\\[\\*testing\\.T\\] contains assertions that must only be used in the goroutine running the test function"
+ superGenericHelper[*testing.T, int](t) // want "go-require: superGenericHelper\\[\\*testing\\.T, int\\] contains assertions that must only be used in the goroutine running the test function"
+ }()
+}
+
+type GoRequireCheckerSuite struct {
+ suite.Suite
+}
+
+func TestGoRequireCheckerSuite(t *testing.T) {
+ suite.Run(t, new(GoRequireCheckerSuite))
+}
+
+func (s *GoRequireCheckerSuite) TestAll() {
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ s.Fail("boom!")
+ s.Require().Fail("boom!")
+
+ go func() {
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T()) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ s.suiteHelper() // want "go-require: s.suiteHelper contains assertions that must only be used in the goroutine running the test function"
+ s.Fail("boom!")
+ s.Require().Fail("boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ s.Run("", func() {
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ s.Fail("boom!")
+ s.Require().Fail("boom!")
+
+ go func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+
+ s.T().Run("", func(t *testing.T) {
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+ })
+
+ if true {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }
+ }(s.T())
+
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ s.Fail("boom!")
+ s.Require().Fail("boom!")
+ })
+
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T()) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ s.suiteHelper() // want "go-require: s.suiteHelper contains assertions that must only be used in the goroutine running the test function"
+ s.Fail("boom!")
+ s.Require().Fail("boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ s.Fail("boom!")
+ s.Require().Fail("boom!")
+
+ go run()
+ go assertSomething(s.T())
+ go requireSomething(s.T()) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ go s.suiteHelper() // want "go-require: s.suiteHelper contains assertions that must only be used in the goroutine running the test function"
+
+ s.Run("", s.suiteHelper)
+ s.T().Run("", func(t *testing.T) { requireSomething(t) })
+
+ go s.T().Run("", requireSomething)
+ go s.Run("", s.suiteHelper)
+}
+
+func (s *GoRequireCheckerSuite) TestAsertFailNow() {
+ var err error
+ var b bool
+
+ s.Fail("boom!")
+ s.Failf("boom!", "msg with args %d %s", 42, "42")
+ s.Assert().Fail("boom!")
+ s.Assert().Failf("boom!", "msg with args %d %s", 42, "42")
+ s.FailNow("boom!")
+ s.FailNowf("boom!", "msg with args %d %s", 42, "42")
+ s.Assert().FailNow("boom!")
+ s.Assert().FailNowf("boom!", "msg with args %d %s", 42, "42")
+ s.NoError(err)
+ s.NoErrorf(err, "msg with args %d %s", 42, "42")
+ s.Assert().NoError(err)
+ s.Assert().NoErrorf(err, "msg with args %d %s", 42, "42")
+ s.True(b)
+ s.Truef(b, "msg with args %d %s", 42, "42")
+ s.Assert().True(b)
+ s.Assert().Truef(b, "msg with args %d %s", 42, "42")
+
+ go func() {
+ s.Fail("boom!")
+ s.Failf("boom!", "msg with args %d %s", 42, "42")
+ s.Assert().Fail("boom!")
+ s.Assert().Failf("boom!", "msg with args %d %s", 42, "42")
+ s.FailNow("boom!") // want "go-require: s\\.FailNow must only be used in the goroutine running the test function"
+ s.FailNowf("boom!", "msg with args %d %s", 42, "42") // want "go-require: s\\.FailNowf must only be used in the goroutine running the test function"
+ s.Assert().FailNow("boom!") // want "go-require: s\\.Assert\\(\\)\\.FailNow must only be used in the goroutine running the test function"
+ s.Assert().FailNowf("boom!", "msg with args %d %s", 42, "42") // want "go-require: s\\.Assert\\(\\)\\.FailNowf must only be used in the goroutine running the test function"
+ s.NoError(err)
+ s.NoErrorf(err, "msg with args %d %s", 42, "42")
+ s.Assert().NoError(err)
+ s.Assert().NoErrorf(err, "msg with args %d %s", 42, "42")
+ s.True(b)
+ s.Truef(b, "msg with args %d %s", 42, "42")
+ s.Assert().True(b)
+ s.Assert().Truef(b, "msg with args %d %s", 42, "42")
+ }()
+}
+
+func (s *GoRequireCheckerSuite) suiteHelper() {
+ s.T().Helper()
+
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ s.Fail("boom!")
+ s.Require().Fail("boom!")
+
+ go func() {
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T()) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ s.suiteHelper() // want "go-require: s.suiteHelper contains assertions that must only be used in the goroutine running the test function"
+ s.Fail("boom!")
+ s.Require().Fail("boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ s.Fail("boom!")
+ s.Require().Fail("boom!")
+}
+
+func run() {}
+
+func assertSomething(t *testing.T) {
+ t.Helper()
+
+ assert.NoError(t, nil)
+ assert.Error(t, nil)
+ assert.True(t, false)
+}
+
+func proxy(t *testing.T) {
+ requireSomething(t)
+}
+
+func requireSomething(t *testing.T) {
+ t.Helper()
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+}
+
+func requireSomethingInGo(t *testing.T) {
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+}
+
+func helperNotUsedInGoroutine(t *testing.T) {
+ t.Helper()
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ go func() {
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "go-require: requireSomething contains assertions that must only be used in the goroutine running the test function"
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+}
+
+type testingT interface {
+ require.TestingT
+}
+
+func genericHelper[T testingT](t T) {
+ run()
+
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!")
+
+ go func() {
+ assert.Fail(t, "boom!")
+ require.Fail(t, "boom!") // want "go-require: require must only be used in the goroutine running the test function"
+ }()
+}
+
+func superGenericHelper[T testingT, T2 any](t T) T2 {
+ require.Fail(t, "boom!")
+ var zero T2
+ return zero
+}
diff --git a/internal/checkers/call_meta.go b/internal/checkers/call_meta.go
index c9102c44..a6b8f1c1 100644
--- a/internal/checkers/call_meta.go
+++ b/internal/checkers/call_meta.go
@@ -35,6 +35,10 @@ type CallMeta struct {
ArgsRaw []ast.Expr
}
+func (c CallMeta) String() string {
+ return c.SelectorXStr + "." + c.Fn.Name
+}
+
// FnMeta stores meta info about assertion function itself, for example "Equal".
type FnMeta struct {
// Range contains start and end position of function Name.
diff --git a/internal/checkers/checkers_registry.go b/internal/checkers/checkers_registry.go
index 8259c9e6..2a3b673c 100644
--- a/internal/checkers/checkers_registry.go
+++ b/internal/checkers/checkers_registry.go
@@ -19,6 +19,7 @@ var registry = checkersRegistry{
{factory: asCheckerFactory(NewSuiteExtraAssertCall), enabledByDefault: true},
{factory: asCheckerFactory(NewSuiteDontUsePkg), enabledByDefault: true},
// Advanced checkers.
+ {factory: asCheckerFactory(NewGoRequire), enabledByDefault: true},
{factory: asCheckerFactory(NewRequireError), enabledByDefault: true},
{factory: asCheckerFactory(NewSuiteTHelper), enabledByDefault: false},
}
diff --git a/internal/checkers/checkers_registry_test.go b/internal/checkers/checkers_registry_test.go
index a0f0ca92..828f9fb1 100644
--- a/internal/checkers/checkers_registry_test.go
+++ b/internal/checkers/checkers_registry_test.go
@@ -46,6 +46,7 @@ func TestAll(t *testing.T) {
"expected-actual",
"suite-extra-assert-call",
"suite-dont-use-pkg",
+ "go-require",
"require-error",
"suite-thelper",
}
@@ -73,6 +74,7 @@ func TestEnabledByDefault(t *testing.T) {
"expected-actual",
"suite-extra-assert-call",
"suite-dont-use-pkg",
+ "go-require",
"require-error",
}
if !slices.Equal(expected, checkerList) {
diff --git a/internal/checkers/error_is_as.go b/internal/checkers/error_is_as.go
index 7b0d315a..5e3b682a 100644
--- a/internal/checkers/error_is_as.go
+++ b/internal/checkers/error_is_as.go
@@ -32,7 +32,7 @@ func NewErrorIsAs() ErrorIsAs { return ErrorIsAs{} }
func (ErrorIsAs) Name() string { return "error-is-as" }
func (checker ErrorIsAs) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic {
- switch fnName := call.Fn.Name; fnName {
+ switch call.Fn.Name {
case "Error", "Errorf":
if len(call.Args) >= 2 && isError(pass, call.Args[1]) {
const proposed = "ErrorIs"
@@ -110,8 +110,8 @@ func (checker ErrorIsAs) Check(pass *analysis.Pass, call *CallMeta) *analysis.Di
// https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/errorsas/errorsas.go
var (
- defaultReport = fmt.Sprintf("second argument to %s.%s must be a non-nil pointer to either a type that implements error, or to any interface type", call.SelectorXStr, fnName) //nolint:lll
- errorPtrReport = fmt.Sprintf("second argument to %s.%s should not be *error", call.SelectorXStr, fnName)
+ defaultReport = fmt.Sprintf("second argument to %s must be a non-nil pointer to either a type that implements error, or to any interface type", call) //nolint:lll
+ errorPtrReport = fmt.Sprintf("second argument to %s should not be *error", call)
)
target := call.Args[1]
diff --git a/internal/checkers/go_require.go b/internal/checkers/go_require.go
new file mode 100644
index 00000000..dae1c8dd
--- /dev/null
+++ b/internal/checkers/go_require.go
@@ -0,0 +1,298 @@
+package checkers
+
+import (
+ "fmt"
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/ast/inspector"
+
+ "github.com/Antonboom/testifylint/internal/analysisutil"
+)
+
+const (
+ goRequireFnReportFormat = "%s contains assertions that must only be used in the goroutine running the test function"
+ goRequireCallReportFormat = "%s must only be used in the goroutine running the test function"
+)
+
+// GoRequire takes idea from go vet's "testinggoroutine" check
+// and detects usage of require package's functions or assert.FailNow in the goroutines
+//
+// go func() {
+// conn, err = lis.Accept()
+// require.NoError(t, err)
+//
+// if assert.Error(err) {
+// assert.FailNow(t, msg)
+// }
+// }()
+type GoRequire struct{}
+
+// NewGoRequire constructs GoRequire checker.
+func NewGoRequire() GoRequire { return GoRequire{} }
+func (GoRequire) Name() string { return "go-require" }
+
+// Check should be consistent with
+// https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/testinggoroutine/testinggoroutine.go
+//
+// But due to the fact that the Check covers cases missed by go vet,
+// the implementation turned out to be terribly complicated.
+//
+// In simple words, the algorithm is as follows:
+// - we walk along the call tree and store the status, whether we are in the test goroutine or not;
+// - if we are in a test goroutine, then require is allowed, otherwise not;
+// - when we encounter the launch of a subtest or `go` statement, the status changes;
+// - in order to correctly handle the return to the correct status when exiting the current function,
+// we have to store a stack of statuses (inGoroutineRunningTestFunc).
+//
+// Other test functions called in the test function are also analyzed to make a verdict about the current function.
+// This leads to recursion, which the cache of processed functions (processedFuncs) helps reduce the impact of.
+// Also, because of this, we have to pre-collect a list of test function declarations (testsDecls).
+func (checker GoRequire) Check(pass *analysis.Pass, inspector *inspector.Inspector) (diagnostics []analysis.Diagnostic) {
+ testsDecls := make(funcDeclarations)
+ inspector.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(node ast.Node) {
+ fd := node.(*ast.FuncDecl)
+
+ if hasTestingTParam(pass, fd) || isTestifySuiteMethod(pass, fd) {
+ if tf, ok := pass.TypesInfo.ObjectOf(fd.Name).(*types.Func); ok {
+ testsDecls[tf] = fd
+ }
+ }
+ })
+
+ var inGoroutineRunningTestFunc boolStack
+ processedFuncs := make(map[*ast.FuncDecl]goRequireVerdict)
+
+ nodesFilter := []ast.Node{
+ (*ast.FuncDecl)(nil),
+ (*ast.GoStmt)(nil),
+ (*ast.CallExpr)(nil),
+ }
+ inspector.Nodes(nodesFilter, func(node ast.Node, push bool) bool {
+ if fd, ok := node.(*ast.FuncDecl); ok {
+ if !hasTestingTParam(pass, fd) && !isTestifySuiteMethod(pass, fd) {
+ // Not testing function.
+ return false
+ }
+
+ if push {
+ inGoroutineRunningTestFunc.Push(true)
+ } else {
+ inGoroutineRunningTestFunc.Pop()
+ }
+ return true
+ }
+
+ if _, ok := node.(*ast.GoStmt); ok {
+ if push {
+ inGoroutineRunningTestFunc.Push(false)
+ } else {
+ inGoroutineRunningTestFunc.Pop()
+ }
+ return true
+ }
+
+ ce := node.(*ast.CallExpr)
+ if isSubTestRun(pass, ce) {
+ if push {
+ // t.Run spawns the new testing goroutine and declines
+ // possible warnings from previous "simple" goroutine.
+ inGoroutineRunningTestFunc.Push(true)
+ } else {
+ inGoroutineRunningTestFunc.Pop()
+ }
+ return true
+ }
+
+ if !push {
+ return false
+ }
+ if inGoroutineRunningTestFunc.Last() {
+ // We are in testing goroutine and can skip any assertion checks.
+ return true
+ }
+
+ testifyCall := NewCallMeta(pass, ce)
+ if testifyCall != nil {
+ switch checker.checkCall(testifyCall) {
+ case goRequireVerdictRequire:
+ d := newDiagnostic(checker.Name(), testifyCall, fmt.Sprintf(goRequireCallReportFormat, "require"), nil)
+ diagnostics = append(diagnostics, *d)
+
+ case goRequireVerdictAssertFailNow:
+ d := newDiagnostic(checker.Name(), testifyCall, fmt.Sprintf(goRequireCallReportFormat, testifyCall), nil)
+ diagnostics = append(diagnostics, *d)
+
+ case goRequireVerdictNoExit:
+ }
+ return false
+ }
+
+ // Case of nested function call.
+ {
+ calledFd := testsDecls.Get(pass, ce)
+ if calledFd == nil {
+ return true
+ }
+
+ if v := checker.checkFunc(pass, calledFd, testsDecls, processedFuncs); v != goRequireVerdictNoExit {
+ caller := analysisutil.NodeString(pass.Fset, ce.Fun)
+ d := newDiagnostic(checker.Name(), ce, fmt.Sprintf(goRequireFnReportFormat, caller), nil)
+ diagnostics = append(diagnostics, *d)
+ }
+ }
+ return true
+ })
+
+ return diagnostics
+}
+
+func (checker GoRequire) checkFunc(
+ pass *analysis.Pass,
+ fd *ast.FuncDecl,
+ testsDecls funcDeclarations,
+ processedFuncs map[*ast.FuncDecl]goRequireVerdict,
+) (result goRequireVerdict) {
+ if v, ok := processedFuncs[fd]; ok {
+ return v
+ }
+
+ ast.Inspect(fd, func(node ast.Node) bool {
+ if result != goRequireVerdictNoExit {
+ return false
+ }
+
+ if _, ok := node.(*ast.GoStmt); ok {
+ return false
+ }
+
+ ce, ok := node.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ testifyCall := NewCallMeta(pass, ce)
+ if testifyCall != nil {
+ if v := checker.checkCall(testifyCall); v != goRequireVerdictNoExit {
+ result, processedFuncs[fd] = v, v
+ }
+ return false
+ }
+
+ // Case of nested function call.
+ {
+ calledFd := testsDecls.Get(pass, ce)
+ if calledFd == nil {
+ return true
+ }
+ if calledFd == fd {
+ // Recursion.
+ return true
+ }
+
+ if v := checker.checkFunc(pass, calledFd, testsDecls, processedFuncs); v != goRequireVerdictNoExit {
+ result = v
+ return false
+ }
+ return true
+ }
+ })
+
+ return result
+}
+
+type goRequireVerdict int
+
+const (
+ goRequireVerdictNoExit goRequireVerdict = iota
+ goRequireVerdictRequire
+ goRequireVerdictAssertFailNow
+)
+
+func (checker GoRequire) checkCall(call *CallMeta) goRequireVerdict {
+ if !call.IsAssert {
+ return goRequireVerdictRequire
+ }
+ if fnName := call.Fn.Name; (fnName == "FailNow") || (fnName == "FailNowf") {
+ return goRequireVerdictAssertFailNow
+ }
+ return goRequireVerdictNoExit
+}
+
+type funcDeclarations map[*types.Func]*ast.FuncDecl
+
+// Get returns the declaration of a called function or method.
+// Currently, only static calls within the same package are supported, otherwise returns nil.
+func (fd funcDeclarations) Get(pass *analysis.Pass, ce *ast.CallExpr) *ast.FuncDecl {
+ var obj types.Object
+
+ switch fun := ce.Fun.(type) {
+ case *ast.SelectorExpr:
+ obj = pass.TypesInfo.ObjectOf(fun.Sel)
+
+ case *ast.Ident:
+ obj = pass.TypesInfo.ObjectOf(fun)
+
+ case *ast.IndexExpr:
+ if id, ok := fun.X.(*ast.Ident); ok {
+ obj = pass.TypesInfo.ObjectOf(id)
+ }
+
+ case *ast.IndexListExpr:
+ if id, ok := fun.X.(*ast.Ident); ok {
+ obj = pass.TypesInfo.ObjectOf(id)
+ }
+ }
+
+ if tf, ok := obj.(*types.Func); ok {
+ return fd[tf]
+ }
+ return nil
+}
+
+type boolStack []bool
+
+func (s *boolStack) Push(v bool) {
+ *s = append(*s, v)
+}
+
+func (s *boolStack) Pop() bool {
+ n := len(*s)
+ if n == 0 {
+ return false
+ }
+
+ last := (*s)[n-1]
+ *s = (*s)[:n-1]
+ return last
+}
+
+func (s boolStack) Last() bool {
+ n := len(s)
+ if n == 0 {
+ return false
+ }
+ return s[n-1]
+}
+
+func isSubTestRun(pass *analysis.Pass, ce *ast.CallExpr) bool {
+ se, ok := ce.Fun.(*ast.SelectorExpr)
+ if !ok || se.Sel == nil {
+ return false
+ }
+ return (isTestingTPtr(pass, se.X) || implementsTestifySuiteIface(pass, se.X)) && se.Sel.Name == "Run"
+}
+
+func hasTestingTParam(pass *analysis.Pass, fd *ast.FuncDecl) bool {
+ if fd.Type == nil || fd.Type.Params == nil {
+ return false
+ }
+
+ for _, param := range fd.Type.Params.List {
+ if isTestingTPtr(pass, param.Type) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/testgen/assertion_expander.go b/internal/testgen/assertion_expander.go
index 4de7f78e..db8bce2a 100644
--- a/internal/testgen/assertion_expander.go
+++ b/internal/testgen/assertion_expander.go
@@ -124,7 +124,7 @@ func buildAssertion(selector, fn, args, reportedMsgf, proposedSel, proposedFn st
if or(proposedSel, proposedFn) != "" {
reportedMsgf = fmt.Sprintf(reportedMsgf, or(proposedSel, selector), or(proposedFn, fn))
}
- s += " // want " + quoteReport(reportedMsgf)
+ s += " // want " + QuoteReport(reportedMsgf)
}
return s
}
@@ -147,6 +147,6 @@ func withSuffixF(s string) string {
return s + "f"
}
-func quoteReport(msg string) string {
+func QuoteReport(msg string) string {
return fmt.Sprintf("%q", regexp.QuoteMeta(msg))
}
diff --git a/internal/testgen/gen.go b/internal/testgen/gen.go
index 26eb0dde..4ed7f9e3 100644
--- a/internal/testgen/gen.go
+++ b/internal/testgen/gen.go
@@ -11,6 +11,7 @@ const header = "// Code generated by testifylint/internal/testgen. DO NOT EDIT."
var fm = template.FuncMap{
"NewAssertionExpander": NewAssertionExpander,
+ "QuoteReport": QuoteReport,
// NOTE(a.telyshev): Sub-template multiple arguments problem (example in BaseTestsGenerator):
// - https://stackoverflow.com/a/18276262
diff --git a/internal/testgen/gen_go_require.go b/internal/testgen/gen_go_require.go
new file mode 100644
index 00000000..bc74c697
--- /dev/null
+++ b/internal/testgen/gen_go_require.go
@@ -0,0 +1,503 @@
+package main
+
+import (
+ "text/template"
+
+ "github.com/Antonboom/testifylint/internal/checkers"
+)
+
+type GoRequireTestsGenerator struct{}
+
+func (GoRequireTestsGenerator) Checker() checkers.Checker {
+ return checkers.NewGoRequire()
+}
+
+func (g GoRequireTestsGenerator) TemplateData() any {
+ var (
+ name = g.Checker().Name()
+ requireReport = name + ": require must only be used in the goroutine running the test function%.s%.s"
+ assertFailNowReport = name + ": %s.%s must only be used in the goroutine running the test function"
+ fnReport = name + ": %s contains assertions that must only be used in the goroutine running the test function"
+ )
+
+ return struct {
+ CheckerName CheckerName
+ FnReport string
+ Assertions []Assertion
+ Requires []Assertion
+ }{
+ CheckerName: CheckerName(name),
+ FnReport: fnReport,
+ Assertions: []Assertion{
+ {Fn: "Fail", Argsf: `"boom!"`},
+ {Fn: "FailNow", Argsf: `"boom!"`, ReportMsgf: assertFailNowReport, ProposedFn: "FailNow"},
+ {Fn: "NoError", Argsf: "err"},
+ {Fn: "True", Argsf: "b"},
+ },
+ Requires: []Assertion{
+ {Fn: "Fail", Argsf: `"boom!"`, ReportMsgf: requireReport, ProposedFn: "Fail"},
+ {Fn: "FailNow", Argsf: `"boom!"`, ReportMsgf: requireReport, ProposedFn: "FailNow"},
+ {Fn: "NoError", Argsf: "err", ReportMsgf: requireReport, ProposedFn: "NoError"},
+ {Fn: "True", Argsf: "b", ReportMsgf: requireReport, ProposedFn: "True"},
+ },
+ }
+}
+
+func (GoRequireTestsGenerator) ErroredTemplate() Executor {
+ return template.Must(template.New("GoRequireTestsGenerator.ErroredTemplate").
+ Funcs(fm).
+ Parse(goRequireTestTmpl))
+}
+
+func (GoRequireTestsGenerator) GoldenTemplate() Executor {
+ // NOTE(a.telyshev): Usually this warning leads to full refactoring of test architecture.
+ return nil
+}
+
+const goRequireTestTmpl = header + `
+
+package {{ .CheckerName.AsPkgName }}
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+)
+
+{{ define "assertions" }}
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "{{ printf .FnReport "requireSomething" }}"
+
+ {{ range $ai, $assrn := $.Assertions }}
+ {{ NewAssertionExpander.Expand $assrn "assert" "t" nil }}
+ {{- end }}
+ {{ range $ai, $assrn := $.Requires }}
+ {{ NewAssertionExpander.Expand $assrn "require" "t" nil }}
+ {{- end }}
+{{- end }}
+
+{{ define "silent-assertions" }}
+ run()
+ assertSomething(t)
+ requireSomething(t)
+
+ {{ range $ai, $assrn := $.Assertions }}
+ {{ NewAssertionExpander.Expand $assrn.WithoutReport "assert" "t" nil }}
+ {{- end }}
+ {{ range $ai, $assrn := $.Requires }}
+ {{ NewAssertionExpander.Expand $assrn.WithoutReport "require" "t" nil }}
+ {{- end }}
+{{- end }}
+
+{{ define "assertions-short" }}
+ run()
+ assertSomething(t)
+ requireSomething(t) // want "{{ printf .FnReport "requireSomething" }}"
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Assertions 0) "assert" "t" nil }}
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Requires 0) "require" "t" nil }}
+{{- end }}
+
+{{ define "silent-assertions-short" }}
+ run()
+ assertSomething(t)
+ requireSomething(t)
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Assertions 0).WithoutReport "assert" "t" nil }}
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Requires 0).WithoutReport "require" "t" nil }}
+{{- end }}
+
+func {{ .CheckerName.AsTestName }}_Smoke(t *testing.T) {
+ var err error
+ var b bool
+
+ {{ template "silent-assertions" . }}
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ for i := 0; i < 2; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ {{ template "assertions" . }}
+
+ if assert.Error(t, err) {
+ {{- range $ai, $assrn := $.Assertions }}
+ {{- if eq $assrn.Fn "FailNow"}}
+ {{ NewAssertionExpander.Expand $assrn "assert" "t" nil }}
+ {{- end }}
+ {{- end }}
+ }
+ }(i)
+ }
+}
+
+func {{ .CheckerName.AsTestName }}(t *testing.T) {
+ defer func() {
+ {{- template "silent-assertions-short" . }}
+
+ func() {
+ {{- template "silent-assertions-short" . }}
+ }()
+ }()
+
+ defer func() {
+ go func() {
+ {{ template "assertions-short" . }}
+ }()
+ }()
+
+ defer run()
+ defer assertSomething(t)
+ defer requireSomething(t)
+
+ t.Cleanup(func() {
+ {{- template "silent-assertions-short" . }}
+
+ func() {
+ {{- template "silent-assertions-short" . }}
+ }()
+
+ go func() {
+ {{- template "assertions-short" . }}
+
+ func() {
+ func(t *testing.T) {
+ {{- template "assertions-short" . }}
+ }(t)
+ }()
+ }()
+ })
+
+ func() {
+ func() {
+ func() {
+ func() {
+ {{- template "silent-assertions-short" . }}
+ }()
+
+ func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ go func() {
+ {{- template "assertions-short" . }}
+ }()
+ })
+ }(t)
+
+ go func() {
+ {{- template "assertions-short" . }}
+ }()
+ }()
+ }()
+ }()
+
+ if false {
+ {{- template "silent-assertions-short" . }}
+ }
+
+ go func() {
+ {{- template "assertions-short" . }}
+
+ go func() {
+ {{- template "assertions-short" . }}
+
+ go func() {
+ {{- template "assertions-short" . }}
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+ })
+ {{ template "assertions-short" . }}
+ }()
+ {{ template "assertions-short" . }}
+ }()
+ {{ template "assertions-short" . }}
+ }()
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ go func() {
+ {{- template "assertions-short" . }}
+
+ go func () {
+ go genericHelper[*testing.T](t) // want {{ QuoteReport (printf .FnReport "genericHelper[*testing.T]") }}
+ go superGenericHelper[*testing.T, int](t) // want {{ QuoteReport (printf .FnReport "superGenericHelper[*testing.T, int]") }}
+ }()
+ }()
+ {{ template "silent-assertions-short" . }}
+ })
+ {{ template "silent-assertions-short" . }}
+ })
+ {{ template "silent-assertions-short" . }}
+ })
+
+ go func() {
+ {{- template "assertions-short" . }}
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ go func(t *testing.T) {
+ {{- template "assertions-short" . }}
+
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+ })
+
+ if true {
+ {{- template "assertions-short" . }}
+ }
+ }(t)
+ {{ template "silent-assertions-short" . }}
+ })
+ {{ template "assertions-short" . }}
+ }()
+
+ cases := []struct{}{}
+ for _, tt := range cases {
+ tt := tt
+ t.Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+
+ go func() {
+ {{- template "assertions-short" . }}
+ _ = tt
+ }()
+ })
+ }
+
+ run()
+ assertSomething(t)
+ requireSomething(t)
+
+ go run()
+ go assertSomething(t)
+ go requireSomething(t) // want "{{ printf .FnReport "requireSomething" }}"
+
+ go func() {
+ run()
+ }()
+ go func() {
+ assertSomething(t)
+ }()
+ go func() {
+ requireSomething(t) // want "{{ printf .FnReport "requireSomething" }}"
+ }()
+
+ var err error
+ var b bool
+ {{ range $ai, $assrn := $.Assertions }}
+ go {{ NewAssertionExpander.NotFmtSingleMode.Expand $assrn "assert" "t" nil }}
+ {{- end }}
+ {{ range $ai, $assrn := $.Requires }}
+ go {{ NewAssertionExpander.NotFmtSingleMode.Expand $assrn "require" "t" nil }}
+ {{- end }}
+
+ go requireSomethingInGo(t)
+ go func() {
+ requireSomethingInGo(t)
+ }()
+
+ go proxy(t) // want "{{ printf .FnReport "proxy" }}"
+ go func() {
+ proxy(t) // want "{{ printf .FnReport "proxy" }}"
+ }()
+
+ t.Run("", assertSomething)
+ t.Run("", requireSomething)
+
+ go t.Run("", assertSomething)
+ go t.Run("", requireSomething)
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+ for i := 1; i <= 2; i++ {
+ i := i
+ go t.Run(fmt.Sprintf("uncaught-%d", i), func(t *testing.T) {
+ defer wg.Done()
+
+ {{- template "silent-assertions-short" . }}
+ })
+ }
+ wg.Wait()
+
+ genericHelper(t)
+ genericHelper[*testing.T](t)
+ superGenericHelper[*testing.T, int](t)
+
+ go func() {
+ genericHelper(t) // want "{{ printf .FnReport "genericHelper" }}"
+ genericHelper[*testing.T](t) // want {{ QuoteReport (printf .FnReport "genericHelper[*testing.T]") }}
+ superGenericHelper[*testing.T, int](t) // want {{ QuoteReport (printf .FnReport "superGenericHelper[*testing.T, int]") }}
+ }()
+}
+
+{{ define "suite-assertions-short" }}
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T()) // want "{{ printf .FnReport "requireSomething" }}"
+ s.suiteHelper() // want "{{ printf .FnReport "s.suiteHelper" }}"
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Assertions 0) "s" "" nil }}
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Requires 0) "s.Require()" "" nil }}
+{{- end }}
+
+{{ define "suite-silent-assertions-short" }}
+ run()
+ assertSomething(s.T())
+ requireSomething(s.T())
+ s.suiteHelper()
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Assertions 0).WithoutReport "s" "" nil }}
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Requires 0).WithoutReport "s.Require()" "" nil }}
+{{- end }}
+
+{{ $suiteName := .CheckerName.AsSuiteName }}
+
+type {{ $suiteName }} struct {
+ suite.Suite
+}
+
+func Test{{ $suiteName }}(t *testing.T) {
+ suite.Run(t, new({{ $suiteName }}))
+}
+
+func (s *{{ $suiteName }}) TestAll() {
+ {{- template "suite-silent-assertions-short" . }}
+
+ go func() {
+ {{- template "suite-assertions-short" . }}
+
+ s.Run("", func() {
+ {{- template "suite-silent-assertions-short" . }}
+
+ go func(t *testing.T) {
+ {{- template "assertions-short" . }}
+
+ s.T().Run("", func(t *testing.T) {
+ {{- template "silent-assertions-short" . }}
+ })
+
+ if true {
+ {{- template "assertions-short" . }}
+ }
+ }(s.T())
+ {{ template "suite-silent-assertions-short" . }}
+ })
+ {{ template "suite-assertions-short" . }}
+ }()
+
+ {{ template "suite-silent-assertions-short" . }}
+
+ go run()
+ go assertSomething(s.T())
+ go requireSomething(s.T()) // want "{{ printf .FnReport "requireSomething" }}"
+ go s.suiteHelper() // want "{{ printf .FnReport "s.suiteHelper" }}"
+
+ s.Run("", s.suiteHelper)
+ s.T().Run("", func(t *testing.T) { requireSomething (t) })
+
+ go s.T().Run("", requireSomething)
+ go s.Run("", s.suiteHelper)
+}
+
+func (s *{{ $suiteName }}) TestAsertFailNow() {
+ var err error
+ var b bool
+
+ {{ range $ai, $assrn := $.Assertions }}
+ {{ NewAssertionExpander.Expand $assrn.WithoutReport "s" "" nil }}
+ {{ NewAssertionExpander.Expand $assrn.WithoutReport "s.Assert()" "" nil }}
+ {{- end }}
+
+ go func() {
+ {{- range $ai, $assrn := $.Assertions }}
+ {{ NewAssertionExpander.Expand $assrn "s" "" nil }}
+ {{ NewAssertionExpander.Expand $assrn "s.Assert()" "" nil }}
+ {{- end }}
+ }()
+}
+
+func (s *{{ $suiteName }}) suiteHelper() {
+ s.T().Helper()
+
+ {{ template "suite-silent-assertions-short" . }}
+
+ go func() {
+ {{- template "suite-assertions-short" . }}
+ }()
+
+ {{ template "suite-silent-assertions-short" . }}
+}
+
+func run() {}
+
+func assertSomething(t *testing.T) {
+ t.Helper()
+
+ assert.NoError(t, nil)
+ assert.Error(t, nil)
+ assert.True(t, false)
+}
+
+func proxy(t *testing.T) {
+ requireSomething(t)
+}
+
+func requireSomething(t *testing.T) {
+ t.Helper()
+
+ {{ template "silent-assertions-short" . }}
+}
+
+func requireSomethingInGo(t *testing.T) {
+ go func() {
+ {{- template "assertions-short" . }}
+ }()
+}
+
+func helperNotUsedInGoroutine(t *testing.T) {
+ t.Helper()
+
+ {{ template "silent-assertions-short" . }}
+
+ go func() {
+ {{- template "assertions-short" . }}
+ }()
+}
+
+type testingT interface {
+ require.TestingT
+}
+
+func genericHelper[T testingT](t T) {
+ run()
+
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Assertions 0).WithoutReport "assert" "t" nil }}
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Requires 0).WithoutReport "require" "t" nil }}
+
+ go func() {
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Assertions 0) "assert" "t" nil }}
+ {{ NewAssertionExpander.NotFmtSingleMode.Expand (index $.Requires 0) "require" "t" nil }}
+ }()
+}
+
+func superGenericHelper[T testingT, T2 any](t T) T2 {
+ require.Fail(t, "boom!")
+ var zero T2
+ return zero
+}
+`
diff --git a/internal/testgen/gen_suite_thelper.go b/internal/testgen/gen_suite_thelper.go
index e6bd1cea..a49bbe2a 100644
--- a/internal/testgen/gen_suite_thelper.go
+++ b/internal/testgen/gen_suite_thelper.go
@@ -16,8 +16,8 @@ func (SuiteTHelperTestsGenerator) Checker() checkers.Checker {
func (g SuiteTHelperTestsGenerator) TemplateData() any {
var (
name = g.Checker().Name()
- report1 = quoteReport(name + ": suite helper method must start with suite.T().Helper()")
- report2 = quoteReport(name + ": suite helper method must start with s.T().Helper()")
+ report1 = QuoteReport(name + ": suite helper method must start with suite.T().Helper()")
+ report2 = QuoteReport(name + ": suite helper method must start with s.T().Helper()")
)
return struct {
diff --git a/internal/testgen/main.go b/internal/testgen/main.go
index 92b5af45..444e2a1d 100644
--- a/internal/testgen/main.go
+++ b/internal/testgen/main.go
@@ -29,6 +29,7 @@ var checkerTestsGenerators = []CheckerTestsGenerator{
ErrorIsAsTestsGenerator{},
ExpectedActualTestsGenerator{},
FloatCompareTestsGenerator{},
+ GoRequireTestsGenerator{},
LenTestsGenerator{},
NilCompareTestsGenerator{},
RequireErrorTestsGenerator{},