-
Notifications
You must be signed in to change notification settings - Fork 382
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
Improve performance of the Go test suite #2060
Conversation
Codecov Report
@@ Coverage Diff @@
## main #2060 +/- ##
==========================================
+ Coverage 48.03% 48.15% +0.11%
==========================================
Files 224 224
Lines 21266 21271 +5
==========================================
+ Hits 10216 10243 +27
+ Misses 9834 9809 -25
- Partials 1216 1219 +3
Continue to review full report at Codecov.
|
1b81577
to
e44b2b1
Compare
e44b2b1
to
77a9725
Compare
@@ -10,10 +10,25 @@ import ( | |||
"github.com/stretchr/testify/require" | |||
) | |||
|
|||
func TestActivityUsernameChange(t *testing.T) { | |||
func TestActivity(t *testing.T) { |
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.
@chiiph as discussed in #1821 , this is how I see "test suites" being implemented in standard Go tests/sub-tests:
- the "suite" is the top-level test function (for mysql I created one by test file, with the name being the name of the file/entity).
- the suite setup/teardown is any code executed and deferred (respectively) before the sub-tests (can also be
t.Cleanup
instead of defer, as is the case inCreateMySQLDS
). - I used a table-driven approach to run the suite's tests, as it tends to minimize code repetition.
- Per-test setup/teardown is just the code inside the
t.Run
closure that runs before calling the test function. - If some test has specific setup/teardown, of course it can simply be done in that test's function.
In this case I did not need any shared state so I didn't use a struct to hold the suite's tests, but it is a possibility (I could've used a struct and store e.g. the datastore there so its methods would've accessed it there instead of as an argument).
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.
Yeah, makes sense and it's simple enough.
@@ -1584,7 +1558,9 @@ func TestSaveTonsOfUsers(t *testing.T) { | |||
errCh <- err | |||
return | |||
} | |||
atomic.AddInt32(&count1, 1) | |||
if atomic.AddInt32(&count1, 1) >= 100 { |
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.
@chiiph let me know if this is still good enough for the goal of the test (which I believe was related to locking when adding many users concurrently). I tried to find a balance between the batch size and the test duration.
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.
It's probably fine, the locking happened pretty much right away.
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.
Looks great! Left a few minor comments.
@@ -41,7 +41,7 @@ jobs: | |||
|
|||
- name: Run Go Tests | |||
run: | | |||
MYSQL_TEST=1 make test-go | |||
NETWORK_TEST=1 REDIS_TEST=1 MYSQL_TEST=1 make test-go |
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.
Not a huge fan of these, but I get it.
We did remove REDIS_TEST
, so we should remove it from here. I would also like to remove MYSQL_TEST
entirely and just keep working on making things better.
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.
Yeah, I agree with you, it's not very discoverable, but as mentioned in the description the approach I would've preferred was quite an undertaking, especially without fully understanding that part of the code. I believe the REDIS_TEST
env var is still used in the server/sso
tests.
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.
Oh... darn, missed that :( Allllrighty then
@@ -10,10 +10,25 @@ import ( | |||
"github.com/stretchr/testify/require" | |||
) | |||
|
|||
func TestActivityUsernameChange(t *testing.T) { | |||
func TestActivity(t *testing.T) { |
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.
Yeah, makes sense and it's simple enough.
@@ -1584,7 +1558,9 @@ func TestSaveTonsOfUsers(t *testing.T) { | |||
errCh <- err | |||
return | |||
} | |||
atomic.AddInt32(&count1, 1) | |||
if atomic.AddInt32(&count1, 1) >= 100 { |
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.
It's probably fine, the locking happened pretty much right away.
} | ||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
defer TruncateTables(t, ds, "teams", "invites", "invite_teams") |
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.
Would truncating all the tables be much slower?
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.
Probably not, though some are seeded by the sql schema file and should be left alone. It's usually quite clear when one is missing (as a subsequent test will fail with a duplicate error or wrong number of elements, etc.), but yeah I don't think it'd be significantly slower. I'll try it out.
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.
Updated the PR, it seems roughly as fast (10-12 seconds on my machine).
@@ -233,7 +233,9 @@ func createMySQLDSWithOptions(t *testing.T, opts *DatastoreTestOptions) *Datasto | |||
strings.TrimPrefix(details.Name(), "github.com/fleetdm/fleet/v4/"), "/", "_", | |||
) | |||
cleanName = strings.ReplaceAll(cleanName, ".", "_") | |||
return initializeDatabase(t, cleanName, opts) | |||
ds := initializeDatabase(t, cleanName, opts) | |||
t.Cleanup(func() { ds.Close() }) |
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.
💪
Closes #1805 .
This PR addresses two categories of slow tests: network requests and creating temporary mysql databases.
Network Requests
I introduced the
NETWORK_TEST=1
environment variable, similar to how we haveMYSQL_TEST
andREDIS_TEST
to enable those specific tests. I did enable them on CI as we do want to run them, but it is easier to skip when running tests locally (i.e.make test-go
does not run them automatically).It's not the ideal solution, I would've liked to mock them completely by starting a test server and mocking responses, but after a closer look this seems quite complex to setup as the data returned has a specific format (e.g. for vulnerabilities) and hashes that must match and the requests themselves are sometimes made quite deep in the code (even in 3rd-party code).
Temporary MySQL Databases
I refactored the tests in
server/datastore/mysql
to use sub-tests and drop the number of temporary databases created from 130 to 24. I opted to keep a top-level test (that creates the temp DB) for each "entity" (or .go file) to make it easier to identify the cleanup required and so that the test setup applies to the tests in the same file, a bit easier to reason about and to read the tests that way, I think.The speedup is quite interesting too, comparing
MYSQL_TEST=1 go test ./server/datastore/mysql/ -count=1
locally on this branch vsmain
, I get ~10s vs ~30s (though part of it is also in reducing the number of records created in "load" tests to verify mysql locking).Also, the race detector (
MYSQL_TEST=1 go test ./server/datastore/mysql/ -count=1 -race
) now passes, there was a data race previously (in the test code, so nothing critical).Checklist for submitter
If some of the following don't apply, please write a short explanation why.
Changes file added (if needed)Documented any API changesAdded tests for all functionalityManual QA for all functionality