Chore: Update test database initialization (#81673)

* streamline initialization of test databases, support on-disk sqlite test db

* clean up test databases

* introduce testsuite helper

* use testsuite everywhere we use a test db

* update documentation

* improve error handling

* disable entity integration test until we can figure out locking error
This commit is contained in:
Dan Cech
2024-02-09 09:35:39 -05:00
committed by GitHub
parent de4acb27ce
commit 790e1feb93
104 changed files with 801 additions and 189 deletions
@@ -2,7 +2,6 @@ package test
import (
"fmt"
"os"
"testing"
"time"
@@ -105,37 +104,6 @@ func convertToRawPermissions(permissions []accesscontrol.Permission) []rawPermis
return raw
}
func getDBType() string {
dbType := migrator.SQLite
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
dbType = db
}
return dbType
}
func getTestDB(t *testing.T, dbType string) sqlutil.TestDB {
switch dbType {
case "mysql":
return sqlutil.MySQLTestDB()
case "postgres":
return sqlutil.PostgresTestDB()
default:
f, err := os.CreateTemp(".", "grafana-test-db-")
require.NoError(t, err)
t.Cleanup(func() {
err := os.Remove(f.Name())
require.NoError(t, err)
})
return sqlutil.TestDB{
DriverName: "sqlite3",
ConnStr: f.Name(),
}
}
}
func TestMigrations(t *testing.T) {
// Run initial migration to have a working DB
x := setupTestDB(t)
@@ -253,12 +221,21 @@ func TestMigrations(t *testing.T) {
func setupTestDB(t *testing.T) *xorm.Engine {
t.Helper()
dbType := getDBType()
testDB := getTestDB(t, dbType)
dbType := sqlutil.GetTestDBType()
testDB, err := sqlutil.GetTestDB(dbType)
require.NoError(t, err)
t.Cleanup(testDB.Cleanup)
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := x.Close(); err != nil {
fmt.Printf("failed to close xorm engine: %v", err)
}
})
err = migrator.NewDialect(x.DriverName()).CleanDB(x)
require.NoError(t, err)
@@ -3,7 +3,6 @@ package migrations
import (
"errors"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
@@ -21,13 +20,23 @@ import (
)
func TestMigrations(t *testing.T) {
testDB := sqlutil.SQLite3TestDB()
testDB, err := sqlutil.GetTestDB(SQLite)
require.NoError(t, err)
t.Cleanup(testDB.Cleanup)
const query = `select count(*) as count from migration_log`
result := struct{ Count int }{}
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := x.Close(); err != nil {
fmt.Printf("failed to close xorm engine: %v", err)
}
})
err = NewDialect(x.DriverName()).CleanDB(x)
require.NoError(t, err)
@@ -61,16 +70,25 @@ func TestMigrations(t *testing.T) {
}
func TestMigrationLock(t *testing.T) {
dbType := getDBType()
dbType := sqlutil.GetTestDBType()
if dbType == SQLite {
t.Skip()
}
testDB := getTestDB(t, dbType)
testDB, err := sqlutil.GetTestDB(dbType)
require.NoError(t, err)
t.Cleanup(testDB.Cleanup)
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := x.Close(); err != nil {
fmt.Printf("failed to close xorm engine: %v", err)
}
})
dialect := NewDialect(x.DriverName())
sess := x.NewSession()
@@ -157,17 +175,28 @@ func TestMigrationLock(t *testing.T) {
}
func TestMigratorLocking(t *testing.T) {
dbType := getDBType()
testDB := getTestDB(t, dbType)
dbType := sqlutil.GetTestDBType()
// skip for SQLite for now since it occasionally fails for not clear reason
// anyway starting migrations concurretly for the same migrator is impossible use case
if dbType == SQLite {
t.Skip()
}
testDB, err := sqlutil.GetTestDB(dbType)
require.NoError(t, err)
t.Cleanup(testDB.Cleanup)
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := x.Close(); err != nil {
fmt.Printf("failed to close xorm engine: %v", err)
}
})
err = NewDialect(x.DriverName()).CleanDB(x)
require.NoError(t, err)
@@ -194,17 +223,27 @@ func TestMigratorLocking(t *testing.T) {
}
func TestDatabaseLocking(t *testing.T) {
dbType := getDBType()
dbType := sqlutil.GetTestDBType()
// skip for SQLite since there is no database locking (only migrator locking)
if dbType == SQLite {
t.Skip()
}
testDB := getTestDB(t, dbType)
testDB, err := sqlutil.GetTestDB(dbType)
require.NoError(t, err)
t.Cleanup(testDB.Cleanup)
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := x.Close(); err != nil {
fmt.Printf("failed to close xorm engine: %v", err)
}
})
err = NewDialect(x.DriverName()).CleanDB(x)
require.NoError(t, err)
@@ -280,37 +319,6 @@ func checkStepsAndDatabaseMatch(t *testing.T, mg *Migrator, expected []string) {
require.Failf(t, "the number of migrations does not match log in database", msg)
}
func getDBType() string {
dbType := SQLite
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
dbType = db
}
return dbType
}
func getTestDB(t *testing.T, dbType string) sqlutil.TestDB {
switch dbType {
case "mysql":
return sqlutil.MySQLTestDB()
case "postgres":
return sqlutil.PostgresTestDB()
default:
f, err := os.CreateTemp(".", "grafana-test-db-")
require.NoError(t, err)
t.Cleanup(func() {
err := os.Remove(f.Name())
require.NoError(t, err)
})
return sqlutil.TestDB{
DriverName: "sqlite3",
ConnStr: f.Name(),
}
}
}
func replaceDBName(t *testing.T, connStr, dbType string) string {
switch dbType {
case "mysql":
@@ -31,8 +31,13 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegration_DashboardPermissionFilter(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
)
@@ -26,6 +27,10 @@ const (
page int64 = 1
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestBuilder_EqualResults_Basic(t *testing.T) {
user := &user.SignedInUser{
UserID: 1,
+76 -60
View File
@@ -91,8 +91,8 @@ func ProvideService(cfg *setting.Cfg,
return s, nil
}
func ProvideServiceForTests(cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*SQLStore, error) {
return initTestDB(cfg, features, migrations, InitTestDBOpt{EnsureDefaultOrgAndUser: true})
func ProvideServiceForTests(t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*SQLStore, error) {
return initTestDB(t, cfg, features, migrations, InitTestDBOpt{EnsureDefaultOrgAndUser: true})
}
func newSQLStore(cfg *setting.Cfg, engine *xorm.Engine,
@@ -145,11 +145,6 @@ func (ss *SQLStore) Migrate(isDatabaseLockingEnabled bool) error {
return migrator.Start(isDatabaseLockingEnabled, ss.dbCfg.MigrationLockAttemptTimeout)
}
// Sync syncs changes to the database.
func (ss *SQLStore) Sync() error {
return ss.engine.Sync2()
}
// Reset resets database state.
// If default org and user creation is enabled, it will be ensured they exist in the database.
func (ss *SQLStore) Reset() error {
@@ -388,16 +383,10 @@ func (ss *SQLStore) RecursiveQueriesAreSupported() (bool, error) {
return *ss.recursiveQueriesAreSupported, nil
}
// ITestDB is an interface of arguments for testing db
type ITestDB interface {
Helper()
Fatalf(format string, args ...any)
Logf(format string, args ...any)
Log(args ...any)
}
var testSQLStoreSetup = false
var testSQLStore *SQLStore
var testSQLStoreMutex sync.Mutex
var testSQLStoreCleanup []func()
// InitTestDBOpt contains options for InitTestDB.
type InitTestDBOpt struct {
@@ -407,10 +396,10 @@ type InitTestDBOpt struct {
}
// InitTestDBWithMigration initializes the test DB given custom migrations.
func InitTestDBWithMigration(t ITestDB, migration registry.DatabaseMigrator, opts ...InitTestDBOpt) *SQLStore {
func InitTestDBWithMigration(t sqlutil.ITestDB, migration registry.DatabaseMigrator, opts ...InitTestDBOpt) *SQLStore {
t.Helper()
features := getFeaturesForTesting(opts...)
store, err := initTestDB(setting.NewCfg(), features, migration, opts...)
store, err := initTestDB(t, setting.NewCfg(), features, migration, opts...)
if err != nil {
t.Fatalf("failed to initialize sql store: %s", err)
}
@@ -418,20 +407,46 @@ func InitTestDBWithMigration(t ITestDB, migration registry.DatabaseMigrator, opt
}
// InitTestDB initializes the test DB.
func InitTestDB(t ITestDB, opts ...InitTestDBOpt) *SQLStore {
func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *SQLStore {
t.Helper()
features := getFeaturesForTesting(opts...)
store, err := initTestDB(setting.NewCfg(), features, migrations.ProvideOSSMigrations(features), opts...)
store, err := initTestDB(t, setting.NewCfg(), features, migrations.ProvideOSSMigrations(features), opts...)
if err != nil {
t.Fatalf("failed to initialize sql store: %s", err)
}
return store
}
func InitTestDBWithCfg(t ITestDB, opts ...InitTestDBOpt) (*SQLStore, *setting.Cfg) {
store := InitTestDB(t, opts...)
return store, store.Cfg
func SetupTestDB() {
testSQLStoreMutex.Lock()
defer testSQLStoreMutex.Unlock()
if testSQLStoreSetup {
fmt.Printf("ERROR: Test DB already set up, SetupTestDB called twice\n")
os.Exit(1)
}
testSQLStoreSetup = true
}
func CleanupTestDB() {
testSQLStoreMutex.Lock()
defer testSQLStoreMutex.Unlock()
if !testSQLStoreSetup {
fmt.Printf("ERROR: Test DB not set up, SetupTestDB not called\n")
os.Exit(1)
}
if testSQLStore != nil {
if err := testSQLStore.GetEngine().Close(); err != nil {
fmt.Printf("Failed to close testSQLStore engine: %s\n", err)
}
for _, cleanup := range testSQLStoreCleanup {
cleanup()
}
testSQLStoreCleanup = []func(){}
testSQLStore = nil
}
}
func getFeaturesForTesting(opts ...InitTestDBOpt) featuremgmt.FeatureToggles {
@@ -450,24 +465,43 @@ func getFeaturesForTesting(opts ...InitTestDBOpt) featuremgmt.FeatureToggles {
}
//nolint:gocyclo
func initTestDB(testCfg *setting.Cfg,
func initTestDB(t sqlutil.ITestDB, testCfg *setting.Cfg,
features featuremgmt.FeatureToggles,
migration registry.DatabaseMigrator,
opts ...InitTestDBOpt) (*SQLStore, error) {
testSQLStoreMutex.Lock()
defer testSQLStoreMutex.Unlock()
if !testSQLStoreSetup {
t.Fatalf(`
ERROR: Test DB not set up, are you missing TestMain?
https://github.com/grafana/grafana/blob/main/contribute/backend/style-guide.md
Example:
package mypkg
import (
"testing"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
`)
os.Exit(1)
}
if len(opts) == 0 {
opts = []InitTestDBOpt{{EnsureDefaultOrgAndUser: false, FeatureFlags: []string{}}}
}
if testSQLStore == nil {
dbType := migrator.SQLite
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
dbType = db
}
dbType := sqlutil.GetTestDBType()
// set test db config
cfg := setting.NewCfg()
@@ -482,21 +516,21 @@ func initTestDB(testCfg *setting.Cfg,
if _, err := sec.NewKey("type", dbType); err != nil {
return nil, err
}
switch dbType {
case "mysql":
if _, err := sec.NewKey("connection_string", sqlutil.MySQLTestDB().ConnStr); err != nil {
return nil, err
}
case "postgres":
if _, err := sec.NewKey("connection_string", sqlutil.PostgresTestDB().ConnStr); err != nil {
return nil, err
}
default:
if _, err := sec.NewKey("connection_string", sqlutil.SQLite3TestDB().ConnStr); err != nil {
return nil, err
}
testDB, err := sqlutil.GetTestDB(dbType)
if err != nil {
return nil, err
}
if _, err := sec.NewKey("connection_string", testDB.ConnStr); err != nil {
return nil, err
}
if _, err := sec.NewKey("path", testDB.Path); err != nil {
return nil, err
}
testSQLStoreCleanup = append(testSQLStoreCleanup, testDB.Cleanup)
// useful if you already have a database that you want to use for tests.
// cannot just set it on testSQLStore as it overrides the config in Init
if _, present := os.LookupEnv("SKIP_MIGRATIONS"); present {
@@ -539,24 +573,6 @@ func initTestDB(testCfg *setting.Cfg,
if err := testSQLStore.Migrate(false); err != nil {
return nil, err
}
if err := testSQLStore.Dialect.TruncateDBTables(engine); err != nil {
return nil, err
}
if err := testSQLStore.Reset(); err != nil {
return nil, err
}
// Make sure the changes are synced, so they get shared with eventual other DB connections
// XXX: Why is this only relevant when not skipping migrations?
if !testSQLStore.dbCfg.SkipMigrations {
if err := testSQLStore.Sync(); err != nil {
return nil, err
}
}
return testSQLStore, nil
}
// nolint:staticcheck
+8
View File
@@ -2,6 +2,7 @@ package sqlstore
import (
"context"
"os"
"testing"
"time"
@@ -11,6 +12,13 @@ import (
"github.com/grafana/grafana/pkg/services/org"
)
func TestMain(m *testing.M) {
SetupTestDB()
code := m.Run()
CleanupTestDB()
os.Exit(code)
}
func TestIntegrationIsUniqueConstraintViolation(t *testing.T) {
store := InitTestDB(t)
+113 -29
View File
@@ -1,25 +1,123 @@
package sqlutil
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
)
// ITestDB is an interface of arguments for testing db
type ITestDB interface {
Helper()
Fatalf(format string, args ...any)
Logf(format string, args ...any)
Log(args ...any)
Cleanup(func())
}
type TestDB struct {
DriverName string
ConnStr string
Path string
Cleanup func()
}
func SQLite3TestDB() TestDB {
// To run all tests in a local test database, set ConnStr to "grafana_test.db"
return TestDB{
DriverName: "sqlite3",
// ConnStr specifies an In-memory database shared between connections.
ConnStr: "file::memory:?cache=shared",
func GetTestDBType() string {
dbType := "sqlite3"
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
dbType = db
}
return dbType
}
func MySQLTestDB() TestDB {
func GetTestDB(dbType string) (*TestDB, error) {
switch dbType {
case "mysql":
return mySQLTestDB()
case "postgres":
return postgresTestDB()
case "sqlite3":
return sqLite3TestDB()
}
return nil, fmt.Errorf("unknown test db type: %s", dbType)
}
func sqLite3TestDB() (*TestDB, error) {
if os.Getenv("SQLITE_INMEMORY") == "true" {
return &TestDB{
DriverName: "sqlite3",
ConnStr: "file::memory:",
Cleanup: func() {},
}, nil
}
ret := &TestDB{
DriverName: "sqlite3",
Cleanup: func() {},
}
sqliteDb := os.Getenv("SQLITE_TEST_DB")
if sqliteDb == "" {
// try to create a database file in the user's cache directory
dir, err := os.UserCacheDir()
if err != nil {
return nil, err
}
// if cache dir doesn't exist, fall back to temp dir
if _, err := os.Stat(dir); errors.Is(err, fs.ErrNotExist) {
dir = os.TempDir()
if _, err := os.Stat(dir); err != nil {
return nil, err
}
}
err = os.Mkdir(filepath.Join(dir, "grafana-test"), 0750)
if err != nil && !errors.Is(err, fs.ErrExist) {
return nil, err
}
f, err := os.CreateTemp(filepath.Join(dir, "grafana-test"), "grafana-test-*.db")
if err != nil {
return nil, err
}
sqliteDb = f.Name()
ret.Cleanup = func() {
// remove db file if it exists
err := os.Remove(sqliteDb)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
fmt.Printf("Error removing sqlite db file %s: %v\n", sqliteDb, err)
}
// remove wal & shm files if they exist
err = os.Remove(sqliteDb + "-wal")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
fmt.Printf("Error removing sqlite wal file %s: %v\n", sqliteDb+"-wal", err)
}
err = os.Remove(sqliteDb + "-shm")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
fmt.Printf("Error removing sqlite shm file %s: %v\n", sqliteDb+"-shm", err)
}
}
}
ret.ConnStr = "file:" + sqliteDb + "?cache=private&mode=rwc"
if os.Getenv("SQLITE_JOURNAL_MODE") != "false" {
ret.ConnStr += "&_journal_mode=WAL"
}
ret.Path = sqliteDb
return ret, nil
}
func mySQLTestDB() (*TestDB, error) {
host := os.Getenv("MYSQL_HOST")
if host == "" {
host = "localhost"
@@ -29,13 +127,14 @@ func MySQLTestDB() TestDB {
port = "3306"
}
conn_str := fmt.Sprintf("grafana:password@tcp(%s:%s)/grafana_tests?collation=utf8mb4_unicode_ci&sql_mode='ANSI_QUOTES'&parseTime=true", host, port)
return TestDB{
return &TestDB{
DriverName: "mysql",
ConnStr: conn_str,
}
Cleanup: func() {},
}, nil
}
func PostgresTestDB() TestDB {
func postgresTestDB() (*TestDB, error) {
host := os.Getenv("POSTGRES_HOST")
if host == "" {
host = "localhost"
@@ -44,25 +143,10 @@ func PostgresTestDB() TestDB {
if port == "" {
port = "5432"
}
connStr := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanatest sslmode=disable",
host, port)
return TestDB{
connStr := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanatest sslmode=disable", host, port)
return &TestDB{
DriverName: "postgres",
ConnStr: connStr,
}
}
func MSSQLTestDB() TestDB {
host := os.Getenv("MSSQL_HOST")
if host == "" {
host = "localhost"
}
port := os.Getenv("MSSQL_PORT")
if port == "" {
port = "1433"
}
return TestDB{
DriverName: "mssql",
ConnStr: fmt.Sprintf("server=%s;port=%s;database=grafanatest;user id=grafana;password=Password!", host, port),
}
Cleanup: func() {},
}, nil
}