-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
suite: Experimental - Fuzz test support #1142
base: master
Are you sure you want to change the base?
Conversation
This runs the fuzz tests in fuzz test mode, but doesn't fail when any of the suite assertion methods are used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution! 🙂
I've put down some questions to better understand the decisions you've made.
suite.Assertions = assert.New(f) | ||
suite.require = require.New(f) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might be problematic. If we called suite.SetT(t)
then called suite.SetF()
, would that not lose the "T" we set in SetT(t)
?
If so, it seems like that would be the wrong behaviour? Should we not find a way to add an "F" or "T" if the relevant Assertions and requires have already been instantiated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. The current change assumes that you will not have a situation like that. From my current very limited understanding of both testify and go fuzz tests, for having a suite of fuzz tests, your suite's entrypoint "test function" will accept a testing.F
type parameter. The fuzz target has a testing.T
context, but that's not relevant i think to testify
as that gets invoked by the go test runner.
if suite.T() != nil { | ||
tT = suite.T() | ||
} | ||
if suite.F() != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we checking for the existence of suite.F
and suite.T
when previously we weren't?
Is it possible that the existence of both of those should be guaranteed before the code reaches this point (i.e. when suite is first instantiated)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this stage, the current suite under test could either be a fuzz test suite (either in normal/fuzz mode) or a non-fuzz test suite. Hence, depending on which mode we are in, initialize suite.require
appropriately. Hence, the current panic()
where i assert that at least one of them is set.
Again, this assumes that during a single run, for a suite, it can only have testing.T
or testing.F
set.
if tT == nil { | ||
panic("Neither T nor F was non-nil") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we want to test for this? Is it not possible for a suite to have both non-nil T
and F
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, same assumption as above.
} | ||
return suite.Assertions | ||
} | ||
|
||
func failOnPanic(t *testing.T) { | ||
func failOnPanic(t testing.TB) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the logic for expanding the interface to include benchmarks and removing the pointer here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we run a fuzz test we have a testing.F
injected into the test binary instead of testing.T
.
The reason for replacing *testing.T
by testing.TB
is so that failOnPanic()
can be called with a testing.F
value as well (testing.TB
interface is satisfied by T
, F
and B
source)
@@ -80,10 +112,17 @@ func (suite *Suite) Run(name string, subtest func()) bool { | |||
|
|||
// Run takes a testing suite and runs all of the tests attached | |||
// to it. | |||
func Run(t *testing.T, suite TestingSuite) { | |||
func Run(t testing.TB, suite TestingSuite) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question as line 92
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully the above also explains this.
@@ -193,12 +232,13 @@ func runTests(t testing.TB, tests []testing.InternalTest) { | |||
|
|||
r, ok := t.(runner) | |||
if !ok { // backwards compatibility with Go 1.6 and below |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't officially support Go 1.6 anymore. Can we remove this check altogether or at least remove the old backwards compatibility change and update this to reflect 1.18 and fuzzing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a good idea. Should I create a separate change to remove this and rebase on top it?
@@ -193,12 +232,13 @@ func runTests(t testing.TB, tests []testing.InternalTest) { | |||
|
|||
r, ok := t.(runner) | |||
if !ok { // backwards compatibility with Go 1.6 and below | |||
// t.F doesn't have Run() so Fuzz tests also | |||
// take this branch | |||
if !testing.RunTests(allTestsFilter, tests) { | |||
t.Fail() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What i am observing is this which explains the behavior I am seeing (which may have been clear to you folks, but i am seeing why now):
- When the fuzz test is run in "unit test" mode, the function call
RunTests()
returns with afalse
value and hence the test fails (considering the sample test in the PR) - When the fuzz test is run in fuzz mode, this function call,
RunTests()
returnstrue
(considering the sample test in the PR)
When I uncomment the t.Fail()
in the test, the test fails in fuzz mode, however, the return value of RunTests()
is still true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I think i have found the issue. The problem seems to be that when we call the suite.EqualError()
method, it reports any failures using the testing context's (testing.T
value) methods. However, the problem is since we are running a Fuzz test, we only have a testing.F
injected via go test
. The testing.T
value is created and injected into the fuzz target as part of the test run. So, we need to make this testing.T
created as part of the test run available to testify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to next experiment with updating the test discovery and running logic using https://pkg.go.dev/testing@go1.18beta2#InternalFuzzTarget for the fuzz tests. i think that may be required and certainly feels correct.
I don't think that's going to help. That is an internal type which refers to the "internal" fuzz target function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I think i have found the issue. The problem seems to be that when we call the
suite.EqualError()
method, it reports any failures using the testing context's (testing.T
value) methods. However, the problem is since we are running a Fuzz test, we only have atesting.F
injected viago test
. Thetesting.T
value is created and injected into the fuzz target as part of the test run. So, we need to make thistesting.T
created as part of the test run available to testify.
Hence, when i update my test as follows to make a call to the SetT()
method, it fails as expected in fuzz mode as well:
type ExampleFuzzSuite struct {
suite.Suite
}
func (suite *ExampleFuzzSuite) FuzzAFunc() {
f := suite.F()
f.Add("foo")
f.Fuzz(func(t *testing.T, aStr string) {
fmt.Println("I am in Fuzz target")
// this fails the test run when run in fuzz mode
//t.Fail()
// this doesn't fail the test when run in fuzz mode if i comment
// out the next line: https://github.com/stretchr/testify/pull/1142/files#r821269235
suite.SetT(t)
suite.EqualError(nil, "an error")
})
}
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
func FuzzSuite(f *testing.F) {
suite.Run(f, new(ExampleFuzzSuite))
}
Thanks for the review so far. I have replied to the comments. I suspect we may have run into an issue with how we handle assertions and report failures using |
How's it going? Go supports fuzzing in its standard toolchain beginning in Go 1.18. Native Go fuzz tests are supported by OSS-Fuzz. |
Summary
Add support for fuzz tests as introduced in Go 1.18 (still beta) and documented here. Some additional implementation details are available in this blog post.
Changes
TBD
Motivation
Support fuzz tests in testify
Example usage
Example test:
With this modification to
testify
, the above test run in fuzz mode looks as follows:I expected the test to fail since i have
suite.EqualError(nil, "an error')
.In "unit test" mode, the test fails: