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{},