Speed up MySQL tests (#585)

Improves MySQL test time (on my 2020 MBP) to ~18s from ~125s.

- Use separate databases for each test to allow parallelization.
- Run migrations only once at beginning of tests and then reload
  generated schema.
- Add `--innodb-file-per-table=OFF` for ~20% additional speedup.
This commit is contained in:
Zach Wasserman
2021-04-03 11:42:27 -07:00
committed by GitHub
parent 37d519a4fb
commit 9f71fcf440
7 changed files with 100 additions and 46 deletions
+1 -5
View File
@@ -42,10 +42,6 @@ ifdef CIRCLE_TAG
DOCKER_IMAGE_TAG = ${CIRCLE_TAG}
endif
ifndef MYSQL_PORT_3306_TCP_ADDR
MYSQL_PORT_3306_TCP_ADDR = 127.0.0.1
endif
KIT_VERSION = "\
-X github.com/kolide/kit/version.appName=${APP_NAME} \
-X github.com/kolide/kit/version.version=${VERSION} \
@@ -123,7 +119,7 @@ lint-go:
lint: lint-go lint-js
test-go:
go test -tags full ./...
go test -tags full -parallel 8 ./...
analyze-go:
go test -tags full -race -cover ./...
+2 -1
View File
@@ -16,7 +16,8 @@ services:
mysql_test:
image: mysql:5.7
command: mysqld --datadir=/tmpfs --slow_query_log=1 --log_output=TABLE --log-queries-not-using-indexes --event-scheduler=ON
# innodb-file-per-table=OFF gives ~20% speedup for test runs.
command: mysqld --datadir=/tmpfs --slow_query_log=1 --log_output=TABLE --log-queries-not-using-indexes --event-scheduler=ON --innodb-file-per-table=OFF
tmpfs: /tmpfs
environment: *mysql-default-environment
ports:
-1
View File
@@ -34,7 +34,6 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
testCreateUser,
testSaveUser,
testUserByID,
testPasswordResetRequests,
testSearchHosts,
testSearchHostsLimit,
testSearchLabels,
+96 -24
View File
@@ -1,7 +1,10 @@
package datastore
import (
"database/sql"
"fmt"
"os"
"os/exec"
"testing"
"github.com/WatchBeam/clock"
@@ -13,46 +16,115 @@ import (
"github.com/stretchr/testify/require"
)
func setupMySQL(t *testing.T) (ds *mysql.Datastore, teardown func()) {
const (
schemaDbName = "schemadb"
dumpfile = "dump.sql"
testUsername = "root"
testPassword = "toor"
testAddress = "localhost:3307"
)
func connectMySQL(t *testing.T, testName string) *mysql.Datastore {
config := config.MysqlConfig{
Username: "fleet",
Password: "insecure",
Database: "fleet",
// When using docker-compose.yml for local testing
Address: "127.0.0.1:3307",
}
// When using Docker link on CI
if h, ok := os.LookupEnv("MYSQL_PORT_3306_TCP_ADDR"); ok {
config.Address = h + ":3306"
Username: testUsername,
Password: testPassword,
Database: testName,
Address: testAddress,
}
// Create datastore client
ds, err := mysql.New(config, clock.NewMockClock(), mysql.Logger(log.NewNopLogger()), mysql.LimitAttempts(1))
require.Nil(t, err)
teardown = func() {
ds.Close()
return ds
}
// initializeSchema initializes a database schema using the normal Fleet
// migrations, then outputs the schema with mysqldump within the MySQL Docker
// container.
func initializeSchema(t *testing.T) {
// Create the database (must use raw MySQL client to do this)
db, err := sql.Open(
"mysql",
fmt.Sprintf("%s:%s@tcp(%s)/?multiStatements=true", testUsername, testPassword, testAddress),
)
require.NoError(t, err)
defer db.Close()
_, err = db.Exec("DROP DATABASE IF EXISTS schemadb; CREATE DATABASE schemadb;")
require.NoError(t, err)
// Create a datastore client in order to run migrations as usual
config := config.MysqlConfig{
Username: testUsername,
Password: testPassword,
Address: testAddress,
Database: schemaDbName,
}
ds, err := mysql.New(config, clock.NewMockClock(), mysql.Logger(log.NewNopLogger()), mysql.LimitAttempts(1))
require.Nil(t, err)
defer ds.Close()
require.Nil(t, ds.MigrateTables())
// Dump schema to dumpfile
if out, err := exec.Command(
"docker-compose", "exec", "-T", "mysql_test",
// Command run inside container
"mysqldump",
"-u"+testUsername, "-p"+testPassword,
"schemadb",
"--compact", "--skip-comments",
"--result-file="+dumpfile,
).CombinedOutput(); err != nil {
t.Error(err)
t.Error(string(out))
t.FailNow()
}
}
// initializeDatabase loads the dumped schema into a newly created database in
// MySQL. This is much faster than running the full set of migrations on each
// test.
func initializeDatabase(t *testing.T, dbName string) {
// Load schema from dumpfile
if out, err := exec.Command(
"docker-compose", "exec", "-T", "mysql_test",
// Command run inside container
"mysql",
"-u"+testUsername, "-p"+testPassword,
"-e",
fmt.Sprintf(
"DROP DATABASE IF EXISTS %s; CREATE DATABASE %s; USE %s; SET FOREIGN_KEY_CHECKS=0; SOURCE %s;",
dbName, dbName, dbName, dumpfile,
),
).CombinedOutput(); err != nil {
t.Error(err)
t.Error(string(out))
t.FailNow()
}
return ds, teardown
}
func TestMySQL(t *testing.T) {
if _, ok := os.LookupEnv("MYSQL_TEST"); !ok {
t.SkipNow()
t.Skip("MySQL tests are disabled")
}
ds, teardown := setupMySQL(t)
defer teardown()
// get rid of database if it is hanging around
err := ds.Drop()
require.Nil(t, err)
// Initialize the schema once for the entire test run.
initializeSchema(t)
for _, f := range testFunctions {
// Copy test function to a variable scoped within the loop so we don't
// run the same test a bunch of times.
testFunc := f
t.Run(test.FunctionName(testFunc), func(t *testing.T) {
t.Parallel()
t.Run(test.FunctionName(f), func(t *testing.T) {
defer func() { require.Nil(t, ds.Drop()) }()
require.Nil(t, ds.MigrateTables())
f(t, ds)
// Create a new database and load the schema for each test
initializeDatabase(t, test.FunctionName(testFunc))
ds := connectMySQL(t, test.FunctionName(testFunc))
defer ds.Close()
testFunc(t, ds)
})
}
@@ -1,7 +1,6 @@
package live_query
import (
"fmt"
"os"
"testing"
@@ -33,10 +32,6 @@ func setupRedisLiveQuery(t *testing.T) (store *redisLiveQuery, teardown func())
useTLS = false
)
if a, ok := os.LookupEnv("REDIS_PORT_6379_TCP_ADDR"); ok {
addr = fmt.Sprintf("%s:6379", a)
}
store = NewRedisLiveQuery(pubsub.NewRedisPool(addr, password, database, useTLS))
_, err := store.pool.Get().Do("PING")
-5
View File
@@ -2,7 +2,6 @@ package pubsub
import (
"context"
"fmt"
"os"
"sync"
"testing"
@@ -68,10 +67,6 @@ func setupRedis(t *testing.T) (store *redisQueryResults, teardown func()) {
useTLS = false
)
if a, ok := os.LookupEnv("REDIS_PORT_6379_TCP_ADDR"); ok {
addr = fmt.Sprintf("%s:6379", a)
}
store = NewRedisQueryResults(NewRedisPool(addr, password, database, useTLS))
_, err := store.pool.Get().Do("PING")
+1 -5
View File
@@ -1,13 +1,12 @@
package sso
import (
"fmt"
"os"
"testing"
"time"
"github.com/gomodule/redigo/redis"
"github.com/fleetdm/fleet/server/pubsub"
"github.com/gomodule/redigo/redis"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -20,9 +19,6 @@ func newPool(t *testing.T) *redis.Pool {
database = 0
useTLS = false
)
if a, ok := os.LookupEnv("REDIS_PORT_6379_TCP_ADDR"); ok {
addr = fmt.Sprintf("%s:6379", a)
}
p := pubsub.NewRedisPool(addr, password, database, useTLS)
_, err := p.Get().Do("PING")