Files
silverwind 1031e34987 test: run integration tests on random ports and speed up slow tests (#39842)
Integration test processes now get their own work directory, config file
and random ports, so several runs can execute in parallel on one
machine. The slowest tests also lose fixed sleeps and needlessly
expensive setup, with their assertions kept.

- Batch PostgreSQL sequence resets, the main per-test fixture cost there
- Cheap hashing params in hash and OAuth2 fixture tests, full-cost
vectors stay
- Single fetch for negative Actions runner assertions instead of 1s
polls
- Queue flushes and backdated timestamps instead of sleeps
- In-memory SQLite test journal, tmpfs for PostgreSQL in CI
- E2E instance writes SSH keys into its work directory instead of
`~/.ssh/authorized_keys`
2026-10-18 12:56:04 +00:00

197 lines
6.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"gitea.dev/modules/auth/password/hash"
"gitea.dev/modules/consts"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
"github.com/kballard/go-shellquote"
)
var giteaTestSourceRoot *string // intentionally use a pointer to make sure the uninitialized access panics
func GetGiteaTestSourceRoot() string {
return *giteaTestSourceRoot
}
func detectGiteaTestRoot() string {
_, 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)
}
return giteaRoot
}
func SetupGiteaTestEnv() {
if giteaTestSourceRoot != nil {
return // already initialized
}
IsInTesting = true
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.
// * If the "log.Fatal" is abused in tests, should fix.
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 == "" {
giteaRoot = detectGiteaTestRoot()
}
giteaTestSourceRoot = &giteaRoot
return giteaRoot
}
giteaRoot := initGiteaRoot()
initGiteaPaths := func() {
// need to load assets (options, public) from the source code directory for testing
StaticRootPath = giteaRoot
// during testing, the AppPath must point to the pre-built Gitea binary in the source root
// it needs to be called by git hooks
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(consts.IsWindows, ".exe", "")
}
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
// do not really use such config file, the test can run concurrently, using the same config file will cause data-race between tests
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.
// At the moment, GITEA_TEST_CONF is always in Gitea's source root
CustomConf = filepath.Join(giteaRoot, 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)
}
}
}
initWorkPathAndConfig := func() {
// init paths and config system for testing
getTestEnv := func(key string) string { return "" }
InitWorkPathAndCommonConfig(getTestEnv, ArgWorkPathAndCustomConf{CustomConf: CustomConf})
if err := 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)
PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
}
initGiteaPaths()
giteaConf := initGiteaConf()
cleanUpEnv()
initWorkPathAndConfig()
if RepoRootPath == "" || AppDataPath == "" {
panic("SetupGiteaTestEnv failed, paths are not initialized")
}
SSH.RootPath = AppDataTempDir("ssh").JoinPath() // prevent tests from writing into ~/.ssh
// 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
}
// PrepareIntegrationTestConfig isolates each process for parallel runs
func PrepareIntegrationTestConfig() (workPath string, err error) {
giteaTestRoot := detectGiteaTestRoot()
isInCI := os.Getenv("CI") != ""
testDatabase := os.Getenv("GITEA_TEST_DATABASE")
if testDatabase == "" {
if isInCI {
return "", errors.New("GITEA_TEST_DATABASE environment variable not set")
}
// for local development, default to sqlite. CI needs to explicitly set a database to avoid unexpected results
testDatabase = "sqlite"
_, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_DATABASE not set - defaulting to %s\n", testDatabase)
}
workPath, err = os.MkdirTemp(filepath.Join(giteaTestRoot, "tests/integration"), "gitea-integration-"+testDatabase+"-")
if err != nil {
return "", err
}
confFile := filepath.Join(workPath, "app.ini")
giteaConf, err := filepath.Rel(giteaTestRoot, confFile)
if err != nil {
return "", err
}
_ = os.Setenv("GITEA_TEST_ROOT", giteaTestRoot)
_ = os.Setenv("GITEA_TEST_CONF", giteaConf)
tmplBuf, err := os.ReadFile(filepath.Join(giteaTestRoot, "tests", testDatabase+".ini.tmpl"))
if err != nil {
return "", err
}
tmpl := string(tmplBuf)
envVars, err := shellquote.Split(os.Getenv("MAKEFILE_VARS"))
if err != nil {
return "", err
}
httpListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
defer httpListener.Close()
sshListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
defer sshListener.Close()
_, httpPort, _ := net.SplitHostPort(httpListener.Addr().String())
_, sshPort, _ := net.SplitHostPort(sshListener.Addr().String())
envVarMap := map[string]string{
"TEST_WORK_PATH": workPath,
"TEST_LOGGER": "test,file",
"TEST_HTTP_PORT": httpPort,
"TEST_SSH_PORT": sshPort,
}
for _, env := range append(os.Environ(), envVars...) {
k, v, _ := strings.Cut(env, "=")
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
envVarMap[k] = v
}
for k, v := range envVarMap {
tmpl = strings.ReplaceAll(tmpl, fmt.Sprintf("{{%s}}", k), v)
}
return workPath, os.WriteFile(confFile, []byte(tmpl), 0o644)
}