From 8cfcef32c613a5b4a97e5f77eac17e5074ea2cda Mon Sep 17 00:00:00 2001 From: Morgan Peyre Date: Wed, 22 Apr 2026 22:58:59 +0200 Subject: [PATCH] Fix cmd tests by mocking builtin paths (#37369) After 07ada3666b, PrepareConsoleLoggerLevel can fail in tests when InstallLock is true, due to the incorrect config file is loaded. This PR fixes cmd test setup by mocking builtin paths Fixes #37368 --------- Co-authored-by: Morgan PEYRE Co-authored-by: wxiaoguang --- cmd/admin_user_change_password_test.go | 5 +- cmd/cert_test.go | 2 + cmd/cmd.go | 2 +- cmd/main_test.go | 4 +- models/migrations/base/tests.go | 3 +- models/unittest/testdb.go | 38 +------ modules/setting/path.go | 6 + modules/setting/testenv.go | 104 ++++++++++++------ .../migration-test/migration_test.go | 2 +- tests/test_utils.go | 11 +- 10 files changed, 101 insertions(+), 76 deletions(-) diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go index 902632f3e49..cf497517f74 100644 --- a/cmd/admin_user_change_password_test.go +++ b/cmd/admin_user_change_password_test.go @@ -4,6 +4,7 @@ package cmd import ( + "io" "testing" "code.gitea.io/gitea/models/db" @@ -82,7 +83,9 @@ func TestChangePasswordCommand(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := microcmdUserChangePassword().Run(ctx, tc.args) + cmd := microcmdUserChangePassword() + cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard + err := cmd.Run(ctx, tc.args) require.Error(t, err) require.Contains(t, err.Error(), tc.expectedErr) }) diff --git a/cmd/cert_test.go b/cmd/cert_test.go index 4242d8915b3..c5775e52048 100644 --- a/cmd/cert_test.go +++ b/cmd/cert_test.go @@ -4,6 +4,7 @@ package cmd import ( + "io" "path/filepath" "testing" @@ -107,6 +108,7 @@ func TestCertCommandFailures(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { app := cmdCert() + app.Writer, app.ErrWriter = io.Discard, io.Discard tempDir := t.TempDir() certFile := filepath.Join(tempDir, "cert.pem") diff --git a/cmd/cmd.go b/cmd/cmd.go index 25e90a16950..9d70b057015 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -124,7 +124,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl if setting.InstallLock { // During config loading, there might also be logs (for example: deprecation warnings). // It must make sure that console logger is set up before config is loaded. - log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.") + log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it. CustomConf=%s", setting.CustomConf) return nil, errors.New("console logger must be setup before config is loaded") } level := defaultLevel diff --git a/cmd/main_test.go b/cmd/main_test.go index b1f6bb3ba9b..3cd2c984e64 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -62,9 +62,10 @@ func runTestApp(app *cli.Command, args ...string) (runResult, error) { } func TestCliCmd(t *testing.T) { - defaultWorkPath := filepath.Dir(setting.AppPath) + defaultWorkPath := filepath.FromSlash("/tmp/mocked-work-path") defaultCustomPath := filepath.Join(defaultWorkPath, "custom") defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") + defer setting.MockBuiltinPaths(defaultWorkPath, "", "")() cli.CommandHelpTemplate = "(command help template)" cli.RootCommandHelpTemplate = "(app help template)" @@ -157,7 +158,6 @@ func TestCliCmd(t *testing.T) { for _, c := range cases { t.Run(c.cmd, func(t *testing.T) { - defer test.MockVariableValue(&setting.InstallLock, false)() app := newTestApp(cli.Command{ Action: func(ctx context.Context, cmd *cli.Command) error { _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 17ea951b5a6..117975928ca 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -202,16 +202,15 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table func mainTest(m *testing.M) int { testlogger.Init() + setting.SetupGiteaTestEnv() tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") if err != nil { testlogger.Panicf("Unable to create temporary data path %v\n", err) } defer cleanup() - setting.AppDataPath = tmpDataPath - unittest.InitSettingsForTesting() if err = git.InitFull(); err != nil { testlogger.Panicf("Unable to InitFull: %v\n", err) } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 63c9a3a9994..0c1458e2ce5 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -13,10 +13,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/system" - "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/storage" @@ -29,37 +27,6 @@ import ( "xorm.io/xorm/names" ) -// InitSettingsForTesting initializes config provider and load common settings for tests -func InitSettingsForTesting() { - setting.SetupGiteaTestEnv() - - log.OsExiter = func(code int) { - if code != 0 { - // non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details - panic(fmt.Errorf("non-zero exit code during testing: %d", code)) - } - os.Exit(0) - } - if setting.CustomConf == "" { - setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") - _ = os.Remove(setting.CustomConf) - } - - // init paths and config system for testing - getTestEnv := func(key string) string { - return "" - } - setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf}) - - if err := setting.PrepareAppDataPath(); err != nil { - log.Fatal("Can not prepare APP_DATA_PATH: %v", err) - } - // register the dummy hash algorithm function used in the test fixtures - _ = hash.Register("dummy", hash.NewDummyHasher) - - setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") -} - // TestOptions represents test options type TestOptions struct { FixtureFiles []string @@ -75,11 +42,12 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) { func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) - InitSettingsForTesting() + setting.SetupGiteaTestEnv() giteaRoot := setting.GetGiteaTestSourceRoot() fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} if err := CreateTestEngine(fixturesOpts); err != nil { - testlogger.Panicf("Error creating test engine: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "Error creating test database engine: %v\n", err) + os.Exit(1) } setting.AppURL = "https://try.gitea.io/" diff --git a/modules/setting/path.go b/modules/setting/path.go index f51457a620e..45c6759a73e 100644 --- a/modules/setting/path.go +++ b/modules/setting/path.go @@ -198,6 +198,12 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP CustomConf = tmpCustomConf.Value } +func MockBuiltinPaths(workPath, customPath, customConf string) func() { + oldApp, oldCustom, oldConf := appWorkPathBuiltin, customPathBuiltin, customConfBuiltin + appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = workPath, customPath, customConf + return func() { appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = oldApp, oldCustom, oldConf } +} + // AppDataTempDir returns a managed temporary directory for the application data. // Using empty sub will get the managed base temp directory, and it's safe to delete it. // Gitea only creates subdirectories under it, but not the APP_TEMP_PATH directory itself. diff --git a/modules/setting/testenv.go b/modules/setting/testenv.go index 853521c328a..27bf72e860a 100644 --- a/modules/setting/testenv.go +++ b/modules/setting/testenv.go @@ -10,6 +10,8 @@ import ( "runtime" "strings" + "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -25,48 +27,84 @@ func SetupGiteaTestEnv() { } IsInTesting = true - giteaRoot := os.Getenv("GITEA_TEST_ROOT") - if giteaRoot == "" { - _, filename, _, _ := runtime.Caller(0) - giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) - fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") - if _, err := os.Stat(fixturesDir); err != nil { - panic("in gitea source code directory, fixtures directory not found: " + fixturesDir) + + log.OsExiter = func(code int) { + if code != 0 { + // non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details + panic(fmt.Errorf("non-zero exit code during testing: %d", code)) + } + os.Exit(0) + } + + initGiteaRoot := func() string { + giteaRoot := os.Getenv("GITEA_TEST_ROOT") + if giteaRoot == "" { + _, filename, _, _ := runtime.Caller(0) + giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) + fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") + if _, err := os.Stat(fixturesDir); err != nil { + panic("in gitea source code directory, fixtures directory not found: " + fixturesDir) + } + } + giteaTestSourceRoot = &giteaRoot + return giteaRoot + } + + initGiteaPaths := func() { + appWorkPathBuiltin = *giteaTestSourceRoot + AppWorkPath = appWorkPathBuiltin + AppPath = filepath.Join(AppWorkPath, "gitea") + util.Iif(IsWindows, ".exe", "") + StaticRootPath = AppWorkPath // need to load assets (options, public) from the source code directory for testing + } + + initGiteaConf := func() string { + // giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF" + giteaConf := os.Getenv("GITEA_TEST_CONF") + if giteaConf == "" { + // if no GITEA_TEST_CONF, then it is in unit test, use a temp (non-existing / empty) config file + giteaConf = "custom/conf/app-test-tmp.ini" + customConfBuiltin = filepath.Join(AppWorkPath, giteaConf) + CustomConf = customConfBuiltin + _ = os.Remove(CustomConf) + } else { + // CustomConf must be absolute path to make tests pass, + CustomConf = filepath.Join(AppWorkPath, giteaConf) + } + return giteaConf + } + + cleanUpEnv := func() { + // also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones) + UnsetUnnecessaryEnvVars() + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) { + k, _, _ := strings.Cut(env, "=") + _ = os.Unsetenv(k) + } } } - appWorkPathBuiltin = giteaRoot - AppWorkPath = giteaRoot - AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "") - StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing + initWorkPathAndConfig := func() { + // init paths and config system for testing + getTestEnv := func(key string) string { return "" } + InitWorkPathAndCommonConfig(getTestEnv, ArgWorkPathAndCustomConf{CustomConf: CustomConf}) - // giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF" - giteaConf := os.Getenv("GITEA_TEST_CONF") - if giteaConf == "" { - // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. - // It's easier for developers to debug bugs step by step with a debugger. - // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. - giteaConf = "tests/sqlite.ini" - _, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_CONF not set - defaulting to %s\n", giteaConf) - if !EnableSQLite3 { - _, _ = fmt.Fprintf(os.Stderr, "sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n") - os.Exit(1) + if err := PrepareAppDataPath(); err != nil { + log.Fatal("Can not prepare APP_DATA_PATH: %v", err) } - } - // CustomConf must be absolute path to make tests pass, - CustomConf = filepath.Join(AppWorkPath, giteaConf) - // also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones) - UnsetUnnecessaryEnvVars() - for _, env := range os.Environ() { - if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) { - k, _, _ := strings.Cut(env, "=") - _ = os.Unsetenv(k) - } + // register the dummy hash algorithm function used in the test fixtures + _ = hash.Register("dummy", hash.NewDummyHasher) + PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") } + giteaRoot := initGiteaRoot() + initGiteaPaths() + giteaConf := initGiteaConf() + cleanUpEnv() + initWorkPathAndConfig() + // TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future _ = os.Setenv("GITEA_ROOT", giteaRoot) _ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts - giteaTestSourceRoot = &giteaRoot } diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index 5d240263801..1e1dc7bf142 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -37,7 +37,7 @@ var currentEngine *xorm.Engine func initMigrationTest(t *testing.T) func() { testlogger.Init() - unittest.InitSettingsForTesting() + setting.SetupGiteaTestEnv() assert.NotEmpty(t, setting.RepoRootPath) assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) diff --git a/tests/test_utils.go b/tests/test_utils.go index 34645e5370b..b724a30610a 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -6,6 +6,7 @@ package tests import ( "database/sql" "fmt" + "os" "path/filepath" "testing" @@ -26,7 +27,15 @@ import ( func InitTest() { testlogger.Init() - unittest.InitSettingsForTesting() + if os.Getenv("GITEA_TEST_CONF") == "" { + // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. + // It's easier for developers to debug bugs step by step with a debugger. + // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. + giteaConf := "tests/sqlite.ini" + _ = os.Setenv("GITEA_TEST_CONF", giteaConf) + _, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_CONF not set - defaulting to %s\n", giteaConf) + } + setting.SetupGiteaTestEnv() setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" if err := git.InitFull(); err != nil {