From deb31d3f3030d2a6aad2f966a9efc365f49daf67 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 1 May 2026 23:38:38 +0800 Subject: [PATCH] Refactor database connection (#37496) Clean up legacy copied&pasted code, introduce the unique "database connection" function. Move migration testing helper function PrepareTestEnv to a separate package. By the way, remove "shadow connection secrets" tricks: showing connection string on UI is useless --------- Co-authored-by: Nicolas --- Makefile | 2 +- cmd/dump.go | 4 +- models/db/conn.go | 173 ++++++++++++++ .../db/conn_test.go | 10 +- ...ith_schema.go => driver_postgresschema.go} | 4 +- models/db/driver_sqlite_mattn.go | 34 +++ models/db/engine_dump.go | 8 +- models/db/engine_init.go | 28 +-- models/db/engine_test.go | 2 +- models/migrations/base/db_test.go | 5 +- models/migrations/base/tests.go | 223 ------------------ models/migrations/migrationtest/tests.go | 120 ++++++++++ models/migrations/v1_14/main_test.go | 4 +- models/migrations/v1_14/v176_test.go | 4 +- models/migrations/v1_14/v177_test.go | 4 +- models/migrations/v1_15/main_test.go | 4 +- models/migrations/v1_15/v181_test.go | 4 +- models/migrations/v1_15/v182_test.go | 4 +- models/migrations/v1_16/main_test.go | 4 +- models/migrations/v1_16/v189_test.go | 4 +- models/migrations/v1_16/v193_test.go | 4 +- models/migrations/v1_16/v195_test.go | 4 +- models/migrations/v1_16/v210_test.go | 4 +- models/migrations/v1_17/main_test.go | 4 +- models/migrations/v1_17/v221_test.go | 4 +- models/migrations/v1_18/main_test.go | 4 +- models/migrations/v1_18/v229_test.go | 4 +- models/migrations/v1_18/v230_test.go | 4 +- models/migrations/v1_19/main_test.go | 4 +- models/migrations/v1_19/v233_test.go | 4 +- models/migrations/v1_20/main_test.go | 4 +- models/migrations/v1_20/v259_test.go | 4 +- models/migrations/v1_21/main_test.go | 4 +- models/migrations/v1_22/main_test.go | 4 +- models/migrations/v1_22/v283_test.go | 4 +- models/migrations/v1_22/v286_test.go | 4 +- models/migrations/v1_22/v287_test.go | 4 +- models/migrations/v1_22/v293_test.go | 4 +- models/migrations/v1_22/v294_test.go | 4 +- models/migrations/v1_23/main_test.go | 4 +- models/migrations/v1_23/v302_test.go | 4 +- models/migrations/v1_23/v304_test.go | 4 +- models/migrations/v1_25/main_test.go | 4 +- models/migrations/v1_25/v321_test.go | 6 +- models/migrations/v1_25/v322_test.go | 6 +- models/migrations/v1_26/main_test.go | 4 +- models/migrations/v1_26/v325_test.go | 4 +- models/migrations/v1_26/v326_test.go | 4 +- models/migrations/v1_26/v327_test.go | 4 +- models/migrations/v1_26/v329_test.go | 4 +- models/migrations/v1_27/main_test.go | 4 +- models/migrations/v1_27/v331_test.go | 6 +- models/unittest/testdb.go | 106 ++++++++- modules/setting/database.go | 157 ++---------- modules/setting/database_sqlite.go | 15 -- options/locale/locale_en-US.json | 2 - routers/init.go | 6 - routers/install/install.go | 4 +- routers/web/admin/admin_test.go | 58 ----- routers/web/admin/config.go | 63 +---- routers/web/healthcheck/check.go | 10 +- templates/admin/config.tmpl | 4 - .../migration-test/gitea-v1.6.4.mssql.sql.gz | Bin 12969 -> 13260 bytes .../migration-test/gitea-v1.7.0.mssql.sql.gz | Bin 13068 -> 13369 bytes .../migration-test/migration_test.go | 174 ++++---------- tests/sqlite.ini.tmpl | 2 +- tests/test_utils.go | 96 +------- 67 files changed, 611 insertions(+), 865 deletions(-) create mode 100644 models/db/conn.go rename modules/setting/database_test.go => models/db/conn_test.go (88%) rename models/db/{sql_postgres_with_schema.go => driver_postgresschema.go} (92%) create mode 100644 models/db/driver_sqlite_mattn.go delete mode 100644 models/migrations/base/tests.go create mode 100644 models/migrations/migrationtest/tests.go delete mode 100644 modules/setting/database_sqlite.go diff --git a/Makefile b/Makefile index ca4df2b1749..ad7739c07b9 100644 --- a/Makefile +++ b/Makefile @@ -164,7 +164,7 @@ TEST_PGSQL_PASSWORD ?= postgres TEST_PGSQL_SCHEMA ?= gtestschema TEST_MINIO_ENDPOINT ?= minio:9000 TEST_MSSQL_HOST ?= mssql:1433 -TEST_MSSQL_DBNAME ?= gitea +TEST_MSSQL_DBNAME ?= testgitea TEST_MSSQL_USERNAME ?= sa TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 diff --git a/cmd/dump.go b/cmd/dump.go index 49f4d9e8946..40b73f69aa1 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -203,8 +203,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error { } }() - targetDBType := cmd.String("database") - if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { + targetDBType := setting.DatabaseType(cmd.String("database")) + if targetDBType != "" && targetDBType != setting.Database.Type { log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) } else { log.Info("Dumping database...") diff --git a/models/db/conn.go b/models/db/conn.go new file mode 100644 index 00000000000..de6f3cd5ec7 --- /dev/null +++ b/models/db/conn.go @@ -0,0 +1,173 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +type ConnOptions struct { + Type setting.DatabaseType + Host string + Database string + User string + Passwd string + Schema string + SSLMode string + + SQLitePath string + SQLiteBusyTimeout int + SQLiteJournalMode string +} + +type SQLiteConnStrOptions struct { + FilePath string + BusyTimeout int + JournalMode string +} + +func GlobalConnOptions() ConnOptions { + return ConnOptions{ + Type: setting.Database.Type, + Host: setting.Database.Host, + Database: setting.Database.Name, + User: setting.Database.User, + Passwd: setting.Database.Passwd, + Schema: setting.Database.Schema, + SSLMode: setting.Database.SSLMode, + + SQLitePath: setting.Database.Path, + SQLiteBusyTimeout: setting.Database.SQLiteBusyTimeout, + SQLiteJournalMode: setting.Database.SQLiteJournalMode, + } +} + +const sqlDriverPostgresSchema = "postgresschema" + +var makeSQLiteConnStr = func(opts SQLiteConnStrOptions) (string, string, error) { + return "", "", errors.New(`this Gitea binary was not built with SQLite3 support, get an official release or rebuild with: -tags sqlite,sqlite_unlock_notify`) +} + +func ConnStrDefaultDatabase(opts ConnOptions) (string, string, error) { + opts.Database, opts.Schema = "", "" + return ConnStr(opts) +} + +func ConnStr(opts ConnOptions) (string, string, error) { + switch { + case opts.Type.IsMySQL(): + // use unix socket or tcp socket + connType := util.Iif(strings.HasPrefix(opts.Host, "/"), "unix", "tcp") + // allow (Postgres-inspired) default value to work in MySQL + tls := util.Iif(opts.SSLMode == "disable", "false", opts.SSLMode) + // in case the database name is a partial connection string which contains "?" parameters + paramSep := util.Iif(strings.Contains(opts.Database, "?"), "&", "?") + connStr := fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", opts.User, opts.Passwd, connType, opts.Host, opts.Database, paramSep, tls) + return "mysql", connStr, nil + + case opts.Type.IsPostgreSQL(): + connStr := makePgSQLConnStr(opts.Host, opts.User, opts.Passwd, opts.Database, opts.SSLMode) + driver := util.Iif(opts.Schema == "", "postgres", sqlDriverPostgresSchema) + registerPostgresSchemaDriver() + return driver, connStr, nil + + case opts.Type.IsMSSQL(): + host, port := parseMSSQLHostPort(opts.Host) + connStr := fmt.Sprintf("server=%s; port=%s; user id=%s; password=%s;", host, port, opts.User, opts.Passwd) + if opts.Database != "" { + connStr += "; database=" + opts.Database + } + return "mssql", connStr, nil + + case opts.Type.IsSQLite3(): + if opts.SQLitePath == "" { + return "", "", errors.New("sqlite3 database path cannot be empty") + } + if err := os.MkdirAll(filepath.Dir(opts.SQLitePath), os.ModePerm); err != nil { + return "", "", fmt.Errorf("failed to create directories: %w", err) + } + return makeSQLiteConnStr(SQLiteConnStrOptions{ + FilePath: opts.SQLitePath, + JournalMode: opts.SQLiteJournalMode, + BusyTimeout: opts.SQLiteBusyTimeout, + }) + } + return "", "", fmt.Errorf("unknown database type: %s", opts.Type) +} + +// parsePgSQLHostPort parses given input in various forms defined in +// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +// and returns proper host and port number. +func parsePgSQLHostPort(info string) (host, port string) { + if h, p, err := net.SplitHostPort(info); err == nil { + host, port = h, p + } else { + // treat the "info" as "host", if it's an IPv6 address, remove the wrapper + host = info + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + } + + // set fallback values + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "5432" + } + return host, port +} + +func makePgSQLConnStr(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) { + dbName, dbParam, _ := strings.Cut(dbName, "?") + host, port := parsePgSQLHostPort(dbHost) + connURL := url.URL{ + Scheme: "postgres", + User: url.UserPassword(dbUser, dbPasswd), + Host: net.JoinHostPort(host, port), + Path: dbName, + OmitHost: false, + RawQuery: dbParam, + } + query := connURL.Query() + if strings.HasPrefix(host, "/") { // looks like a unix socket + query.Add("host", host) + connURL.Host = ":" + port + } + query.Set("sslmode", dbsslMode) + connURL.RawQuery = query.Encode() + return connURL.String() +} + +// parseMSSQLHostPort splits the host into host and port +func parseMSSQLHostPort(info string) (string, string) { + // the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future + host, port := "127.0.0.1", "0" + if strings.Contains(info, ":") { + host = strings.Split(info, ":")[0] + port = strings.Split(info, ":")[1] + } else if strings.Contains(info, ",") { + host = strings.Split(info, ",")[0] + port = strings.TrimSpace(strings.Split(info, ",")[1]) + } else if len(info) > 0 { + host = info + } + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "0" + } + return host, port +} diff --git a/modules/setting/database_test.go b/models/db/conn_test.go similarity index 88% rename from modules/setting/database_test.go rename to models/db/conn_test.go index a742d54f8cb..ba33d252f2b 100644 --- a/modules/setting/database_test.go +++ b/models/db/conn_test.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package setting +package db import ( "testing" @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_parsePostgreSQLHostPort(t *testing.T) { +func TestParsePgSQLHostPort(t *testing.T) { tests := map[string]struct { HostPort string Host string @@ -49,14 +49,14 @@ func Test_parsePostgreSQLHostPort(t *testing.T) { for k, test := range tests { t.Run(k, func(t *testing.T) { t.Log(test.HostPort) - host, port := parsePostgreSQLHostPort(test.HostPort) + host, port := parsePgSQLHostPort(test.HostPort) assert.Equal(t, test.Host, host) assert.Equal(t, test.Port, port) }) } } -func Test_getPostgreSQLConnectionString(t *testing.T) { +func TestMakePgSQLConnStr(t *testing.T) { tests := []struct { Host string User string @@ -103,7 +103,7 @@ func Test_getPostgreSQLConnectionString(t *testing.T) { } for _, test := range tests { - connStr := getPostgreSQLConnectionString(test.Host, test.User, test.Passwd, test.Name, test.SSLMode) + connStr := makePgSQLConnStr(test.Host, test.User, test.Passwd, test.Name, test.SSLMode) assert.Equal(t, test.Output, connStr) } } diff --git a/models/db/sql_postgres_with_schema.go b/models/db/driver_postgresschema.go similarity index 92% rename from models/db/sql_postgres_with_schema.go rename to models/db/driver_postgresschema.go index 812fe4a6a61..b6735007631 100644 --- a/models/db/sql_postgres_with_schema.go +++ b/models/db/driver_postgresschema.go @@ -18,8 +18,8 @@ var registerOnce sync.Once func registerPostgresSchemaDriver() { registerOnce.Do(func() { - sql.Register("postgresschema", &postgresSchemaDriver{}) - dialects.RegisterDriver("postgresschema", dialects.QueryDriver("postgres")) + sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) + dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) }) } diff --git a/models/db/driver_sqlite_mattn.go b/models/db/driver_sqlite_mattn.go new file mode 100644 index 00000000000..4988a43d3f2 --- /dev/null +++ b/models/db/driver_sqlite_mattn.go @@ -0,0 +1,34 @@ +//go:build sqlite + +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/setting" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + setting.SupportedDatabaseTypes = append(setting.SupportedDatabaseTypes, "sqlite3") + makeSQLiteConnStr = makeSQLiteConnStrMattnCGO +} + +func makeSQLiteConnStrMattnCGO(opts SQLiteConnStrOptions) (string, string, error) { + var params []string + params = append(params, "cache=shared") + params = append(params, "mode=rwc") + params = append(params, "_busy_timeout="+strconv.Itoa(opts.BusyTimeout)) + params = append(params, "_txlock=immediate") + if opts.JournalMode != "" { + params = append(params, "_journal_mode="+opts.JournalMode) + } + connStr := fmt.Sprintf("file:%s?%s", opts.FilePath, strings.Join(params, "&")) + return "sqlite3", connStr, nil +} diff --git a/models/db/engine_dump.go b/models/db/engine_dump.go index 63f2d4e093c..1d8d555b448 100644 --- a/models/db/engine_dump.go +++ b/models/db/engine_dump.go @@ -3,10 +3,14 @@ package db -import "xorm.io/xorm/schemas" +import ( + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm/schemas" +) // DumpDatabase dumps all data from database according the special database SQL syntax to file system. -func DumpDatabase(filePath, dbType string) error { +func DumpDatabase(filePath string, dbType setting.DatabaseType) error { var tbs []*schemas.Table for _, t := range registeredModels { t, err := xormEngine.TableInfo(t) diff --git a/models/db/engine_init.go b/models/db/engine_init.go index ef5db3ff5ea..65192d3327a 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -6,7 +6,6 @@ package db import ( "context" "fmt" - "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -24,31 +23,23 @@ func init() { // newXORMEngine returns a new XORM engine from the configuration func newXORMEngine() (*xorm.Engine, error) { - connStr, err := setting.DBConnStr() + connOpts := GlobalConnOptions() + driver, connStr, err := ConnStr(connOpts) if err != nil { return nil, err } - var engine *xorm.Engine - - if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 { - // OK whilst we sort out our schema issues - create a schema aware postgres - registerPostgresSchemaDriver() - engine, err = xorm.NewEngine("postgresschema", connStr) - } else { - engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr) - } - + engine, err := xorm.NewEngine(driver, connStr) if err != nil { return nil, err } - switch setting.Database.Type { - case "mysql": + switch { + case connOpts.Type.IsMySQL(): engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) - case "mssql": + case connOpts.Type.IsMSSQL(): engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"}) } - engine.SetSchema(setting.Database.Schema) + engine.SetSchema(connOpts.Schema) return engine, nil } @@ -56,10 +47,7 @@ func newXORMEngine() (*xorm.Engine, error) { func InitEngine(ctx context.Context) error { xe, err := newXORMEngine() if err != nil { - if strings.Contains(err.Error(), "SQLite3 support") { - return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) - } - return fmt.Errorf("failed to connect to database: %w", err) + return fmt.Errorf("failed to init database engine: %w", err) } xe.SetMapper(names.GonicMapper{}) diff --git a/models/db/engine_test.go b/models/db/engine_test.go index 1c218df77f3..6a6264b5353 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -30,7 +30,7 @@ func TestDumpDatabase(t *testing.T) { assert.NoError(t, db.GetEngine(t.Context()).Sync(new(Version))) for _, dbType := range setting.SupportedDatabaseTypes { - assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), dbType)) + assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), setting.DatabaseType(dbType))) } } diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index dc4e502e421..ce6e1169d83 100644 --- a/models/migrations/base/db_test.go +++ b/models/migrations/base/db_test.go @@ -6,17 +6,18 @@ package base import ( "testing" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "xorm.io/xorm/names" ) func TestMain(m *testing.M) { - MainTest(m) + migrationtest.MainTest(m) } func Test_DropTableColumns(t *testing.T) { - x, deferable := PrepareTestEnv(t, 0) + x, deferable := migrationtest.PrepareTestEnv(t, 0) defer deferable() // FIXME: this logic seems wrong. Need to add an assertion here in the future, but it seems causing failure. if x == nil || t.Failed() { diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go deleted file mode 100644 index 0ec979a5131..00000000000 --- a/models/migrations/base/tests.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package base - -import ( - "database/sql" - "fmt" - "os" - "path" - "path/filepath" - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/testlogger" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/require" - "xorm.io/xorm" - "xorm.io/xorm/schemas" -) - -// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests - -func newXORMEngine(t *testing.T) (*xorm.Engine, error) { - if err := db.InitEngine(t.Context()); err != nil { - return nil, err - } - x := unittest.GetXORMEngine() - return x, nil -} - -func deleteDB() error { - switch { - case setting.Database.Type.IsSQLite3(): - if err := util.Remove(setting.Database.Path); err != nil { - return err - } - return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - return nil - case setting.Database.Type.IsPostgreSQL(): - db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - db.Close() - - // Check if we need to set up a specific schema - if len(setting.Database.Schema) != 0 { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schrows.Close() - - if !schrows.Next() { - // Create and set up a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - if err != nil { - return err - } - } - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - if err != nil { - return err - } - return nil - } - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { - return err - } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { - return err - } - default: - return fmt.Errorf("unsupported database type: %s", setting.Database.Type) - } - - return nil -} - -// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. -// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. -// -// fixtures in `models/migrations/fixtures/` will be loaded automatically -func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, func()) { - t.Helper() - ourSkip := 2 - ourSkip += skip - deferFn := testlogger.PrintCurrentTest(t, ourSkip) - giteaRoot := setting.GetGiteaTestSourceRoot() - require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath)) - - if err := deleteDB(); err != nil { - t.Fatalf("unable to reset database: %v", err) - return nil, deferFn - } - - x, err := newXORMEngine(t) - require.NoError(t, err) - if x != nil { - oldDefer := deferFn - deferFn = func() { - oldDefer() - if err := x.Close(); err != nil { - t.Errorf("error during close: %v", err) - } - if err := deleteDB(); err != nil { - t.Errorf("unable to reset database: %v", err) - } - } - } - if err != nil { - return x, deferFn - } - - if len(syncModels) > 0 { - if err := x.Sync(syncModels...); err != nil { - t.Errorf("error during sync: %v", err) - return x, deferFn - } - } - - fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name()) - - if _, err := os.Stat(fixturesDir); err == nil { - t.Logf("initializing fixtures from: %s", fixturesDir) - if err := unittest.InitFixtures( - unittest.FixturesOptions{ - Dir: fixturesDir, - }, x); err != nil { - t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) - return x, deferFn - } - if err := unittest.LoadFixtures(); err != nil { - t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) - return x, deferFn - } - } else if !os.IsNotExist(err) { - t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) - } else { - t.Logf("no fixtures found in: %s", fixturesDir) - } - - return x, deferFn -} - -func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { - tables, err := x.DBMetas() - require.NoError(t, err) - tableMap := make(map[string]*schemas.Table) - for _, table := range tables { - tableMap[table.Name] = table - } - return tableMap -} - -func mainTest(m *testing.M) int { - testlogger.Init() - err := setting.PrepareIntegrationTestConfig() - if err != nil { - return testlogger.MainErrorf("Unable to prepare integration test config: %v", err) - } - setting.SetupGiteaTestEnv() - - if err = git.InitFull(); err != nil { - return testlogger.MainErrorf("Unable to InitFull: %v", err) - } - setting.LoadDBSetting() - setting.InitLoggersForTest() - return m.Run() -} - -func MainTest(m *testing.M) { - os.Exit(mainTest(m)) -} diff --git a/models/migrations/migrationtest/tests.go b/models/migrations/migrationtest/tests.go new file mode 100644 index 00000000000..ed8bb16ef1a --- /dev/null +++ b/models/migrations/migrationtest/tests.go @@ -0,0 +1,120 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrationtest + +import ( + "os" + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/testlogger" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. +// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. +// +// fixtures in `models/migrations/fixtures/` will be loaded automatically +func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, func()) { + t.Helper() + ourSkip := 2 + ourSkip += skip + deferFn := testlogger.PrintCurrentTest(t, ourSkip) + giteaRoot := setting.GetGiteaTestSourceRoot() + require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath)) + + cleanup, err := unittest.ResetTestDatabase() + if err != nil { + t.Fatalf("unable to reset database: %v", err) + return nil, deferFn + } + { + oldDefer := deferFn + deferFn = func() { + cleanup() + oldDefer() + } + } + + err = db.InitEngine(t.Context()) + if !assert.NoError(t, err) { + return nil, deferFn + } + x := unittest.GetXORMEngine() + { + oldDefer := deferFn + deferFn = func() { + _ = x.Close() + oldDefer() + } + } + + if len(syncModels) > 0 { + if err := x.Sync(syncModels...); err != nil { + t.Errorf("error during sync: %v", err) + return x, deferFn + } + } + + fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name()) + + if _, err := os.Stat(fixturesDir); err == nil { + t.Logf("initializing fixtures from: %s", fixturesDir) + if err := unittest.InitFixtures( + unittest.FixturesOptions{ + Dir: fixturesDir, + }, x); err != nil { + t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) + return x, deferFn + } + if err := unittest.LoadFixtures(); err != nil { + t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) + return x, deferFn + } + } else if !os.IsNotExist(err) { + t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) + } else { + t.Logf("no fixtures found in: %s", fixturesDir) + } + + return x, deferFn +} + +func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { + tables, err := x.DBMetas() + require.NoError(t, err) + tableMap := make(map[string]*schemas.Table) + for _, table := range tables { + tableMap[table.Name] = table + } + return tableMap +} + +func mainTest(m *testing.M) int { + testlogger.Init() + err := setting.PrepareIntegrationTestConfig() + if err != nil { + return testlogger.MainErrorf("Unable to prepare integration test config: %v", err) + } + setting.SetupGiteaTestEnv() + + if err = git.InitFull(); err != nil { + return testlogger.MainErrorf("Unable to InitFull: %v", err) + } + setting.LoadDBSetting() + setting.InitLoggersForTest() + return m.Run() +} + +func MainTest(m *testing.M) { + os.Exit(mainTest(m)) +} diff --git a/models/migrations/v1_14/main_test.go b/models/migrations/v1_14/main_test.go index 978f88577c3..6ed240c4075 100644 --- a/models/migrations/v1_14/main_test.go +++ b/models/migrations/v1_14/main_test.go @@ -6,9 +6,9 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_14/v176_test.go b/models/migrations/v1_14/v176_test.go index 5c1db4db71c..aa57b5ad1d9 100644 --- a/models/migrations/v1_14/v176_test.go +++ b/models/migrations/v1_14/v176_test.go @@ -6,7 +6,7 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -47,7 +47,7 @@ func Test_RemoveInvalidLabels(t *testing.T) { } // load and prepare the test database - x, deferable := base.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_14/v177_test.go b/models/migrations/v1_14/v177_test.go index 263f69f3380..a86fb98830c 100644 --- a/models/migrations/v1_14/v177_test.go +++ b/models/migrations/v1_14/v177_test.go @@ -6,7 +6,7 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -34,7 +34,7 @@ func Test_DeleteOrphanedIssueLabels(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(IssueLabel), new(Label)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueLabel), new(Label)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_15/main_test.go b/models/migrations/v1_15/main_test.go index d01585e9978..768bbd310b0 100644 --- a/models/migrations/v1_15/main_test.go +++ b/models/migrations/v1_15/main_test.go @@ -6,9 +6,9 @@ package v1_15 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_15/v181_test.go b/models/migrations/v1_15/v181_test.go index 73b5c1f3d6c..e230c684eab 100644 --- a/models/migrations/v1_15/v181_test.go +++ b/models/migrations/v1_15/v181_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_AddPrimaryEmail2EmailAddress(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(User)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(User)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_15/v182_test.go b/models/migrations/v1_15/v182_test.go index 5fc6a0c467e..c0a1378534c 100644 --- a/models/migrations/v1_15/v182_test.go +++ b/models/migrations/v1_15/v182_test.go @@ -6,7 +6,7 @@ package v1_15 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_AddIssueResourceIndexTable(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Issue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Issue)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/main_test.go b/models/migrations/v1_16/main_test.go index 7f93d6e9e5e..c54424788d6 100644 --- a/models/migrations/v1_16/main_test.go +++ b/models/migrations/v1_16/main_test.go @@ -6,9 +6,9 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_16/v189_test.go b/models/migrations/v1_16/v189_test.go index fb56ac8e116..44424dd3691 100644 --- a/models/migrations/v1_16/v189_test.go +++ b/models/migrations/v1_16/v189_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/assert" @@ -27,7 +27,7 @@ func (ls *LoginSourceOriginalV189) TableName() string { func Test_UnwrapLDAPSourceCfg(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index 2e827f0550b..f68dd6d92d8 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -31,7 +31,7 @@ func Test_AddRepoIDForAttachment(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) defer deferrable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_16/v195_test.go b/models/migrations/v1_16/v195_test.go index 946e06e3997..bbfa5e162a8 100644 --- a/models/migrations/v1_16/v195_test.go +++ b/models/migrations/v1_16/v195_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func Test_AddTableCommitStatusIndex(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(CommitStatus)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(CommitStatus)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/v210_test.go b/models/migrations/v1_16/v210_test.go index 3b4ac7aa4b1..7bff2572e11 100644 --- a/models/migrations/v1_16/v210_test.go +++ b/models/migrations/v1_16/v210_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func Test_RemigrateU2FCredentials(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_17/main_test.go b/models/migrations/v1_17/main_test.go index 571a4f55a34..86522018719 100644 --- a/models/migrations/v1_17/main_test.go +++ b/models/migrations/v1_17/main_test.go @@ -6,9 +6,9 @@ package v1_17 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_17/v221_test.go b/models/migrations/v1_17/v221_test.go index a2dc0fae554..6fda9b9980e 100644 --- a/models/migrations/v1_17/v221_test.go +++ b/models/migrations/v1_17/v221_test.go @@ -7,7 +7,7 @@ import ( "encoding/base32" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -38,7 +38,7 @@ func Test_StoreWebauthnCredentialIDAsBytes(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_18/main_test.go b/models/migrations/v1_18/main_test.go index ebcfb45a941..b8641526f3d 100644 --- a/models/migrations/v1_18/main_test.go +++ b/models/migrations/v1_18/main_test.go @@ -6,9 +6,9 @@ package v1_18 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_18/v229_test.go b/models/migrations/v1_18/v229_test.go index 5722dd35574..638983ad0ba 100644 --- a/models/migrations/v1_18/v229_test.go +++ b/models/migrations/v1_18/v229_test.go @@ -7,7 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,7 @@ func Test_UpdateOpenMilestoneCounts(t *testing.T) { type ExpectedMilestone issues.Milestone // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_18/v230_test.go b/models/migrations/v1_18/v230_test.go index 25b2f6525da..e5e28ea63fc 100644 --- a/models/migrations/v1_18/v230_test.go +++ b/models/migrations/v1_18/v230_test.go @@ -6,7 +6,7 @@ package v1_18 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -18,7 +18,7 @@ func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(oauth2Application)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(oauth2Application)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_19/main_test.go b/models/migrations/v1_19/main_test.go index 87e807be6e1..784ca0e46ed 100644 --- a/models/migrations/v1_19/main_test.go +++ b/models/migrations/v1_19/main_test.go @@ -6,9 +6,9 @@ package v1_19 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_19/v233_test.go b/models/migrations/v1_19/v233_test.go index 7436ff7483c..3f7900c58f0 100644 --- a/models/migrations/v1_19/v233_test.go +++ b/models/migrations/v1_19/v233_test.go @@ -6,7 +6,7 @@ package v1_19 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" @@ -39,7 +39,7 @@ func Test_AddHeaderAuthorizationEncryptedColWebhook(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go index 2fd63a7118e..3ceb9a3c66d 100644 --- a/models/migrations/v1_20/main_test.go +++ b/models/migrations/v1_20/main_test.go @@ -6,9 +6,9 @@ package v1_20 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go index 0bf63719e5e..3864eecb78a 100644 --- a/models/migrations/v1_20/v259_test.go +++ b/models/migrations/v1_20/v259_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -66,7 +66,7 @@ func Test_ConvertScopedAccessTokens(t *testing.T) { }) } - x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(AccessToken)) defer deferable() if x == nil || t.Failed() { t.Skip() diff --git a/models/migrations/v1_21/main_test.go b/models/migrations/v1_21/main_test.go index 536a7ade088..daf98d40f4c 100644 --- a/models/migrations/v1_21/main_test.go +++ b/models/migrations/v1_21/main_test.go @@ -6,9 +6,9 @@ package v1_21 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go index ac8facd6aa0..e02c8a53282 100644 --- a/models/migrations/v1_22/main_test.go +++ b/models/migrations/v1_22/main_test.go @@ -6,9 +6,9 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go index 743f860466f..8e4c9410bd7 100644 --- a/models/migrations/v1_22/v283_test.go +++ b/models/migrations/v1_22/v283_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func Test_AddCombinedIndexToIssueUser(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueUser)) defer deferable() assert.NoError(t, AddCombinedIndexToIssueUser(x)) diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index b4a50f6fcb4..1bd7fac2f18 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "xorm.io/xorm" @@ -64,7 +64,7 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { } // Prepare and load the testing database - return base.PrepareTestEnv(t, 0, + return migrationtest.PrepareTestEnv(t, 0, new(Repository), new(CommitStatus), new(RepoArchiver), diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go index 2b42a33c389..21946a662ab 100644 --- a/models/migrations/v1_22/v287_test.go +++ b/models/migrations/v1_22/v287_test.go @@ -7,7 +7,7 @@ import ( "strconv" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_UpdateBadgeColName(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Badge)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Badge)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go index c7b643c7e08..bc3a33055cc 100644 --- a/models/migrations/v1_22/v293_test.go +++ b/models/migrations/v1_22/v293_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/models/project" "github.com/stretchr/testify/assert" @@ -14,7 +14,7 @@ import ( func Test_CheckProjectColumnsConsistency(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index 1cf03d61201..a711b5ec5f0 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "xorm.io/xorm/schemas" @@ -20,7 +20,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ProjectIssue)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go index f7b2caed83d..ffccac0fd3d 100644 --- a/models/migrations/v1_23/main_test.go +++ b/models/migrations/v1_23/main_test.go @@ -6,9 +6,9 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go index b008b6fc03d..1832adf39ab 100644 --- a/models/migrations/v1_23/v302_test.go +++ b/models/migrations/v1_23/v302_test.go @@ -6,7 +6,7 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionTask)) defer deferable() assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x)) diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go index c3dfa5e7e7e..9af84cd257f 100644 --- a/models/migrations/v1_23/v304_test.go +++ b/models/migrations/v1_23/v304_test.go @@ -6,7 +6,7 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -33,7 +33,7 @@ func Test_AddIndexForReleaseSha1(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Release)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Release)) defer deferable() assert.NoError(t, AddIndexForReleaseSha1(x)) diff --git a/models/migrations/v1_25/main_test.go b/models/migrations/v1_25/main_test.go index d2c4a4105d3..33c981edb97 100644 --- a/models/migrations/v1_25/main_test.go +++ b/models/migrations/v1_25/main_test.go @@ -6,9 +6,9 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_25/v321_test.go b/models/migrations/v1_25/v321_test.go index 3ef2c68aa33..0749a20e20e 100644 --- a/models/migrations/v1_25/v321_test.go +++ b/models/migrations/v1_25/v321_test.go @@ -6,7 +6,7 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -44,12 +44,12 @@ func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) defer deferrable() require.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x)) - tables := base.LoadTableSchemasMap(t, x) + tables := migrationtest.LoadTableSchemasMap(t, x) table := tables["review_state"] column := table.GetColumn("updated_files") assert.Equal(t, "LONGTEXT", column.SQLType.Name) diff --git a/models/migrations/v1_25/v322_test.go b/models/migrations/v1_25/v322_test.go index 78d890704c5..19646140351 100644 --- a/models/migrations/v1_25/v322_test.go +++ b/models/migrations/v1_25/v322_test.go @@ -6,7 +6,7 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -23,11 +23,11 @@ func Test_ExtendCommentTreePathLength(t *testing.T) { TreePath string `xorm:"VARCHAR(255)"` } - x, deferrable := base.PrepareTestEnv(t, 0, new(Comment)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Comment)) defer deferrable() require.NoError(t, ExtendCommentTreePathLength(x)) - table := base.LoadTableSchemasMap(t, x)["comment"] + table := migrationtest.LoadTableSchemasMap(t, x)["comment"] column := table.GetColumn("tree_path") assert.Contains(t, []string{"NVARCHAR", "VARCHAR"}, column.SQLType.Name) assert.EqualValues(t, 4000, column.Length) diff --git a/models/migrations/v1_26/main_test.go b/models/migrations/v1_26/main_test.go index 5aa12d553c9..0b271b9bbcf 100644 --- a/models/migrations/v1_26/main_test.go +++ b/models/migrations/v1_26/main_test.go @@ -6,9 +6,9 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_26/v325_test.go b/models/migrations/v1_26/v325_test.go index d4a66fee816..3fd658e01b9 100644 --- a/models/migrations/v1_26/v325_test.go +++ b/models/migrations/v1_26/v325_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/require" @@ -38,7 +38,7 @@ func Test_FixMissedRepoIDWhenMigrateAttachments(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) defer deferrable() require.NoError(t, FixMissedRepoIDWhenMigrateAttachments(x)) diff --git a/models/migrations/v1_26/v326_test.go b/models/migrations/v1_26/v326_test.go index b92eed35f69..a0225eb774c 100644 --- a/models/migrations/v1_26/v326_test.go +++ b/models/migrations/v1_26/v326_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -57,7 +57,7 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { TargetURL string } - x, deferable := base.PrepareTestEnv(t, 0, + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Repository), new(ActionRun), new(ActionRunJob), diff --git a/models/migrations/v1_26/v327_test.go b/models/migrations/v1_26/v327_test.go index 971707be4f9..98e948cf05c 100644 --- a/models/migrations/v1_26/v327_test.go +++ b/models/migrations/v1_26/v327_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/require" ) @@ -17,7 +17,7 @@ func Test_AddDisabledToActionRunner(t *testing.T) { Name string } - x, deferable := base.PrepareTestEnv(t, 0, new(ActionRunner)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionRunner)) defer deferable() _, err := x.Insert(&ActionRunner{Name: "runner"}) diff --git a/models/migrations/v1_26/v329_test.go b/models/migrations/v1_26/v329_test.go index cab8e79906d..e4bebfb71dc 100644 --- a/models/migrations/v1_26/v329_test.go +++ b/models/migrations/v1_26/v329_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -22,7 +22,7 @@ func (UserBadgeBefore) TableName() string { } func Test_AddUniqueIndexForUserBadge(t *testing.T) { - x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(UserBadgeBefore)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_27/main_test.go b/models/migrations/v1_27/main_test.go index e269e3df9a8..0c6a6a24401 100644 --- a/models/migrations/v1_27/main_test.go +++ b/models/migrations/v1_27/main_test.go @@ -6,9 +6,9 @@ package v1_27 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_27/v331_test.go b/models/migrations/v1_27/v331_test.go index 45f467cf9bc..2302fee024f 100644 --- a/models/migrations/v1_27/v331_test.go +++ b/models/migrations/v1_27/v331_test.go @@ -8,7 +8,7 @@ import ( "slices" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,7 +49,7 @@ func (actionArtifactBeforeV331) TableName() string { } func Test_AddActionRunAttemptModel(t *testing.T) { - x, deferable := base.PrepareTestEnv(t, 0, + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(actionRunBeforeV331), new(actionRunJobBeforeV331), new(actionArtifactBeforeV331), @@ -69,7 +69,7 @@ func Test_AddActionRunAttemptModel(t *testing.T) { require.NoError(t, AddActionRunAttemptModel(x)) - tableMap := base.LoadTableSchemasMap(t, x) + tableMap := migrationtest.LoadTableSchemasMap(t, x) attemptTable := tableMap["action_run_attempt"] require.NotNil(t, attemptTable) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index bd832348e7c..116fdab4968 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -5,6 +5,8 @@ package unittest import ( "context" + "database/sql" + "errors" "fmt" "os" "path/filepath" @@ -102,6 +104,101 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { return exitStatus } +func ResetTestDatabase() (cleanup func(), err error) { + defer func() { + if cleanup == nil { + cleanup = func() {} + } + }() + + connOpts := db.GlobalConnOptions() + driverDefault, connStrDefault, err := db.ConnStrDefaultDatabase(connOpts) + if err != nil { + return nil, err + } + driverDatabase, connStrDatabase, err := db.ConnStr(connOpts) + if err != nil { + return nil, err + } + + if connOpts.Type.IsSQLite3() { + if !strings.HasSuffix(connOpts.SQLitePath, "-test.db") { + return nil, errors.New(`testing database file for sqlite3 must end in "-test.db"`) + } + _ = os.Remove(connOpts.SQLitePath) + err = os.MkdirAll(filepath.Dir(connOpts.SQLitePath), os.ModePerm) + if err != nil { + return nil, err + } + cleanup = func() { + _ = os.Remove(connOpts.SQLitePath) + _ = os.Remove(filepath.Dir(connOpts.SQLitePath)) + } + return cleanup, nil + } + + if !strings.Contains(connOpts.Database, "test") { + return nil, fmt.Errorf(`testing database name for %s must contain "test"`, connOpts.Database) + } + + quotedDbName := connOpts.Database + if connOpts.Type.IsMSSQL() { + quotedDbName = `[` + connOpts.Database + `]` + } + + sqlExec := func(sqlDB *sql.DB, sql string) error { + _, err := sqlDB.Exec(sql) + if err != nil { + return fmt.Errorf("failed to execute SQL %q: %w", sql, err) + } + return nil + } + + createDatabase := func() error { + sqlDB, err := sql.Open(driverDefault, connStrDefault) + if err != nil { + return err + } + defer sqlDB.Close() + if err = sqlExec(sqlDB, "DROP DATABASE IF EXISTS "+quotedDbName); err != nil { + return err + } + return sqlExec(sqlDB, "CREATE DATABASE "+quotedDbName) + } + if err = createDatabase(); err != nil { + return nil, err + } + + cleanup = func() { + sqlDB, err := sql.Open(driverDefault, connStrDefault) + if err != nil { + return + } + defer sqlDB.Close() + _, _ = sqlDB.Exec("DROP DATABASE IF EXISTS " + quotedDbName) + } + + createDatabaseSchema := func() error { + if !connOpts.Type.IsPostgreSQL() { + return nil + } + if connOpts.Schema == "" { + return nil + } + sqlDB, err := sql.Open(driverDatabase, connStrDatabase) + if err != nil { + return err + } + defer sqlDB.Close() + if err = sqlExec(sqlDB, "DROP SCHEMA IF EXISTS "+connOpts.Schema); err != nil { + return err + } + return sqlExec(sqlDB, "CREATE SCHEMA "+connOpts.Schema) + } + + return cleanup, createDatabaseSchema() +} + // FixturesOptions fixtures needs to be loaded options type FixturesOptions struct { Dir string @@ -110,11 +207,12 @@ type FixturesOptions struct { // CreateTestEngine creates a memory database and loads the fixture data from fixturesDir func CreateTestEngine(opts FixturesOptions) error { - x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") + driver, connStr, err := db.ConnStr(db.ConnOptions{Type: "sqlite3", SQLitePath: ":memory:"}) + if err != nil { + return err + } + x, err := xorm.NewEngine(driver, connStr) if err != nil { - if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) - } return err } x.SetMapper(names.GonicMapper{}) diff --git a/modules/setting/database.go b/modules/setting/database.go index 1a4bf648058..2b069a62922 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -4,13 +4,7 @@ package setting import ( - "errors" - "fmt" - "net" - "net/url" - "os" "path/filepath" - "strings" "time" ) @@ -20,24 +14,22 @@ var ( // DatabaseTypeNames contains the friendly names for all database types DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", "sqlite3": "SQLite3"} - // EnableSQLite3 use SQLite3, set by build flag - EnableSQLite3 bool - // Database holds the database settings Database = struct { - Type DatabaseType - Host string - Name string - User string - Passwd string - Schema string - SSLMode string - Path string + Type DatabaseType + Host string + Name string + User string + Passwd string + Schema string + SSLMode string + Path string + + SQLiteBusyTimeout int + SQLiteJournalMode string + LogSQL bool - MysqlCharset string CharsetCollation string - Timeout int // seconds - SQLiteJournalMode string DBConnectRetries int DBConnectBackoff time.Duration MaxIdleConns int @@ -47,7 +39,7 @@ var ( AutoMigration bool SlowQueryThreshold time.Duration }{ - Timeout: 500, + SQLiteBusyTimeout: 500, IterateBufferSize: 50, } ) @@ -64,15 +56,14 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() Database.User = sec.Key("USER").String() - if len(Database.Passwd) == 0 { - Database.Passwd = sec.Key("PASSWD").String() - } + Database.Passwd = sec.Key("PASSWD").String() + Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") Database.CharsetCollation = sec.Key("CHARSET_COLLATION").String() Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) - Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) + Database.SQLiteBusyTimeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("") Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2) @@ -91,123 +82,9 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second) } -// DBConnStr returns database connection string -func DBConnStr() (string, error) { - var connStr string - paramSep := "?" - if strings.Contains(Database.Name, paramSep) { - paramSep = "&" - } - switch Database.Type { - case "mysql": - connType := "tcp" - if len(Database.Host) > 0 && Database.Host[0] == '/' { // looks like a unix socket - connType = "unix" - } - tls := Database.SSLMode - if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL - tls = "false" - } - connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", - Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, tls) - case "postgres": - connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Database.SSLMode) - case "mssql": - host, port := ParseMSSQLHostPort(Database.Host) - connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, Database.Name, Database.User, Database.Passwd) - case "sqlite3": - if !EnableSQLite3 { - return "", errors.New("this Gitea binary was not built with SQLite3 support") - } - if err := os.MkdirAll(filepath.Dir(Database.Path), os.ModePerm); err != nil { - return "", fmt.Errorf("Failed to create directories: %w", err) - } - journalMode := "" - if Database.SQLiteJournalMode != "" { - journalMode = "&_journal_mode=" + Database.SQLiteJournalMode - } - connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate%s", - Database.Path, Database.Timeout, journalMode) - default: - return "", fmt.Errorf("unknown database type: %s", Database.Type) - } - - return connStr, nil -} - -// parsePostgreSQLHostPort parses given input in various forms defined in -// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING -// and returns proper host and port number. -func parsePostgreSQLHostPort(info string) (host, port string) { - if h, p, err := net.SplitHostPort(info); err == nil { - host, port = h, p - } else { - // treat the "info" as "host", if it's an IPv6 address, remove the wrapper - host = info - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = host[1 : len(host)-1] - } - } - - // set fallback values - if host == "" { - host = "127.0.0.1" - } - if port == "" { - port = "5432" - } - return host, port -} - -func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) { - dbName, dbParam, _ := strings.Cut(dbName, "?") - host, port := parsePostgreSQLHostPort(dbHost) - connURL := url.URL{ - Scheme: "postgres", - User: url.UserPassword(dbUser, dbPasswd), - Host: net.JoinHostPort(host, port), - Path: dbName, - OmitHost: false, - RawQuery: dbParam, - } - query := connURL.Query() - if strings.HasPrefix(host, "/") { // looks like a unix socket - query.Add("host", host) - connURL.Host = ":" + port - } - query.Set("sslmode", dbsslMode) - connURL.RawQuery = query.Encode() - return connURL.String() -} - -// ParseMSSQLHostPort splits the host into host and port -func ParseMSSQLHostPort(info string) (string, string) { - // the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future - host, port := "127.0.0.1", "0" - if strings.Contains(info, ":") { - host = strings.Split(info, ":")[0] - port = strings.Split(info, ":")[1] - } else if strings.Contains(info, ",") { - host = strings.Split(info, ",")[0] - port = strings.TrimSpace(strings.Split(info, ",")[1]) - } else if len(info) > 0 { - host = info - } - if host == "" { - host = "127.0.0.1" - } - if port == "" { - port = "0" - } - return host, port -} - +// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent type DatabaseType string -func (t DatabaseType) String() string { - return string(t) -} - func (t DatabaseType) IsSQLite3() bool { return t == "sqlite3" } diff --git a/modules/setting/database_sqlite.go b/modules/setting/database_sqlite.go deleted file mode 100644 index c1037cfb27e..00000000000 --- a/modules/setting/database_sqlite.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build sqlite - -// Copyright 2014 The Gogs Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - _ "github.com/mattn/go-sqlite3" -) - -func init() { - EnableSQLite3 = true - SupportedDatabaseTypes = append(SupportedDatabaseTypes, "sqlite3") -} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9c6903edc6c..80692acdaf3 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3315,7 +3315,6 @@ "admin.config.cache_config": "Cache Configuration", "admin.config.cache_adapter": "Cache Adapter", "admin.config.cache_interval": "Cache Interval", - "admin.config.cache_conn": "Cache Connection", "admin.config.cache_item_ttl": "Cache Item TTL", "admin.config.cache_test": "Test Cache", "admin.config.cache_test_failed": "Failed to probe the cache: %v.", @@ -3330,7 +3329,6 @@ "admin.config.instance_web_banner.message_placeholder": "Banner message (supports markdown)", "admin.config.session_config": "Session Configuration", "admin.config.session_provider": "Session Provider", - "admin.config.provider_config": "Provider Config", "admin.config.cookie_name": "Cookie Name", "admin.config.gc_interval_time": "GC Interval Time", "admin.config.session_life_time": "Session Life Time", diff --git a/routers/init.go b/routers/init.go index 92eab5eaf28..e04b711c4d3 100644 --- a/routers/init.go +++ b/routers/init.go @@ -134,12 +134,6 @@ func InitWebInstalled(ctx context.Context) { external.RegisterRenderers() markup.Init(markup_service.FormalRenderHelperFuncs()) - if setting.EnableSQLite3 { - log.Info("SQLite3 support is enabled") - } else if setting.Database.Type.IsSQLite3() { - log.Fatal("SQLite3 support is disabled, but it is used for database setting. Please get or build a Gitea release with SQLite3 support.") - } - mustInitCtx(ctx, common.InitDBEngine) log.Info("ORM engine initialization successful!") mustInit(system.Init) diff --git a/routers/install/install.go b/routers/install/install.go index a0f32fb939c..718ede6564d 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -76,7 +76,7 @@ func Install(ctx *context.Context) { form.DbSchema = setting.Database.Schema form.SSLMode = setting.Database.SSLMode - curDBType := setting.Database.Type.String() + curDBType := string(setting.Database.Type) if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) { curDBType = "mysql" } @@ -328,7 +328,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) cfg.Section("").Key("RUN_MODE").SetValue("prod") - cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) + cfg.Section("database").Key("DB_TYPE").SetValue(string(setting.Database.Type)) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) cfg.Section("database").Key("USER").SetValue(setting.Database.User) diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index ecdd462f9e7..929faa19680 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -16,64 +16,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestShadowPassword(t *testing.T) { - kases := []struct { - Provider string - CfgItem string - Result string - }{ - { - Provider: "redis", - CfgItem: "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180", - Result: "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180", - }, - { - Provider: "mysql", - CfgItem: "root:@tcp(localhost:3306)/gitea?charset=utf8", - Result: "root:******@tcp(localhost:3306)/gitea?charset=utf8", - }, - { - Provider: "mysql", - CfgItem: "/gitea?charset=utf8", - Result: "/gitea?charset=utf8", - }, - { - Provider: "mysql", - CfgItem: "user:mypassword@/dbname", - Result: "user:******@/dbname", - }, - { - Provider: "postgres", - CfgItem: "user=pqgotest dbname=pqgotest sslmode=verify-full", - Result: "user=pqgotest dbname=pqgotest sslmode=verify-full", - }, - { - Provider: "postgres", - CfgItem: "user=pqgotest password= dbname=pqgotest sslmode=verify-full", - Result: "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full", - }, - { - Provider: "postgres", - CfgItem: "postgres://user:pass@hostname/dbname", - Result: "postgres://user:******@hostname/dbname", - }, - { - Provider: "couchbase", - CfgItem: "http://dev-couchbase.example.com:8091/", - Result: "http://dev-couchbase.example.com:8091/", - }, - { - Provider: "couchbase", - CfgItem: "http://user:the_password@dev-couchbase.example.com:8091/", - Result: "http://user:******@dev-couchbase.example.com:8091/", - }, - } - - for _, k := range kases { - assert.Equal(t, k.Result, shadowPassword(k.Provider, k.CfgItem)) - } -} - func TestSelfCheckPost(t *testing.T) { defer test.MockVariableValue(&setting.PublicURLDetection)() defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index bf48e554dfd..03a15b67134 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -7,8 +7,6 @@ package admin import ( "errors" "net/http" - "net/url" - "strings" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/cache" @@ -59,63 +57,6 @@ func TestCache(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/-/admin/config") } -func shadowPasswordKV(cfgItem, splitter string) string { - fields := strings.Split(cfgItem, splitter) - for i := range fields { - if strings.HasPrefix(fields[i], "password=") { - fields[i] = "password=******" - break - } - } - return strings.Join(fields, splitter) -} - -func shadowURL(provider, cfgItem string) string { - u, err := url.Parse(cfgItem) - if err != nil { - log.Error("Shadowing Password for %v failed: %v", provider, err) - return cfgItem - } - if u.User != nil { - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - } - return cfgItem -} - -func shadowPassword(provider, cfgItem string) string { - switch provider { - case "redis": - return shadowPasswordKV(cfgItem, ",") - case "mysql": - // root:@tcp(localhost:3306)/macaron?charset=utf8 - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.Index(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - return cfgItem - case "postgres": - // user=jiahuachen dbname=macaron port=5432 sslmode=disable - if !strings.HasPrefix(cfgItem, "postgres://") { - return shadowPasswordKV(cfgItem, " ") - } - fallthrough - case "couchbase": - return shadowURL(provider, cfgItem) - // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full - // Notice: use shadowURL - } - return cfgItem -} - // Config show admin config page func Config(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.config_summary") @@ -150,8 +91,6 @@ func Config(ctx *context.Context) { ctx.Data["CacheAdapter"] = setting.CacheService.Adapter ctx.Data["CacheInterval"] = setting.CacheService.Interval - - ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) ctx.Data["CacheItemTTL"] = setting.CacheService.TTL sessionCfg := setting.SessionConfig @@ -169,7 +108,7 @@ func Config(ctx *context.Context) { sessionCfg.Secure = realSession.Secure sessionCfg.Domain = realSession.Domain } - sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) + sessionCfg.ProviderConfig = "" ctx.Data["SessionConfig"] = sessionCfg ctx.Data["Git"] = setting.Git diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go index de9b2c8ec17..116aab886bc 100644 --- a/routers/web/healthcheck/check.go +++ b/routers/web/healthcheck/check.go @@ -111,16 +111,10 @@ func checkDatabase(ctx context.Context, checks checks) status { } if setting.Database.Type.IsSQLite3() && st.Status == pass { - if !setting.EnableSQLite3 { + if _, err := os.Stat(setting.Database.Path); err != nil { st.Status = fail st.Time = getCheckTime() - log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled") - } else { - if _, err := os.Stat(setting.Database.Path); err != nil { - st.Status = fail - st.Time = getCheckTime() - log.Error("SQLite3 file exists check failed with error: %v", err) - } + log.Error("SQLite3 file exists check failed with error: %v", err) } } diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index b68f2c1a7ae..c381c5bf1d0 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -244,8 +244,6 @@
{{.CacheInterval}} {{ctx.Locale.Tr "tool.raw_seconds"}}
{{end}} {{if .CacheConn}} -
{{ctx.Locale.Tr "admin.config.cache_conn"}}
-
{{.CacheConn}}
{{ctx.Locale.Tr "admin.config.cache_item_ttl"}}
{{.CacheItemTTL}}
{{end}} @@ -266,8 +264,6 @@
{{ctx.Locale.Tr "admin.config.session_provider"}}
{{.SessionConfig.Provider}}
-
{{ctx.Locale.Tr "admin.config.provider_config"}}
-
{{if .SessionConfig.ProviderConfig}}{{.SessionConfig.ProviderConfig}}{{else}}-{{end}}
{{ctx.Locale.Tr "admin.config.cookie_name"}}
{{.SessionConfig.CookieName}}
{{ctx.Locale.Tr "admin.config.gc_interval_time"}}
diff --git a/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz index 1b676feda1cbced0754c4ce0751e159e4a078e6f..85ba253cf88183a77afb8ee4ad58b1892789cb58 100644 GIT binary patch literal 13260 zcmZX*Q*b6s7cCsy$;3`3w(TdjZF6GVHYd)Li6*vf+qP|;dB3XvuTym{x~s9*>b~jS zU8`0TMZ>_%89i8mfLmC(m>JQ#F*9&5urb&Uqh=M1GmhJ(aZc6P0pWugA9PQTE$3(l))xidz z?(X~Nv&*wC9)J_S*31uP>Vvx2U- zEcdH`_T`YuGp)!n%z11E?>3EvpVRGA<_*K`&=^Wydf=s}z!S$A&Fy zG{11u?%!@M?@pd5w`Xp|RzIPQZeLNdD!$*E-?2`(*GM+@UOMcL|JA)6PEY>*dzYOY1i<^s5ejup^de!t+18c& zQMefgy>0KGCEGZAdEixj-Hm9U?--&*wY@&O?6BN#>w#S5<0=MLWVRxzo7|0TWDzuB zN+H(>X8**C z?GrJn1$UFQxU&!9SIMqGE0G~1bDmYr@8974y60&}UQI=eflW0zjaP$j=Z?NCRTkFC zr5DZfqmv1$guc#L_qa)>_KZgH?xC`y$Fo?PZiR*kgjO5#+YRHO7>K0YU)%3K$lhDM zKi}SfxKY}d*og}nATW;>G>|ZS|!`R)v_vzb{uSAX878EWxoq|D7kLs<8Tf!BgJUi#- z@ELZ;U=q;>#nebEYac0%{NYuH*Au*FFW-D2Ll19Pm~h})gxYbvmxsUIyM>M`o-p4$ z*DQV5KT=_k+vbd(xpIklUmn*tT_jZ`3}$33SG%ktn!eK>tY=!6swiiEnE;9%YW0u} zx<&mKuKixWJkZ}Wm6Eb7wu2{U4^Hd1CCYVM?H<^Z@EX2`q}{(fDw@`>gT&m6X>E-w zQ2*c{1w7{1w8y^Yh`KlDL?_Gq#kDkb#VGEHake^mQoqk_s$KfP8W7SLZh-KIaNv?=tmh+j-VFBFb7*<0nWg?BoxZXaxW12fA9E z^?>0NeCHa--`pHY#s~=zvVk~_6Nu9&UPylz)t}WxrmP5fK2|gdmNrW~{tAcY%g~)U z;olgF+`O#SdCt(IvqnT6h~a(r+CzoT&UbDyi}x#id9OgDm-8tw4#Y9;p;`epsRlD> zT(y~lfxI#xdiKL;)(`kC*nAtIQ4NHRnb*|vHS|nQ7F;8|n)RQKF&xehh;<^T<(s1# zjV6#IlO?;}or#W^)+%#!!mGJ2QR4ICKq)O%bDLF#HGU7Cs5Xd+XT={GzT2=Mr=?a# z_GvwV!7qpsfk69vE$tGQ959>B1~y*-cNTfhF!LTl>3svU zgnM@`w$&3%IALZ^5A!SRxuX;?BfO6IsTpzzN+L$|!m3W^m=`9p@e^)k7%D4<(fP%I zP&=)sebkUE)+w{P%$dF7)TTp*=5aFD*OYt0qEUDwF z<3MN%KugC_o4T&UXjaZENOnto=&+-#h;ilgCJvOO-(i5Dd(WeC1-RZ=Wv8RC^pLZS zpa<>~j9vK6WbtN%`LZeN&77lI7ESR)U4PuTo9s39mSsK?UfLyv8g`}F^6-hCh)=w{3$bEju2fX$Iwggg zO;#T}>E8n3ErYNFRt!h#ay-bvSVelz(y{Y)YT9I6L!A`a@Ha;sLkUL~`7(+u;dKT_CRJanC8n|0^wb3}(R zD9fpE*n+eq_`-N=zRKCDf{0$ZBa*A+?;lYw8~ zoM4G%)e>6aX@WK&a;}yyglYAt3`zSKfxvKW*=(iFJwI!2il?0iPdoLxB~eeg}#Wk~*J*|Yo z&6uFnJKQh^TY7KCT>a`@NNm1QB!2Y~#<2*gJAoE3KBYPn<~rU}FoEvZi_GdOVLarR zsw3KME?XUuLf$hywW6zIeY1RMJ#FpCwB+^T1NdZ`U7f&Jp~orcfd|I4;usb`!>aKd z9{sAh7REq!?NgMg%Zo_zj+ROzOb zAxhIZ$?XKxTb`m_V-h>|eN;Bc?eUEQ`>o+)Jt!U-3hOf58O)Q#v-j> z>b;oJ$lS>FZZf+|b7UqHe$qXoP8hD56?DS;T6kKK86F(&-u&`AfWMd{u`%$Oy3wBxNEHJEi#Q%Ad=4Vp9wq~{VIMpuO`RQ-O{dq+<`2?jUibZwP$CbV@J{iH^Y)0Pk{of1C`$ZQ zR3kX?Sl#0VwdZG8_6TF*NNz^jaX&+aR;7sJ2X~Qpg#Yh+!Lgqrr0Hhk1fRj79_K2# zLPsPY*QILYOJsc@9P_inv8+ONM{BEj1tN?gIep}kWZ{E~hK8)#Hklb6u=jnkQ(&RD)esSSmTUFT?TWFtFd1NikNp+m* zuAf!eB#a1BG<0VH6KOW&^@OUutlIRSc#E9W+yAA##p9ZHw}Iue)A)mm%+}diQi@@Vq7rvi50hv+4Ck|ME#vCsR5{HtsQl!??R4UGBmMg=DW_d?;LGX+ zzUZ4^eQrIsQ@L{W#Du&~S?&qB^I*}}WG@=jl1@9qW5K6ctn;Md3*!A*kJsez3<$!J z&4f}&PdjuzXZMYnTz9&|(2tCA3Zmh<_bQW*OKysY2>gy145Pv~vpbsF+Zo@$;^^`} zCe?>TV}|R;6c8+VCe7NNL2&7~~X+t71-EcR~%75<)Oui4J9HyBgv z7adpV=zRwnA`4+-N;qfVa@EN&FqhbOCkQ< z(8Xs1WUY;Q>V-UY&O}Yi_yFl-T zbSaxpRBNP`(!Y9COV}*Snp3T4 zmUFFb?hIRk&JBJ2i)!sNtm*-?ZR?cAxoGjCqEf4Q`-+_>8q$*fNV-H&!&}^d+qhOo zVC`+m=NfdGm2><#1GFk&F54aV-c!S_0n@r4>w2*kq)@PdhdE zhjPTC*!KhaqK(+kG=Ppu%G$k)N#l_4C*PoX68K-Yh9$og@G91=%1J=DF>^B!=T~D8 zFBsU?H0h$gB_=M&M2z3J@3!T{R{vDNvmeL##J0P6{g>_5g)n&d-}F9GRmwr}YrGd^ z4$De?GOL+5EkSmZPgUX1kLT(pzsZr%De@P1pe zjekzeqiC1q?iN3hG17;fOY4&Py;bQEBN}H~VdgBE%eH^$kK?Ko?ObsmlxR)Z>uDFQI6Q&L zAoQ>K1$c=WsIVNs-~*#}m_LLa_#W7rV0NwaOZkqVEFSR#g0MT(CdSwI4g(u%6(xTV zm114orpd5ic0D}m(CG*uN2z#%d&h+eLzajjXIpih_F`x zWw7(s_io5}|CW?Ys>VqrzfU$rhcC~GCto&v-0cq!!B~`zf|RKRPz3)l$FEDrzLnfD z=W2Q5`KZZVt{<;cgJsbWZpxz`d4`s$vrcV=uU83RlB?Etw`mJyX!k8=_7PmjSFo{9 z%LG&cJx0%Cz#+k+x?>n_k}~mC<3~>0uu!)N4+~UoG)-R)BoHA<4Yk4p{Z&J@`$r{z z)9mX<^Lp@rpQ41s$*xpPs;7NB9;F`JZ7L8623_;Ck)DJ9Js)13YB;)*$o_gP`y&Xp ztRK8~c*2k0%9jC^J6DN!Bq0d7S*{je22=f`OB_M%tS|mNzO8-~QB<=EdgnwYA!L>U zwh<=dNfc+s7K*`jb8|N}2X3-|mfRT>dot8sn-^O3(%S%Gy>})bX@=S`loaOOY1}WW zl+^R5gj4*K)K==NjA5^*Q+5|!ZYZlDzyciBffW6_h*A*Vmn4EibA_Y#HMpEj4f3Np zY>kxl^3%S0^`v~%YC>cjK>s02dFSAN(y(5MM6c0JM7<&aGJYt`adL9sN(RRqeHc-B zoZN}H!uzu~c$At-myxZgl*-?XtzpVBByU*8%(HCpuFw%9LL)mhiLnB6yxb6GU2^fp zKc(oqeubtGrA3^$O}rz?iIJzO&nc7=&#Wa&KAG$MlN|OlqC$uNu8U&@7F_Q_l zkQ64U$Z;?=djjY48+II$W_W2U-&g&M_lg)Yl_&c=4S|wO=5m!s8dI|s=7#Ave0N@7 zpujH^QfDZma10RCJlqu?3jBF51%arq!Jz|a9-1G?x5&4{!iO2xL6QNT>0IL{x!HL% zxa7vDJ2W(1$uTl|IxF&%I)Q6_F1poWtPAL9R^;X&L6E$Et`FM7ibK?j>q$xe(qO4$ z^>E&UXoQv=;p?V?XcYU)wDUuATBL)2k5PP2d@nnP%iFyLm;8!lABi?aEhXwKTV&WXoaT?XU|CCJZ z6=MLZ^5u~e!Hdm~1mSao0$}i^;t=0gg8H5!(o{Yq7BJ;jqPQ*5MEjvhp&4kma$CrM%avFa7k{CF&Ouq(i(BIIKvS$$DaAE`b5y3oke-QhXD_ugL)znkD6njPxf!8o%=1CNhE{Gi&8`zR>_GtA%B@ zv$!waJnTt0J*m;eC-1GshRVgP^?|ggj#zVN(SiUKWx`yN<9Fl(cwkAP)p348B4`zn z2~7-ARP>{ohFYu!JIjgcIo~toJ_1|?h?CmtO0Lz-V+h5d3H=_uaLc9`Ni>D9{Tlwh zvKJv4`=}&+h!X2)sn#@c49K2~6rHRn{yzRB@%Hx_BTE6LQ@+Mu6Jc+_Se)IK{b)nBaI;>?jE7h+7vyf zt$;ZN=M!x9B`Tq&!>qa5B2>gwAr>BXX66*v1HGy<@hbI@^N3Ir^=rBNd;*uS*?#(1LRRa2@P+yE6Y$d5 z=UVRn$u#h6B|phdoG#2I=yR7%(DGswK*~UYcYcZ%=SSe&bZ)Z$|m5U)W)H z##3|`mxi|v+Wl7+xkYCaq{an)W!C!&IS;e#a4yInpw6M3iWh+3cCdJv$GfT5sSDuQ zojZ?)X_IN5g@c#`qex|&dO`bnHH*XqScpLxOfs*K0DKy=pYB_CWX~#(_>HI6Yg=BM zu^Kn0G_6^4XivJeQ>BdvT8nFm*nj?6J9u{HuCYZ+d(|4>i{2mw_<;s%`RJ_y87i?- zJi=>iowdv#Lg#WUy$;)Z>a^D30&6#a;OGI zK$7JWnSxcjYibB&>B5fcV*cSfgd|#}*8%k;E*sFFhMnstyCv&VzFs_Eu~snfY!ZTU=vL@ttY=$HbRnwpR! z6r5UsAOD?dS)W!JSl2FDwxabyQM@w0WE?TA8kd=x5V~mZi?9&|)_k+Q1v7$l(OhWq zEF_yoj0VR{BQg00faE^r7E3MMQ9o<;z*3yG3g-i7#5zF9deYL7t?0)x!Ya_zj8(cE z$Ifk*B|B5~MJ0LlTV6X>nps|&ndh%@h)OD)e-+DqwrepgWhv~}W!C=Ngsyc z()}D78{XRPBb}VRzw=9s#Nu}L-?&mKf?GLpU8$Aj)Sujp^j1lmds4q=_q3C&!!8h9 zfvF3q+ol7l|LC;-;gdMHsYtC@C8--Ymh=Cm1^G->$9&3Ch8$qq%aUk5WA=h5r4el? zw{YlblBi_}1(2+sk=pqw$+AFNLtatCOX{D@5Z_YwOZj;rZLBW)Z_@)H7v6m3a4nVT z)U$}B;j~fbbgb+Iu1!X0r|DPf&%~z=CDgez{bG8`BKnIk5B3-Xb+g-X#6+Kp%#`G1 z=+sq+vM}L^a@(@%x$wI)(O<>+&BX1wC!%5Yq~6a;)PqvgfM{I@fhtBjnP&$-BqCZ` zM>n`xB#ge<<_f_|uZvcX)DbT#m9(+L|UC z&I|$Xi{B3rEm%}Q|0whNMSJ#s|KZ2kBjM(WR)f7Yf12vDnO>#rN%i_o>-({au0MB8 zL9-Y%XPh;ezki!$v6#-+5gEA;hVmgB}Kd%Zi4Vf@z!CnR%2YDE+5% zHSN38ln(cqbDqrnQxdXQ4uw~&cXHPT35Li$2EsZA1-rfhGE(895oOBpe7h`f;e7;+ zF>*eULU2j3CS{{;G zRHrl_GB%!ygyJg2$E3%StX$24jxU$HY@Z%K4mgfA=iDS4Q;F0JmS?OzI0ef$6Rh{@ z-ula)z;cq@)Beg^%=qnczsQz109ODprG7Q?!;S9HGo}vbySOFbmK-=)KKz?`=FtVC z%X6PrO*mN}>TQCPNTT)0=#vbh$=hj9C95WSiP@GT)@I+=DXq&ftU;@(x5N z8=J7sb$8(uSbCT&DnMpecL!7~IKq|N{ z1)D0*Bix(EpGih}i!8vq6SAo>d}kw9TF+~@T)N;;o-s6>6M4#0p@Wfnoa51)kI_UU zc%6yov2jORbV|M{ruM?oMyC=6HfTCIEWg8TrQC%H(?1@;Ce_xw580vWLX2=-`m(m(ts5A}DioB%7Y+Tt^oypFxH zYJ(^UEMF=wU-;6F#EWJz4m3O#`CsKSLt8&+j~GcwbmO`*_YYY$nro=V2u9=$f>djF zI%<4E^bUS-R3ZPdnUWoU$o-}EaDBl3=QO?Fg?;m80vcDpczT&a>rj;Q(n@B2UcJee zsEc`7XI+>MaT9Jk$=JHaeVOw}5d2JMSicwT<>$aE$v_*rfK?!QJ_0*#ToiHpe}sb=Iytc2)MYl)&|$ zcyamKv2ITaK6g?+nZ}&=%vI+Z6BN;hGi!>+%a>Z? zmI&{5TEm>uxzg*j8lHeXp1q2h-Exrv5K(le@A-H>Y<*7J!U?jRL$`4k%etSS{BMre z18noI&b{}`n;QGBNX|)I!yK57NQ}s$N5#;)acr~9VuMb+K;jTQVGo5YSKiTjs(PDs zm)gBA!DO?Y(NF(hqd;vJZtOjfBU2Nuw|*} z`>bO(bo^dVS8vmXtXjLVvF`VXCW_RuTri0@rH{NVOch_vs%;^P>q+!wF0&p{6BUpb zMG|eu;3B7xw6O$;4~)S=Srcre{qSt8usSWL?=Ek`A26pj5=!b5vjFJlDe=mexh=+3 zxP6qbIimb24oQ@PLofZ|=_`K8RlKIgTeWsuVLdF|k%l%zMJ+p^e(;FXxW6QD*@8=^ zarrWG$rZ7x+{g7ZG;%GfL!ihlTlvxwR{MW2$8|@P=HifEX*l%O|Br7Lt@Qt&>njdX zl7T}X_~H5aZ)qeF-K2kLJ7He+@>p3lc)wjtd@)CAqn)I_0Y`v-cR8l6t-?Qc8U&#A zaCJEWbgiy5b~lh|RyT3^Ul(f~y+l=K16^Zx;U9K(pD##=fjvf7Ia8==vp1 zzFcq8PVMqVt#O|?P6n|kO_d^!_UX-(92+Bb_8<-%BOV90#+S^Ur-OFT3%oy}GEboncwuW#sAO^U<>cjIZe!(P!rV zdhB;Qu43~x;s3lDb#~j?gT}Hw!li4uKd3Z3zUwauwQs%Y1RaNar%*25q5piXCX$2= zh7e0n65~YcM>?G*_{A^S-~CN+L4b~9@Zc_@BCpC|ou#W9sp}&KkA>hRaoBgwE<=gs zG~lB`oN`2qE^TcxYI>Qk_VHry(!FN-B-nE!uxMR0iSJYW>G#}B0f)M^}3el0%qAHlzlCyCKeC#WU1w=6p z5=_U7Ka81i#(}*n`a#L8 zDsKodqQiEvy5q(m=4JP{X3i<7TYDd9NWP#0Kc4_hFu!_Op)mto>A;NBc!&%+B)E5r zW$*`k@D#E7@D8X*I(if6{cXYLd<^07=(@mE;U937`NEvM;B!mxXy;Vsd5fJw(fHc@qTB91yChr|aerdN%NA@akCkyL=gVrKq$E0kO?C<-O1M+ik6 zenrr9{43h7zXj^a^jjq)K7*X0r#X9zr1ERYt&?=ZEvOAdk3s@xm)>?YbXo}H&Wf=K zxNAGt6KsrF7a@mnPrS^HeT;&J{u2YHRly*tF|VY97>hM+*C_$ax6UM!fA~hqXCqag z_${Li!!vt-L2(+%p`f!kA{OIj{)!tpqkeA%ywqj7=DI9c9;Z9zNr14zO-WV7)7M1F zzfAEr_uG(Ws9@gR71cqEh%n$DRceqWPn(qoiYBSnGZL18}Z64Vd%D={Jz|IYa~ zAptU;M0I4rG)cnkL&+lGS!j+3NXs2RW%NHg2J!dt78}}vR@sagDQedK0I3P z)SYpF!qUNroeYSuuv=(xo|N~rtpUVpO4al+QWU`yuSlLKKZXeTZHUPO@`*}OOTy$S zWT{|!tSX3KF6+$uHQ_IgHh==VP{nTm-7lF}rb0<`+FN5zZtdRh5}3hwW*En(?O*Qw zuSBWCS42z%Z5s^HPL%sV(SjH1LsxRE#miy2FH8z4-2mH+0@;-?owVqiS?&cjuaUl)_*JDrjDYWS{NVDvs?|9UBNCRj92y3aeCLr_VZ(NH7z$4#>7$Ovym> z-rs~2D_v|^ZDm|6j~Q%b+Q|WzwgWm}sK3TbAtnfWXuYstMhWp_jL<{Qz2|D7E9D6; zx)2j*8r+v?0zw-1@w4>O{baIxD9OtkW;J^}AD=kPqO0gUYR-kuXxpz+%S2;HilvcfCp*dE7SCAiu2gRz!mCAr0 zQNryf?jH-@)?pIhFKJMLwD$$EUqXMi@vB9$osg1a=ywV|3OYo(Lt9}>TNX`VvBbG9 zip72!$i#u5;b-U7d!pSY2u~f+vhG~X56$$5=V?I}5t^ld`La1%-7?xHiuD1)KCBos z@#oO%xf1CWARh*#C|F1!c&nwi_QNV30SFyj3!*c@1L#Cvqr0y7>$4m?y!>|*{;XsN zy&|!7FIhPw8X`T>N=H;y0v%@R`+Z*0488(gC7ZlsLR%}jS_d`ZGJ+79VYJy}Kgs+!MrVMIdoG~^9Phkp5tsl^J3I=d>c_j_RDwOVSntU1*`C5c5 zHLE_w&>fnTVr*Tg!TL^=z=&kn7X+YT`XwD0L~!MGM{0R%55T?CX6Jp$CE#~=kj!gH zsYLy2xRcmj8k~>CX$9e$6-T`+nZX420Z!?ss(CfusIqj(r%fMxVEP=Uk<7ddtnJop ztx_MUqzo1go@#r-3tE~NE)WaS`a%jHA{b=-KX(VF&lOp`3{|cNjX!$&_hBoYSdx5s z%f4yL15PHys99VcpnZ!S`lA)Rs$_B|)eSHqC)H9D5S|HhFq>T*>RVh?Drk}E42(|* zM{wdW@C1c^>+s-@cS;N*9M#fZf!8ub`IXSCHw766g93y-7Hq;|O#~$z)wG|0zfpdn zQ@&-$tKI|o{&TuB)b3MoG>7c?XLMJDZ&F1*AzC{W>lX@`-rC|bNQ4NFIFc*sK*>4@ zA<7E+&K|$ry&i6y5@vy%vI63%YyxJjS28&R*psP-9a2UkoFcVpQ&e$o`A}r#1J>x( z?ZlOuTL9$D1|ZOvDjt-UCR_iDFr_Ep_-sQBbtrsTD8QLb1;>%Wc#2$DC*W#yDjxr! z2t}69*YoA6gsn$(4{V?x;b(ZZizh^?9P_7@5MQvH0Y=oP%?16bRhZ>!T?<}Nv5#ge zd0kEsWRR4Z650126Isk0CyU}8bwYx>46d!oc_+o6PK$L!~$EFhX{=yOB}jq??}d5;6@H3h^&C$mBx27B^u4}K^uLZkCx za!(x0SQG~)jSF@R5My90{V1A*es&RUV*!!*qBd?6l*&bRAeVtfWvHn*m^n7_S}(-*SBw^KVivbo9j) zMJ8Z@;b~lmOnrEUdc~48!TpZjPqYw25l>2Wr-&0$T(O0U=fm2a&mHG7Y|A57Sy-f} zb7j~%X34qcZgWI_9AjP1&v-=yg@$sa5Y4pUCKbY9p6#_D25-xrCx~xqnn9d)gS)^1 z@L@Lu(-mKni84ipInkL ze?)lRO96r)w$KYn#~?4OJ?|9sr|4e<(8_+6qE+ad7Sy zCzIlXW0&LIJ!eBwI|unOOefn;#s?fJLO5JZ6AN{?qfG3pO=L zbKMy>vN5|GeCY{Xm1NU(@(>DNeq@91BrWn$Nz4SPDeW_)dNnFpsZJH@uj~BW!|MiU z73~k^D{3=jeq?7=o)hp*N35`0*Zi?D!7PJ}nG%VsA z2Q^;y-AqE;l&TuR_1~mj*;L@0 zH*bT%Jlvzt!rej73K!qLie%~rw`}nd7|z1U3E0l}xdBN0Oxo7M1imB#Z191V;6cG< z$R&HHA|}1~BP76@)yl^FPdMJuzbTS<(~*X4)ftI!0*P0iJ5XWfwKlz@!9F<{yrgWyNd^XwLX z%)CUtV`NT!PlV35VSn=d3OAM?Xd%C9Gcj+3nOnX-J}yhLdvTL!1A(*T!r q4oKo#X%;$!`1cv6L0hdI2dS#Tz+Qab2+{2OyJ%eB<*^71$E+CR0B+|J_!TAOZW>{F2)e8jq7)M`eTvo1!XX`p-+)K-M{Vsa$o=QWj~Gf z!g+pw-koeg#k%?d|>Rs_hkry%!OJl+&@0`Iwdf>(FWJY-pOkbZly|O)4 z&)2PHE6w;~KH1fh9Xa9o4Tg69xX&q%6o&4r+BW>o4njn0n%D;?jqG{)RUErpY>jqf ze{HCj9QIGk`esSvfv|M&@4h6@9?sL3%|vRHIFsvzbo0!OMRw1-Z}p1O6R$+ylu8JG z2YZc^GPkxC?k0`b{pEl6U{M82oVU4gyX`b88dG2lWXIgv-hF92-{^i2fng5rpBYb> zuk|iEs^^+K8p1^y_YWv3cW{-r{eZG`xBnJB``)_!X7Xr5ZqOZ&a$^$hZs#6dL`Xm? z;_2YsDz&X$&6SB`8uG3%Wo5aJvHMUCc99dr8QSQkEKp)msUb^O@@xAA_3!q?aN~!! z1z)5;d#>yUfj&U>NT+Z-U2hlI(8{DV-| zz*kq@W46L?9Gu%|dq7yJ{^W28PZPpA@V<5PuV*Gw!h+vNNjOZkWg8NU-&qsoRm?fK zGTRrnOd$kHrd3I#n<2{Im&E|^@qaF=>wJ=VWuK2(JN3Q?o@;*)sK&bvAfH}egR9sa z3TQztn~<_;Ykego7ZW1iTJ2#nxA>kD2YY-n2uAf~gzV2}Kh0G15)Mjmh_|FNVdy^l zmPQGmT)Ado$JEVqcKW8hXrAwZ@rz_!>Gs?Fl|QEmhS5HD4N780RC-A|ySYB3YH{oF zeDmgx^qL-*9B%FrZe;QC+;iKfO0^@?Fz6w)sipJ(zSg#7sqo2f1W~H1qqCh_S()hpwU0D}!9 z2}U_c0El7BqAmxD(AC@gATD6Mby;T83Cp+H)%C~c6n%@HDDzXlJ3EBcExsn{sDWNO zj+ncA9(IHFLPsp2imuFY9$oJvQDh~s--9k&cYGsb_KRkm*Cc9`!ehy25tUQ*F1bnNx!5X)m3X;cUWoZj3 z5lb>=f@J`I0%5g0Ba=t$V7pkxYMT@p(?sU-)YR#*?wGC9_=Y1)I~!ZAomiHOQVR zPe)NB(~mj_3u>N`y|}GirxCp0yTy-5K`2{NfxfL+2Yz+Gy{69j_61BUE2F>1g0M}J z)6Ud3-4C?5;*0k(H-wSeqgxWqwKAe+pj3?l78)mzb;9=UN~&**8S<oRd8V93Pl8e4llcvA|K<$n}G` zsc-~Ma}*THu%6GXu!7+y1@?DMV08Klj)`(L<-9K{VwBA%47(+$UU(mk$u^1RAc-{!2;+(&Zy``Hd_UdI>dzuxBS7+pHWoWD#Z{{e6es7w6->JZ z=GYMzd8}r^TwI=w#e-1(EmDl9;M;q)QymGq2Eg0SdK#s+C>R)47f&ZJHab^>am?_e zWcZVa(Kl&*rjRe~a!i^Gz{N>G=4o2qa^M~^%jc)O5S4W8ZPmOUhJf@KU>HpZ%w&uy z5r`Uv$d}b8&I3rCnj9n6ZN8O%x$S+%l?XYy3(6NR`8&}G}p*D$O zPgNc?N+e1u38o;7tHZw8&|ZPZIO4ja^J;@$M~Qo{6cK_rjWABY-s_a(^oD^T(d8E` z)>`qWbp_P+>}++x3_AGu7!Moj5jTx=Tnp{h;3l(X%%%wZRU|BCLSM_syBV2e`a9OR z%;eqBLP&j44oEUoq>Yvix<_pb$j*m#N{P0~mS* zdudqT8{-PIm#)HUp9d5||N6cWor#FRC_hIMwg5l+M!rT;#i*iXV00YwUS!z{N4bh9 z`w`tFRNXtlTU_lvTGC*&mCW?~N^v9sjm?xuywrAe@(>|HHEJ8`?=g@pm$n@a>74V6 z^cH#P?ck7`wt3PNy**rzeDm@2#LW&mkjkeHye-rwbe^+*+@*ag&bzRl_~_|z>iR(i zBG1Ai=M#Xt1EvgSx6a}ljR~g@i4%W-_~+3WY$7?Ju1o;zkb~pg6rusZyT5b_-^1cJ zNdZAclcXRS2~3lFgUU)udH0(l^rbVxMOc}>$QK9_m@Z{LrQ}l&ugCQM!lfx`lW}~i zZP1na&_%jJXcem?4(*O{0gBRpRzCCG@M$eR!I{-}!b5?IKcQooxlVBFRU+IK#)8)t zhx+VOB!zcDBb{;0G~~~=r!1d}LXQrX7Bv=T&WzSiJ1*~d=GETZBh1r88~e9p!#=8Q z&K}dW09`y&Rf-Fnc+2+oB{3u=e~=ZS@EILwpfGtg$xphBu0tgZg=JeO$59~%cbzOy zluXhiiNRRs`Mh>z+^Sh$3QU~3TmWK}_fE@=5|+ZzyQ^9y!aGe|Qu^pbWbady%&+-Y z(MuWhbQSYB4N1oXz`sXXG62H(@^&81q~L7gUl zW@z0F&|qu)zA-KA$WlHj#o%UdQOyaviB1LfY{p^Mx<+XXNY(rxhnNlTAA^W@6O<_( z{>FUp(yGQ#oF?jZU=#<<8q$aoBKH05crd4?=kQEAbcPf6mBR)&90*`FhAa^3yTM)N zZdcUG<|577(Hhn>R(4%lj^%M#xD$91v#~wcC8~_k^E+BD?+x`Kb^HP=71RhniIME|$ z{yi#^L~V=a5m$Q_a@+cH6GNqOWkh+&a0>WzO{iT4mVc^Fw*|sEG6y*Zkyl>x;gVHZ zKWH2QFG(R}L9rKH-6#&5|1{CzvaaDZ3Qzq3;KpfIO}mHD=V>8vx~SV4N8A_kE^07x)L z;F^;{yS%00@WJfF zefacL?ELVmiKD*2d0m%K7P|GMQmkTS32#=NNID3lKeEh|_!Wpia5hr;# zu7bum!sWOi{!8=Xrir*o1b4x+;s@Q3f2Q*Lq5VjJp0JmG)+*m=Kzw}?)iz1dmH9r; zea-J+2KuW6+rxRHr!XK2iVv9v5xy4jrQ=_!-LS{dcQ8%@O~`W|TlzEoR3QZiDqNeD zeIGgF(8>?db0UDB&QK6*9BHTe*8KN#E*C<=7W6$(LAbY4lqGbdA~Dx6qcXk)Y7nlO zlvTmVQ4yA{3#_;R%i0t6AO?Lwf6}T>S0&J9NxxG9hFdo<*nlEjH!`?hGocF&`miy` zLD+qwdcafX@sD*P+GAPV<>i%Sgq3$F*)qGuF<;FSyhC1IuQ5Bp2lm2k+aHDe&?dwW zOtq3W#eA*)Tvkn+iUpXZ4O|B`WNxKS;Eg)U=H(Qaq7s-(d%n!oj55d49qSgY>=ukS z4R6qVql;SX)w7H&%&HG{uunf+qo)`RU32*evsGH&Jb=HbyDG|Rxb*vq(JqAL&ue8@N2 zvXLCZTYr=%p}a+!QGWK|uK>h%&tT2p1}y*B)G#c7z2L{SnzOkkttdMAnUhoDtPLQE zya*cN-`!qdpC_8_G6;t*7Dt$v8wHIa2$pqK&*Z~Ns#qQHXKTC;F^oP)ScNEb(kY}; zCoIh(UkTL9SHfO4rydJ@$hcZLl@FTdYs#0nRqur$`CpkyChHcVi9ncHmOj=OMl{Bn zJDU4uO=oP3o>;G>TQsvN*=zij6ogWx*m!G~@Cg&DY#nYBqHgqcocS4t{C>>&oqVZH zY4mND65y1y3-x`@`mR(5%8>Hp{<1o{(km2Naj~=gVs(0f-zu$F%ucsnC2zt}(^XKb4cOKv6G_pfT6Yf<$|gX~WnQtbZ8jP09~e6E9NDs#YR zJZw)foVblpG05#!Byi9}tt7ue9pO#CD^2O^z{PBQ;dTu;3c{OdU1JdU5K6GBsp=L19}I7YXus-vq$`=o#%qe;apASdx5&Y)l!v+NXeU+ zB`F<}zlsNNlO`$zqE_5I5Hu=rz5rAxEN$iRZ=UHidpY$?h={A)k{2gyIlZ5P6P&Y= z(ct3SPI3sWuZ4pD8%kZE62kOx$TJR-=m$l10=JCZxpF!>(Bo|4;grcd70xfpBbd@! z68VnwtS;&c0pX^;t!B~;dV@+L>054rc4^LwT%``Omg82mheB6cNG{;S?}%vQd>K2Y zZnhjmy$(dy419+m_`hN%$q=m^G5mC~8x7rT#4H2kO_p?hW z4SDf)B5gpBtQnxDsnhsvvc1G@CL>{PzpKy#B%%v!l?`sS#kN^&#Ln=D?w^Sff+Dlf zx5OgHjBX)LgsxPM*^8W<+>9m(5*h-XBp%{)ecCBnE&Ei3w9A%5s3IZK5k$rlz&l`9 z|Mo;XtRCrxi7Dy^(#k2)O5l0q>C(y}WG6Ts(%c%Q)3h_G)YxVM9V_jQ4(aWcgdx`G zhKH1fYoh||?C{Df<(3(Bkd+bFRP6;R$Q{1ereawqgFx{?oDd;6r-9dJ?(q=OIyfUG630Dp0K-MM<~Fo)By%;lOBFn# zxHd^Bz%IvK_95u@qpNpY+4*4+LfP@5ToUz1Wn%c_Gf0Z?pu*vJ6Fme)7CZ-z;m@$q zf*7&-6n(IWWnsa1U`WQ$KqVs|#dv{n>202#{ue78XRIB0@V9(MDAcGt|)iG zOD@XC+dGW+PP8GCC`V+L?6>g@VkR!mo4&NcwK{7yMvqsGItwc2cG0b9F?~?9vSeRy zX4OEai~8vKUsi?0z_M|{C6R~Pfh1$NgObL++|PDvh@=uvQ*UJ=V>;vKR4$j*wblsU zBd<^Y9{*#O@#`yM%s+dcoF(Dwg3GN-s+=3ka|OyZcT>4pcu9ExMUfE}?Hv=T$Z9Cl z&0#S?L)T38Iw zdFsY+kjF)Jm_{#O2|=KgS*+BjnRh?UT*Tlji%RaFQ=6AE(JqBAa9qmp8u{&9iM~O_ z@Zw$wk_D8#v{4wnJNKm4n$fjgr`hC)T0LELg6X(`glJ3p(KO*1!v-4Uuq_dTbKI&z zmZ+gdiFo=1s9`3NIqU>34KN*kX~mO9XAd4ZG9lC=rES*|u(YIU?3B)tJRf)+Y&pqx zx8Y{;U2B|w4L(`z=ecV-yO*+)LFpaMX0P_pcY=r;Sfg+tMD_(J7_%sxKRiI|+@|Ca zELgOzW6o*;0uoVY<=k(Jk}xT)2vfC_k{pwgoXX0X=PX(?qCI6(hM-pYs&5EJ&YAGr zGgFD5;<2Jsf3oA(?i+b8l4W$`x$O!!(y!b`gOyBqcq8yGU`hkr76&}+cb@EAVK_@>Ky0-l&4K1=0my8ZEeE)Jq1<1Khz=Pp)P%&=D*vddYA zo9$ix+_K);Ff3voa%-fe!1`)k&^fON1VeOkW}NAqmdZQnbHLnlSD6UwU6d4bq3Qu2vSk7=YvAe|GL;5y8&&x<=QvBS>&jb5 zI`)PbZ?)G%I@Msf1(b!+Qx2d%ecx9k;I&ML$E?AA6qdPWpjFUHOE|76c6MHIhFxzBr z^OPI+HHwPwDNjA>LHj;$Clh_rjy#YJ{o39B$;580zdD_+RWRXW*l}DS5|wmSw^{bw zkXuQP*}gAKSg%HjOwkXwl2VbFur;&%`9uH+2Z!`)5OJ0!P)>)06*xr#f7R`x0~{x* zRu24D0uEIK9zDn*nQvx$)PIz1851d za*%h~$*cWpRSpzZ3M4#)4f%&Uh5bOX)Bne`w-n&}43K4PXe))C88~;t4P>lfFjY0M zc974eYLY^g(@^E8Z!4`<$;T$gsi4X_Ag-wg%KEdVTncslNABW~`Qp)}sArg;>bdVm zrJq984_esxN1r(|(v>)-|HiGuo}mL^HDZ1StP2e!w0F%tyFP2q1><@l%3pvPEQKOJ zQ@=@?S=-;V?O*>9^Piru(?U|6?0P?BLZOnxwG;!q9D`Vm@l2_hcf(}5V=)MLDd@{} zn);G3&7-WhFIUptBdVR^`5J2BnGn2^fSd``fyTm;ea1XBCQHo)q*r}7vTl5vr&uRn z*}FW2BZa4ZF~QXWQX3XG+%v_WRQCEK)Ac+|c{zT|1ct`L@lq*-v9*G@ebS!x|BS+33L?v9d#!gNYC0z^(quX~XMh!xWfgSouW@IQ1g+3Gw($c| z_k=7;=u8b4Ove|7ZZpFaI-KPfP`7+yYBo36&Bc#2;FYwc4e`)O94$+lozo$E;ToBSBR$S*L@Fs#JGPBPWwhkkEjUpvb@)TvEI#Xs!}PDmrN^$+l;Ab+aF2*Z!hvbH zor|Gr7r^aG4BkU}yr}T9L)k7;4rx;R2ba^eRhlRt@6)qS^^Zu^%3*|xVQYHRzcAGc zjXue1PYNX+kC{2ZcSM|>kY>s9Wy9wKlfrA4B)jf&+z;)i`iRfA2tC7k_T>D4H_%@l zgJc=iYyf8~K^L*iQm2lSGtbpHF`fIh_w_OtffkSKRh42*33`%4M-jHQhB3S9Ia<)a zxG$fjh~guA-QB3lx8c{MbsAjKG>0 zler88u$9Y|{~?ndKmK8;j@SMtIi|{*>f!AA;9N3+T-Mjkl=FoDhY^D@H=<%S{f~tF zAD;+~)mp9?%p0t!UNC9ekFEIMIRj`TRSX9F9~`UgKWD5MOnI{9OMV(lWrE3A%z4q} zOOU`DX;Rt$sgpMUn=<99>HnmGz^HiU3?zFx?}ROziDR-|2U=~WMALb-yYX}pVV9u) zDO=#FA#G~Ktk(SA$t%Unc{Xk*Q!PIvt+e~E-cj5uroF@a8t)928iA3XKI)q4EXY*q#luimMl-gew3VD${ zC$BH5i5x?LcMW~hsIt|`R;Tlk{WKB{ddF@_>a2}_%B=0LRIh)acP?hD;D_v<9bRW* zyL0<1hj(coPOMfzmn-%K**Nw;G4APBh`aRL-hgSo2!i0{0jhYO(6O_d9ukj0cN4qh zlvjU@;CRN&0{`=CXSC6-sYIDFy_Quog4Gmf55}PjA3%d zmL~4obK$~+&-p)%&h%x=l$H|zI_u|l^j+t|Xn#6e$LgHU3wn(U`EB$HS_8LlXr zEo4jjA||>*nYhsWK&C%!ON%7J&lVl`SMF{W$kRe2yQ(CUwMxct9bQMhR$|@eDJ=RA z^BWvykNUlg1$Hy({dIf1mY#OYt77>jx;m*@Ot&-Z(VO$y7LwJ8^c6b!G+H@e>E_L@ zt*mlBMw+dGzOjCz-ngq%SjqqA274x~@cg=dKfhrC8y1uc`=1ed{jN-5C4QKn5W5GK zZ}I2_ra*i`O4wq6%1yiMeC;5&?mob*t_bs*aksuctEy#YI#d6#1ZIx3}dGy`sX5Mx`$TGp=V(}XnvglTqAl?b>h#^SIDMvVJDn=l+blB9SIWiTmJzjOeOG5^Z*SbjSk%qst{4Lj+TBZB;yNOF#92ceEJXh2k;HjY>a|ozN8{d?F{e082)239f(+W~8-v z5U@=GNv#8ma!~B^AmY;nP%ht5J5^YEd1OQGw~!wC{P6Gqe3-ana%<&u`taxX%e7hV zM7Ie>uh%`Ff|RH*yV zR;lWG>!WL?+P542dH(Lp&|mPxC+}Lf_N;)}?%&SeXCmUiKnYRb{GRLlt>$NI+_O8+ z-nIN*so$4R*NZtey>3^p-wyD=-Jd6e)5GiAqPIU6538S%JIl|@p|P*6Zs_djr`?m4 zkCD5RYw5GodN8Hm_tS2>U*Pv&-Hn^@qwBlDX#KstoFCT@H@=>`-dxzSt_dvtuqcnw`h)E|??^Lkqq_5PWAK{myJy5t zloD3CP`rKHOmOS?5;!TPTG|sr#kifH%+y;`HOC*FC=NcPma(F4hj~byc2~~!Gzlxj z`banSw)P*OzhcEZ+Qek@pk^0hR(uflwGkV=U<(Ma+i3UZl;HbmI@0u7g2JCJuu~!K zNKvqP&;&`0k#i^^1l7cGQ5w$-1J;pD7qqM$&hk_9q|Zr7VTw{^)-z611R}$i+5Y`7 zQDcq4vJ`_cjX_&j%AInM*V&?6rf4Se3eii@X$vZd$|o7s^gSTwvM?nb3pbT+Hkc59 z*R<MS@K7tnusbCYBG1ky9cPlx%j;A!BjQ zfuQXKMn%M?eyW#Sd7U5JQw6hGbf^bt;Y3@i&EzH|ujJ$!2~)L)>@|&ni@c~UAX(mT zp*}-$O-CYIsI=J>r4bN%FtBS`y2nBB2lvEnkb#-yq+A9&m7#>x4-uPi@*-XJz;u#a zVNOMqQb6*qb4q8Q*{}Q}and9HgwPr1p)#lmu)3x-F-!_-rIQidgJ&wMD9r|%(D)sGQ+g2G3d{ zL!#sw5obcQoY-U%3c%|cic2XTuZWs{56uSr0Bd*yb|bw$!%;JMy5I%;>K&0S447$= ziVxQ+qA*DLBm_Bg;8k_!>>sgVFz6&lF@K<@`4QXLY`7)56F>iP#u{APFCl&cJ!?X2 zn*xrR-z|_o_UOkiwNmiS$1njtGWnDDnyZXJ|6Rwi3~*z6v9_l+VUSVqXKHWS)~iYA z2Ol>=4@tmdMmb1t@R*-z^wzE(hf8bP8J2u&BxL7E8`4sdWDF_A&axyloAj& zAJSP-djQ>0o5~1pTreTC0W1L1ny_7_Cw^!^nfhriBHpCvK>LwG2ZT^!!F=6)D<48v zf=+LUC*)ZKPpvL^3k%C7jJnn+OG^kX!Se1+O-n!GAEG9mFw34cM3q)YkxUh{8Oy7R zzQF!hjhm6*Cjq396adm=;ZTgkQA$=Q^4QF*&Sct&F$sk>mwPODY0h@%6n=?NrXR}K zVshMyR4^A^35u2-EK%Ji+K9!X$b7_3LJ3Lu7cj{4B}f`*f;NDOB--D1DJFW~>nPx* zvgwTqO2-z+^mS6EHwbR-`N7E9#e##pNQO}k%{K7-65&ODyk77!O(8js@;Azt(NPGM zkL+&khnzQ42go^OgJ<#Rnoh_Mt+?86a?ECtT&V8k4#SvUC|$S6{~3x;o+mMzx;5N$ zI>GN|6=H$H=b2?uCqYHntK1pdaCySZ0y7CZ$l4rm^aI-jew21fR^Y=m7F8*2#DJcZ z$%{UnI0z?;e_5Q1T3D1*YsPpWarJlzSo)W6X^M-!EX1>?uKvqvlSENMk#V=f&W$tY zj$9tg^PmDjl{k*R7UtIB+_pO=JQW~2L5>MlmsJDA0cUsz$%`(g%VK9*!lX`Rkf_~M zeF9UW4bS8xfA}e*YGlJxim)e5&uKR3kC;WC(+2TSDaoNVatW7KRdD|VKiFHq`hj(9 z$iM3xWEV&j(vx09f2SFN2bJqthe&k1y?)Uv27z`S3ki~{W16QVfmmo%UxY?Lv?(4K zIVQyGrzH9WNN?CtaYQ@eUQ+EtV&*=XoG!uj_(1BRQWalh%x9-xHj$;XOLb zc@UhCOO6ZyS|P#aK!-KHSMD?>)emIn`Nn^?k^oHd5vnFhSo1XUIMmVJB$|jn=ePpA zm*p=+pk!kf^d4r#qxOjwyY`So$fMOj^PHCcWE<^6G>L0<ajJa6D0VLN^j1iQc{r< zX>_s9Bv_zGK!hzbI|c_)9V9z{YbvxN;tVQF=A%ZX4>OVFJDXh%b{PFcnq)(bj(a!=<^0G^4>D1ca}=5XjHp)KN?u)Cz5x&ZtkhUN&{469PS z%hirbzsV5ZRn0V7rCJawZKW!WXEhdnBwQSYODt`tIwu&RpA|aRr$RhHOSwYNJ1<2E zVD&Po6YjsaUV#B-y)ls-m#TD?I?}6_bK$Rm#HC!D-*;u)eH85h4%)d0IMl|5rNvyN zJJ_aA4eHm=H7&GK%mEu|Z*~ys{2YZ~v zYQ*y*0SOjL6{L;dS@_Y)-u@sS&`stEdI#d|}D0_|7fQ{3+x^8%!I1xN$-yIONWd4 zwltOstBP-ycQ|JP)3X9@*RiU#x~&22inLa8?~@u%c2t{@d+DKSEw1ya-ZA}-ifrL;72Ao47Nup~@vC3DF5w=BaTcE#^K5p7Hz$E1$^eImC&vAV)EVZvyU zL_GYIkP7LEQ6dwTYipnUrNPwti0k;NClTy-X?^t{WVHi< zOjjqSq^Uum6Z}|zqi1`C9IzQqcd1qqh&mHQ0s1|Ie^vfR3AZ7j4D+7XA~`Q8tmg?Y zkTc|9$czsZ5R1rXQa`|9US=g++2?dWADo^yaQLO|6FHvx*?=On^^dES2b zh+~q)Q-erlO`>^r@+Kf9t?8g!K>@O#g}|sQ z5P+1#H<#oBX|e>dxC9pEP|P(*E1&}wcdujvg4e{4@x+9E5=b}Tu`3Sjsi6n5jXU0z zM98?nyf+fkK9Z;e9Uu5wW5pk=(Vhx|hWbZmaR~Fw^u)4q3C2_t1qJ!)H|5$FaOL z=!_!$sp-E+SqFXQ#o*E3qt57lsxV{yOPhs7KQcc~js&472JO1;Lq3T*@paMcAR7E? zTL_@p60O)GH#x4rf~PIl0Qp!||Guz~AC=E!SL&Q015m@mgIfI&k8ac;Umvr+p=U;K zo(SosSL`~*cJN+zg|I(Fl}*bHByoorAn^7^UU+o|xb_{9Fqeb_-LifZb8fe|%#}y) zPZ{`Th|=xSjPrD_>Esow(-39cTFiOdr{i$>-CD#e?~dvMGP`QUEJHdpBE1GB00AzQ$cOuCvu~EJQEF3sPeSt zBwjx{jcVE5)fM2DQtD&+>yu)T(_baS=>IDYf1~njOE)vqy=zN%OXpd4)(hBr^r@;- I2n6u|0F9u9%m4rY diff --git a/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz index bd869cfa5836cddd10010afefa33cc000fefb44d..4f36c7c0cf162f8d257f9102dccc333eadb395f9 100644 GIT binary patch literal 13369 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 literal 13068 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 S1!RhMdwEqJXsBA?U;hWbLWzX{ diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index 2d6fc947a75..d28442b7140 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "os" - "path" "path/filepath" "regexp" "sort" @@ -26,7 +25,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/testlogger" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,7 +53,7 @@ func availableVersions() ([]string, error) { return nil, err } defer migrationsDir.Close() - versionRE, err := regexp.Compile("gitea-v(?P.+)" + regexp.QuoteMeta("."+setting.Database.Type.String()+".sql.gz")) + versionRE, err := regexp.Compile("gitea-v(?P.+)" + regexp.QuoteMeta("."+string(setting.Database.Type)+".sql.gz")) if err != nil { return nil, err } @@ -64,7 +62,7 @@ func availableVersions() ([]string, error) { if err != nil { return nil, err } - versions := []string{} + var versions []string for _, filename := range filenames { if versionRE.MatchString(filename) { substrings := versionRE.FindStringSubmatch(filename) @@ -76,11 +74,8 @@ func availableVersions() ([]string, error) { } func readSQLFromFile(version string) (string, error) { - filename := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests/integration/migration-test", fmt.Sprintf("gitea-v%s.%s.sql.gz", version, setting.Database.Type)) - - if _, err := os.Stat(filename); os.IsNotExist(err) { - return "", nil - } + filename := fmt.Sprintf("tests/integration/migration-test/gitea-v%s.%s.sql.gz", version, setting.Database.Type) + filename = filepath.Join(setting.GetGiteaTestSourceRoot(), filename) file, err := os.Open(filename) if err != nil { @@ -106,134 +101,51 @@ func restoreOldDB(t *testing.T, version string) { require.NoError(t, err) require.NotEmpty(t, data, "No data found for %s version: %s", setting.Database.Type, version) - switch { - case setting.Database.Type.IsSQLite3(): - util.Remove(setting.Database.Path) - err := os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - assert.NoError(t, err) + cleanup, err := unittest.ResetTestDatabase() + require.NoError(t, err) + _ = cleanup // no clean up yet (not needed at the moment) - db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", setting.Database.Path, setting.Database.Timeout)) - assert.NoError(t, err) - defer db.Close() + connOpts := db.GlobalConnOptions() - _, err = db.Exec(data) - assert.NoError(t, err) - db.Close() - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name) - assert.NoError(t, err) - - _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name) - assert.NoError(t, err) - db.Close() - - db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?multiStatements=true", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec(data) - assert.NoError(t, err) - db.Close() - - case setting.Database.Type.IsPostgreSQL(): - var db *sql.DB - var err error - if setting.Database.Host[0] == '/' { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.SSLMode, setting.Database.Host)) - assert.NoError(t, err) - } else { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - assert.NoError(t, err) + if !connOpts.Type.IsMSSQL() { + if connOpts.Type.IsMySQL() { + connOpts.Database += "?multiStatements=true" } - defer db.Close() + driver, connStr, err := db.ConnStr(connOpts) + require.NoError(t, err) - _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name) - assert.NoError(t, err) + sqlDB, err := sql.Open(driver, connStr) + require.NoError(t, err) + defer sqlDB.Close() - _, err = db.Exec("CREATE DATABASE " + setting.Database.Name) - assert.NoError(t, err) - db.Close() - - // Check if we need to setup a specific schema - if len(setting.Database.Schema) != 0 { - if setting.Database.Host[0] == '/' { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host)) - } else { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - } - require.NoError(t, err) - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - require.NoError(t, err) - require.NotEmpty(t, schrows) - - if !schrows.Next() { - // Create and setup a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - assert.NoError(t, err) - } - schrows.Close() - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - assert.NoError(t, err) - - db.Close() - } - - if setting.Database.Host[0] == '/' { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host)) - } else { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - } - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec(data) - assert.NoError(t, err) - db.Close() - - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec("DROP DATABASE IF EXISTS [gitea]") - assert.NoError(t, err) - - statements := strings.Split(data, "\nGO\n") - for _, statement := range statements { - if len(statement) > 5 && statement[:5] == "USE [" { - dbname := statement[5 : len(statement)-1] - db.Close() - db, err = sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, dbname, setting.Database.User, setting.Database.Passwd)) - assert.NoError(t, err) - defer db.Close() - } - _, err = db.Exec(statement) - assert.NoError(t, err, "Failure whilst running: %s\nError: %v", statement, err) - } - db.Close() - default: - assert.Failf(t, "unsupported database type", "setting.Database.Type=%v", setting.Database.Type) + _, err = sqlDB.Exec(data) + require.NoError(t, err) + return } + + // MSSQL is special. the test fixture will create the [testgitea] database again, so drop it ahead if it exists + driver, connStr, err := db.ConnStrDefaultDatabase(connOpts) + require.NoError(t, err) + sqlDB, err := sql.Open(driver, connStr) + require.NoError(t, err) + + _, err = sqlDB.Exec("DROP DATABASE IF EXISTS [testgitea]") + require.NoError(t, err, "drop existing database testgitea") + + for statement := range strings.SplitSeq(data, "\nGO\n") { + if useStmtAfter, ok := strings.CutPrefix(statement, "USE ["); ok { + _ = sqlDB.Close() + dbname := strings.TrimSuffix(useStmtAfter, "]") // extract the database name from "USE [dbname]" + connOpts.Database = dbname + driver, connStr, err := db.ConnStr(connOpts) + require.NoError(t, err) + sqlDB, err = sql.Open(driver, connStr) + require.NoError(t, err) + } + _, err = sqlDB.Exec(statement) + require.NoError(t, err, "SQL Exec failed when running: %s\nError: %v", statement, err) + } + _ = sqlDB.Close() } func wrappedMigrate(ctx context.Context, x *xorm.Engine) error { diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 03393c620b9..95a1df283fa 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -4,7 +4,7 @@ RUN_MODE = prod [database] DB_TYPE = sqlite3 -PATH = gitea.db +PATH = gitea-test.db [indexer] REPO_INDEXER_ENABLED = true diff --git a/tests/test_utils.go b/tests/test_utils.go index f16754b0562..11e5ac24344 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -4,10 +4,7 @@ package tests import ( - "database/sql" - "fmt" "path/filepath" - "strings" "testing" "code.gitea.io/gitea/models/db" @@ -40,97 +37,14 @@ func InitIntegrationTest() error { } setting.LoadDBSetting() - if err := storage.Init(); err != nil { + cleanupDb, err := unittest.ResetTestDatabase() + if err != nil { return err } + _ = cleanupDb // no clean up yet (not really needed at the moment) - switch { - case setting.Database.Type.IsMySQL(): - { - connType := util.Iif(strings.HasPrefix(setting.Database.Host, "/"), "unix", "tcp") - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s(%s)/", - setting.Database.User, setting.Database.Passwd, connType, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - } - case setting.Database.Type.IsPostgreSQL(): - openPostgreSQL := func() (*sql.DB, error) { - if strings.HasPrefix(setting.Database.Host, "/") { - return sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host)) - } - return sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - } - - // create database - { - db, err := openPostgreSQL() - if err != nil { - return err - } - defer db.Close() - dbRows, err := db.Query(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname = '%s'", setting.Database.Name)) - if err != nil { - return err - } - defer dbRows.Close() - - if !dbRows.Next() { - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - } - // Check if we need to set up a specific schema - if setting.Database.Schema == "" { - break - } - db.Close() - } - - // create schema - { - db, err := openPostgreSQL() - if err != nil { - return err - } - defer db.Close() - - schemaRows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schemaRows.Close() - - if !schemaRows.Next() { - // Create and set up a DB schema - if _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema); err != nil { - return err - } - } - } - - case setting.Database.Type.IsMSSQL(): - { - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - if _, err = db.Exec(fmt.Sprintf("If(db_id(N'%s') IS NULL) BEGIN CREATE DATABASE %s; END;", setting.Database.Name, setting.Database.Name)); err != nil { - return err - } - } - case setting.Database.Type.IsSQLite3(): - default: - return fmt.Errorf("unsupported database type: %s", setting.Database.Type) + if err := storage.Init(); err != nil { + return err } routers.InitWebInstalled(graceful.GetManager().HammerContext())