mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 14:03:24 +09:00
refactor: only reset a database table when the table's data was changed (#37573)
Reduce CI time Saves about 3 minutes for each test suit test-unit: 13min -> 10min (-race) test-pgsql: 24min -> 20min (-race) test-mysql: 15min -> 12min test-mssql: 16min -> 12min --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
co-authored by
silverwind
Claude
parent
6a509da96e
commit
2b93eaf55b
@@ -17,12 +17,15 @@ import (
|
|||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type engineContextKeyType struct{}
|
type contextKey struct{ key string }
|
||||||
|
|
||||||
var engineContextKey = engineContextKeyType{}
|
var (
|
||||||
|
contextKeyEngine = contextKey{"engine"}
|
||||||
|
ContextKeyTestFixtures = contextKey{"test-fixtures"}
|
||||||
|
)
|
||||||
|
|
||||||
func withContextEngine(ctx context.Context, e Engine) context.Context {
|
func withContextEngine(ctx context.Context, e Engine) context.Context {
|
||||||
return context.WithValue(ctx, engineContextKey, e)
|
return context.WithValue(ctx, contextKeyEngine, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -68,7 +71,7 @@ func contextSafetyCheck(e Engine) {
|
|||||||
|
|
||||||
// GetEngine gets an existing db Engine/Statement or creates a new Session
|
// GetEngine gets an existing db Engine/Statement or creates a new Session
|
||||||
func GetEngine(ctx context.Context) Engine {
|
func GetEngine(ctx context.Context) Engine {
|
||||||
if engine, ok := ctx.Value(engineContextKey).(Engine); ok {
|
if engine, ok := ctx.Value(contextKeyEngine).(Engine); ok {
|
||||||
// if reusing the existing session, need to do "contextSafetyCheck" because the Iterate creates a "autoResetStatement=false" session
|
// if reusing the existing session, need to do "contextSafetyCheck" because the Iterate creates a "autoResetStatement=false" session
|
||||||
contextSafetyCheck(engine)
|
contextSafetyCheck(engine)
|
||||||
return engine
|
return engine
|
||||||
@@ -309,7 +312,7 @@ func InTransaction(ctx context.Context) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTransactionSession(ctx context.Context) *xorm.Session {
|
func getTransactionSession(ctx context.Context) *xorm.Session {
|
||||||
e, _ := ctx.Value(engineContextKey).(Engine)
|
e, _ := ctx.Value(contextKeyEngine).(Engine)
|
||||||
if sess, ok := e.(*xorm.Session); ok && sess.IsInTx() {
|
if sess, ok := e.(*xorm.Session); ok && sess.IsInTx() {
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,17 @@ type EngineHook struct {
|
|||||||
var _ contexts.Hook = (*EngineHook)(nil)
|
var _ contexts.Hook = (*EngineHook)(nil)
|
||||||
|
|
||||||
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||||
|
if c.Ctx.Value(ContextKeyTestFixtures) != nil {
|
||||||
|
return c.Ctx, nil
|
||||||
|
}
|
||||||
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
|
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
|
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
|
||||||
|
if c.Ctx.Value(ContextKeyTestFixtures) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
span := gtprof.GetContextSpan(c.Ctx)
|
span := gtprof.GetContextSpan(c.Ctx)
|
||||||
if span != nil {
|
if span != nil {
|
||||||
// Do not record SQL parameters here:
|
// Do not record SQL parameters here:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
|||||||
if err := unittest.InitFixtures(
|
if err := unittest.InitFixtures(
|
||||||
unittest.FixturesOptions{
|
unittest.FixturesOptions{
|
||||||
Dir: fixturesDir,
|
Dir: fixturesDir,
|
||||||
}, x); err != nil {
|
}); err != nil {
|
||||||
t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err)
|
t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err)
|
||||||
return x, deferFn
|
return x, deferFn
|
||||||
}
|
}
|
||||||
@@ -110,6 +110,7 @@ func mainTest(m *testing.M) int {
|
|||||||
if err = git.InitFull(); err != nil {
|
if err = git.InitFull(); err != nil {
|
||||||
return testlogger.MainErrorf("Unable to InitFull: %v", err)
|
return testlogger.MainErrorf("Unable to InitFull: %v", err)
|
||||||
}
|
}
|
||||||
|
setting.Database.SlowQueryThreshold = 0
|
||||||
setting.LoadDBSetting()
|
setting.LoadDBSetting()
|
||||||
setting.InitLoggersForTest()
|
setting.InitLoggersForTest()
|
||||||
return m.Run()
|
return m.Run()
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||||
@@ -12,11 +15,13 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/contexts"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FixturesLoader interface {
|
type FixturesLoader interface {
|
||||||
Load() error
|
Load() error
|
||||||
|
MarkTableChanged(tableName string)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fixturesLoader FixturesLoader
|
var fixturesLoader FixturesLoader
|
||||||
@@ -57,15 +62,101 @@ func loadFixtureResetSeqPgsql(e *xorm.Engine) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fixturesHookStruct struct{}
|
||||||
|
|
||||||
|
func cutSpaceForSQL(s string) (string, string, bool) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
pos := strings.IndexFunc(s, unicode.IsSpace)
|
||||||
|
if pos == -1 {
|
||||||
|
return s, "", false
|
||||||
|
}
|
||||||
|
return s[:pos], strings.TrimSpace(s[pos+1:]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimTableNameQuotes(s string) string {
|
||||||
|
pos := strings.IndexByte(s, '.')
|
||||||
|
if pos != -1 {
|
||||||
|
s = s[pos+1:]
|
||||||
|
}
|
||||||
|
return strings.Trim(s, "\"`[]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fixturesHookStruct) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||||
|
if c.Ctx.Value(db.ContextKeyTestFixtures) != nil {
|
||||||
|
return c.Ctx, nil
|
||||||
|
}
|
||||||
|
ctx, sql := c.Ctx, c.SQL
|
||||||
|
cmdPart, cmdRemaining, ok := cutSpaceForSQL(sql)
|
||||||
|
if !ok {
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the SQLs which don't change data
|
||||||
|
if util.AsciiEqualFold(cmdPart, "SELECT") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "SHOW") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "PRAGMA") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "ALTER") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "CREATE") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "DROP") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "IF") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "SET") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "sp_rename") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "BEGIN") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "ROLLBACK") ||
|
||||||
|
util.AsciiEqualFold(cmdPart, "COMMIT") {
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case util.AsciiEqualFold(cmdPart, "INSERT"):
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
if util.AsciiEqualFold(cmdPart, "INTO") {
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
}
|
||||||
|
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||||
|
case util.AsciiEqualFold(cmdPart, "MERGE"):
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
if util.AsciiEqualFold(cmdPart, "INTO") {
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
}
|
||||||
|
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||||
|
case util.AsciiEqualFold(cmdPart, "UPDATE"):
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||||
|
case util.AsciiEqualFold(cmdPart, "DELETE"):
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
if util.AsciiEqualFold(cmdPart, "FROM") {
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
}
|
||||||
|
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||||
|
case util.AsciiEqualFold(cmdPart, "TRUNCATE"):
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
if util.AsciiEqualFold(cmdPart, "TABLE") {
|
||||||
|
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||||
|
}
|
||||||
|
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||||
|
default:
|
||||||
|
// should either parse the table name if it changes data, or ignore it
|
||||||
|
panic("unrecognized sql: " + sql)
|
||||||
|
}
|
||||||
|
_ = cmdRemaining
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fixturesHookStruct) AfterProcess(c *contexts.ContextHook) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InitFixtures initialize test fixtures for a test database
|
// InitFixtures initialize test fixtures for a test database
|
||||||
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
func InitFixtures(opts FixturesOptions) (err error) {
|
||||||
xormEngine := util.IfZero(util.OptionalArg(engine), GetXORMEngine())
|
xormEngine := GetXORMEngine()
|
||||||
fixturesLoader, err = NewFixturesLoader(xormEngine, opts)
|
fixturesLoader, err = NewFixturesLoader(xormEngine, opts)
|
||||||
// fixturesLoader = NewFixturesLoaderVendor(xormEngine, opts)
|
// fixturesLoader = NewFixturesLoaderVendor(xormEngine, opts)
|
||||||
|
|
||||||
// register the dummy hash algorithm function used in the test fixtures
|
// register the dummy hash algorithm function used in the test fixtures
|
||||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||||
|
xormEngine.AddHook(&fixturesHookStruct{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ type FixtureItem struct {
|
|||||||
|
|
||||||
type fixturesLoaderInternal struct {
|
type fixturesLoaderInternal struct {
|
||||||
xormEngine *xorm.Engine
|
xormEngine *xorm.Engine
|
||||||
xormTableNames map[string]bool
|
tableSyncMap sync.Map
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
dbType schemas.DBType
|
dbType schemas.DBType
|
||||||
fixtures map[string]*FixtureItem
|
fixtures map[string]*FixtureItem
|
||||||
@@ -148,25 +150,36 @@ func (f *fixturesLoaderInternal) Load() error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), db.ContextKeyTestFixtures, true)
|
||||||
|
|
||||||
for _, fixture := range f.fixtures {
|
for _, fixture := range f.fixtures {
|
||||||
if !f.xormTableNames[fixture.tableName] {
|
synced, existing := f.tableSyncMap.Load(fixture.tableName)
|
||||||
|
if synced == true || !existing {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := f.loadFixtures(tx, fixture); err != nil {
|
if err := f.loadFixtures(tx, fixture); err != nil {
|
||||||
return fmt.Errorf("failed to load fixtures from %s: %w", fixture.fileFullPath, err)
|
return fmt.Errorf("failed to load fixtures from %s: %w", fixture.fileFullPath, err)
|
||||||
}
|
}
|
||||||
|
f.tableSyncMap.Store(fixture.tableName, true)
|
||||||
}
|
}
|
||||||
if err = tx.Commit(); err != nil {
|
if err = tx.Commit(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for xormTableName := range f.xormTableNames {
|
f.tableSyncMap.Range(func(k, v any) bool {
|
||||||
if f.fixtures[xormTableName] == nil {
|
tableName, synced := k.(string), v.(bool)
|
||||||
_, _ = f.xormEngine.Exec("DELETE FROM `" + xormTableName + "`")
|
if !synced && f.fixtures[tableName] == nil {
|
||||||
|
_, _ = f.xormEngine.Context(ctx).Exec("DELETE FROM `" + tableName + "`")
|
||||||
}
|
}
|
||||||
}
|
f.tableSyncMap.Store(tableName, true)
|
||||||
|
return true
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fixturesLoaderInternal) MarkTableChanged(tableName string) {
|
||||||
|
f.tableSyncMap.Store(tableName, false)
|
||||||
|
}
|
||||||
|
|
||||||
func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) {
|
func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) {
|
||||||
if files != nil && len(files) == 0 {
|
if files != nil && len(files) == 0 {
|
||||||
return nil, nil //nolint:nilnil // load nothing
|
return nil, nil //nolint:nilnil // load nothing
|
||||||
@@ -215,11 +228,12 @@ func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, er
|
|||||||
f.paramPlaceholder = func(idx int) string { return "?" }
|
f.paramPlaceholder = func(idx int) string { return "?" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a model is not imported in a package (no bean is registered), the table won't exist in database.
|
||||||
|
// So only use tables of registered models (beans).
|
||||||
xormBeans, _ := db.NamesToBean()
|
xormBeans, _ := db.NamesToBean()
|
||||||
f.xormTableNames = map[string]bool{}
|
|
||||||
for _, bean := range xormBeans {
|
for _, bean := range xormBeans {
|
||||||
f.xormTableNames[x.TableName(bean)] = true
|
beanTableName := x.TableName(bean)
|
||||||
|
f.tableSyncMap.Store(trimTableNameQuotes(beanTableName), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ var (
|
|||||||
AutoMigration bool
|
AutoMigration bool
|
||||||
SlowQueryThreshold time.Duration
|
SlowQueryThreshold time.Duration
|
||||||
}{
|
}{
|
||||||
IterateBufferSize: 50,
|
IterateBufferSize: 50,
|
||||||
|
SlowQueryThreshold: 5 * time.Second,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
|||||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||||
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
|
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(Database.SlowQueryThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent
|
// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func InitIntegrationTest() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setting.Database.SlowQueryThreshold = 0
|
||||||
setting.LoadDBSetting()
|
setting.LoadDBSetting()
|
||||||
cleanupDb, err := unittest.ResetTestDatabase()
|
cleanupDb, err := unittest.ResetTestDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user