perf(tests): copy a migrated template DB instead of migrating per test

Most tests opened a throwaway panel DB with database.InitDB, which runs the
full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under
-race because GORM's reflection-heavy migration is what the detector slows
most. internal/web/service does this in ~550 of its 830 tests, so the CI race
job spent ~10 of its ~14.6 minutes re-migrating empty databases.

internal/database/dbtest.InitDB migrates once per test process, then hands
each test its own copy of that file (~130ms under -race) and registers the
CloseDB cleanup. The copy then goes through InitDB like a panel restart, so
every test still starts from the state a fresh install has. Tests that reopen
an existing file, migrate a hand-built legacy DB or target Postgres keep
calling database.InitDB.

Locally under -race: internal/web/service 626s (last CI run) -> 114s,
internal/sub 246s -> 35s.
This commit is contained in:
MHSanaei
2026-09-27 03:04:50 +02:00
parent 33a469315a
commit 12d51d7195
82 changed files with 289 additions and 451 deletions
+2 -4
View File
@@ -15,6 +15,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
"github.com/mhsanaei/3x-ui/v3/internal/web/session"
@@ -31,10 +32,7 @@ func newAPIAuthTestEngine(t *testing.T) (*gin.Engine, *APIController) {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
engine := gin.New()
store := cookie.NewStore([]byte("api-auth-test-secret"))
engine.Use(sessions.Sessions("3x-ui", store))
@@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
@@ -22,10 +23,7 @@ func seedPartlyApplyingClient(t *testing.T, email string, basePort int) (healthy
t.Helper()
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
db := database.GetDB()
ids := make([]int, 0, 2)
@@ -129,10 +127,7 @@ func TestDeleteHandlerFlagsRestartOnPartialApply(t *testing.T) {
func TestImportHandlerFlagsRestartWhenTrafficRestoreFails(t *testing.T) {
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
db := database.GetDB()
ib := &model.Inbound{
UserId: 1, Enable: true, Port: 43340, Tag: "in-import-partial",
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
@@ -45,10 +46,7 @@ func TestClientRenewalPreviewHTTP(t *testing.T) {
{name: "negative count", zone: "UTC", count: -1, invalid: true, wantError: "renewal preview reset and resetCount must not be negative\n"},
} {
t.Run(tt.name, func(t *testing.T) {
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
db := database.GetDB()
if err := db.Create(&model.Setting{Key: "timeLocation", Value: tt.zone}).Error; err != nil {
t.Fatal(err)
+2 -4
View File
@@ -14,6 +14,7 @@ import (
"github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
@@ -25,10 +26,7 @@ func newHostTestDB(t *testing.T) {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
}
type hostEnvelope struct {
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
@@ -23,10 +24,7 @@ func TestNodeSyncPushSkipsOperatorTLSGuard(t *testing.T) {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
prev := runtime.GetManager()
runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
t.Cleanup(func() { runtime.SetManager(prev) })
@@ -16,6 +16,7 @@ import (
"gorm.io/gorm"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
)
@@ -25,10 +26,7 @@ func newNodeCredentialTestEngine(t *testing.T) *gin.Engine {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
engine := gin.New()
engine.Use(func(c *gin.Context) {
+5 -15
View File
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
@@ -57,10 +58,7 @@ func TestValidateRegex(t *testing.T) {
func TestAPITokenMutationRoutesEnforceExpectedScope(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", t.TempDir())
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
row := &model.ApiToken{Name: "route-scope", Token: crypto.HashTokenSHA256("token"), Enabled: true, Scope: model.ApiScopeNodeSync}
if err := database.GetDB().Create(row).Error; err != nil {
t.Fatalf("seed token: %v", err)
@@ -95,10 +93,7 @@ func TestAPITokenMutationRoutesEnforceExpectedScope(t *testing.T) {
// rebind the authenticator without presenting a current code.
func TestUpdateSettingRequiresCodeToReplaceTwoFactorToken(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", t.TempDir())
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
settingService := service.SettingService{}
if err := settingService.SetTwoFactorToken("ORIGINALSECRET234567"); err != nil {
@@ -195,13 +190,8 @@ func TestTestDiscordEndpoint(t *testing.T) {
// Setup DB
t.Setenv("XUI_DB_FOLDER", t.TempDir())
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() {
_ = database.CloseDB()
SetDiscordService(nil)
})
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
t.Cleanup(func() { SetDiscordService(nil) })
settingService := service.SettingService{}
svc := discord.NewDiscordService(settingService)
+2 -5
View File
@@ -11,16 +11,13 @@ import (
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
)
func setupSubBalancerRouter(t *testing.T) *gin.Engine {
t.Helper()
t.Setenv("XUI_DB_FOLDER", t.TempDir())
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
gin.SetMode(gin.TestMode)
router := gin.New()
NewSubBalancerController(router.Group("/panel/api"))
@@ -13,6 +13,7 @@ import (
"gorm.io/gorm"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
)
@@ -21,10 +22,7 @@ func TestWarpIntervalReportsClockPersistenceFailure(t *testing.T) {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
db := database.GetDB()
for _, setting := range []*model.Setting{
{Key: "warpUpdateInterval", Value: "0"},