blob: 0f65c749fe283ee70c5bc46cfbd2fab57dc0c443 [file]
package testutil
import (
"strings"
"github.com/jmoiron/sqlx"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/go/database"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/util"
)
// Connection string to the local MySQL database for testing.
const (
// String to open a local database for testing. The string formatting
// parameters are: username, password, database.
MYSQL_DB_OPEN = "%s:%s@tcp(localhost:3306)/%s?parseTime=true"
// Name of the MySQL lock
SQL_LOCK = "mysql_testlock"
// Name of the shared test database.
TEST_DB_HOST = "localhost"
TEST_DB_PORT = 3306
TEST_DB_NAME = "sk_testing"
// Names of test users. These users should have no password and be
// limited to accessing the sk_testing database.
USER_ROOT = "test_root"
USER_RW = "test_rw"
// Empty password for testing.
TEST_PASSWORD = ""
)
// LocalTestDatabaseConfig returns a DatabaseConfig appropriate for local
// testing.
func LocalTestDatabaseConfig(m []database.MigrationStep) *database.DatabaseConfig {
return &database.DatabaseConfig{
User: USER_RW,
Host: TEST_DB_HOST,
Port: TEST_DB_PORT,
Name: TEST_DB_NAME,
MigrationSteps: m,
}
}
// LocalTestRootDatabaseConfig returns a DatabaseConfig appropriate for local
// testing, with root access.
func LocalTestRootDatabaseConfig(m []database.MigrationStep) *database.DatabaseConfig {
return &database.DatabaseConfig{
User: USER_ROOT,
Host: TEST_DB_HOST,
Port: TEST_DB_PORT,
Name: TEST_DB_NAME,
MigrationSteps: m,
}
}
// Creates an MySQL test database and runs migration tests against it using the
// given migration steps. See Get for required credentials.
// The test assumes that the database is empty and that the readwrite user is
// not allowed to create/drop/alter tables.
func MySQLVersioningTests(t testutils.TestingT, dbName string, migrationSteps []database.MigrationStep) {
// OpenDB as root user and remove all tables.
rootConf := LocalTestRootDatabaseConfig(migrationSteps)
lockDB := GetMySQlLock(t, rootConf)
defer lockDB.Close(t)
rootVdb, err := rootConf.NewVersionedDB()
assert.NoError(t, err)
ClearMySQLTables(t, rootVdb)
assert.NoError(t, rootVdb.Close())
// Configuration for the readwrite user without DDL privileges.
readWriteConf := LocalTestDatabaseConfig(migrationSteps)
// Open DB as readwrite user and make sure it fails because of a missing
// version table.
// Note: This requires the database to be empty.
_, err = readWriteConf.NewVersionedDB()
assert.NotNil(t, err)
rootVdb, err = rootConf.NewVersionedDB()
assert.NoError(t, err)
testDBVersioning(t, rootVdb)
// Make sure it doesn't fail for readwrite user after the migration
_, err = readWriteConf.NewVersionedDB()
assert.NoError(t, err)
// Downgrade database, removing most if not all tables.
downgradeDB(t, rootVdb)
ClearMySQLTables(t, rootVdb)
}
type LockDB struct {
DB *sqlx.DB
}
// Get a lock from MySQL to serialize DB tests.
func GetMySQlLock(t testutils.TestingT, conf *database.DatabaseConfig) *LockDB {
db, err := sqlx.Open("mysql", conf.MySQLString())
assert.NoError(t, err)
for {
var result int
assert.NoError(t, db.Get(&result, "SELECT GET_LOCK(?,5)", SQL_LOCK))
// We obtained the lock. If not try again.
if result == 1 {
return &LockDB{db}
}
}
}
// Release the MySQL lock.
func (l *LockDB) Close(t testutils.TestingT) {
var result int
assert.NoError(t, l.DB.Get(&result, "SELECT RELEASE_LOCK(?)", SQL_LOCK))
assert.Equal(t, result, 1)
assert.NoError(t, l.DB.Close())
}
// Remove all tables from the database.
func ClearMySQLTables(t testutils.TestingT, vdb *database.VersionedDB) {
stmt := `SHOW TABLES`
rows, err := vdb.DB.Query(stmt)
assert.NoError(t, err)
defer util.Close(rows)
names := make([]string, 0)
var tableName string
for rows.Next() {
assert.NoError(t, rows.Scan(&tableName))
names = append(names, tableName)
}
if len(names) > 0 {
stmt = "DROP TABLE " + strings.Join(names, ",")
_, err = vdb.DB.Exec(stmt)
assert.NoError(t, err)
}
}
// MySQLTestDatabase is a convenience struct for using a test database which
// starts in a clean state.
type MySQLTestDatabase struct {
lockDB *LockDB
rootVdb *database.VersionedDB
t testutils.TestingT
}
// SetupMySQLTestDatabase returns a MySQLTestDatabase in a clean state. It must
// be closed after use.
//
// Example usage:
//
// db := SetupMySQLTestDatabase(t, migrationSteps)
// defer util.Close(db)
// ... Tests here ...
func SetupMySQLTestDatabase(t testutils.TestingT, migrationSteps []database.MigrationStep) *MySQLTestDatabase {
conf := LocalTestRootDatabaseConfig(migrationSteps)
lock := GetMySQlLock(t, conf)
rootVdb, err := conf.NewVersionedDB()
assert.NoError(t, err)
ClearMySQLTables(t, rootVdb)
if err := rootVdb.Close(); err != nil {
t.Fatal(err)
}
rootVdb, err = conf.NewVersionedDB()
assert.NoError(t, err)
if err := rootVdb.Migrate(rootVdb.MaxDBVersion()); err != nil {
t.Fatal(err)
}
return &MySQLTestDatabase{lock, rootVdb, t}
}
func (d *MySQLTestDatabase) Close(t testutils.TestingT) {
assert.NoError(t, d.rootVdb.Migrate(0))
assert.NoError(t, d.rootVdb.Close())
d.lockDB.Close(t)
}
// Test wether the migration steps execute correctly.
func testDBVersioning(t testutils.TestingT, vdb *database.VersionedDB) {
// get the DB version
dbVersion, err := vdb.DBVersion()
assert.NoError(t, err)
maxVersion := vdb.MaxDBVersion()
downgradeDB(t, vdb)
// upgrade the the latest version
err = vdb.Migrate(maxVersion)
assert.NoError(t, err)
dbVersion, err = vdb.DBVersion()
assert.NoError(t, err)
assert.Equal(t, maxVersion, dbVersion)
}
func downgradeDB(t testutils.TestingT, vdb *database.VersionedDB) {
// downgrade to 0
err := vdb.Migrate(0)
assert.NoError(t, err)
dbVersion, err := vdb.DBVersion()
assert.NoError(t, err)
assert.Equal(t, 0, dbVersion)
}