This commit is contained in:
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -4,19 +4,24 @@
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/auth/password/hash"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type FixturesLoader interface {
|
||||
Load() error
|
||||
MarkTableChanged(tableName string)
|
||||
}
|
||||
|
||||
var fixturesLoader FixturesLoader
|
||||
@@ -57,15 +62,101 @@ func loadFixtureResetSeqPgsql(e *xorm.Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fixturesHookStruct struct{}
|
||||
|
||||
func cutSpaceForSQL(s string) (string, string, bool) {
|
||||
s = strings.TrimSpace(s)
|
||||
pos := strings.IndexFunc(s, unicode.IsSpace)
|
||||
if pos == -1 {
|
||||
return s, "", false
|
||||
}
|
||||
return s[:pos], strings.TrimSpace(s[pos+1:]), true
|
||||
}
|
||||
|
||||
func trimTableNameQuotes(s string) string {
|
||||
pos := strings.IndexByte(s, '.')
|
||||
if pos != -1 {
|
||||
s = s[pos+1:]
|
||||
}
|
||||
return strings.Trim(s, "\"`[]")
|
||||
}
|
||||
|
||||
func (f fixturesHookStruct) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
if c.Ctx.Value(db.ContextKeyTestFixtures) != nil {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
ctx, sql := c.Ctx, c.SQL
|
||||
cmdPart, cmdRemaining, ok := cutSpaceForSQL(sql)
|
||||
if !ok {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// ignore the SQLs which don't change data
|
||||
if util.AsciiEqualFold(cmdPart, "SELECT") ||
|
||||
util.AsciiEqualFold(cmdPart, "SHOW") ||
|
||||
util.AsciiEqualFold(cmdPart, "PRAGMA") ||
|
||||
util.AsciiEqualFold(cmdPart, "ALTER") ||
|
||||
util.AsciiEqualFold(cmdPart, "CREATE") ||
|
||||
util.AsciiEqualFold(cmdPart, "DROP") ||
|
||||
util.AsciiEqualFold(cmdPart, "IF") ||
|
||||
util.AsciiEqualFold(cmdPart, "SET") ||
|
||||
util.AsciiEqualFold(cmdPart, "sp_rename") ||
|
||||
util.AsciiEqualFold(cmdPart, "BEGIN") ||
|
||||
util.AsciiEqualFold(cmdPart, "ROLLBACK") ||
|
||||
util.AsciiEqualFold(cmdPart, "COMMIT") {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case util.AsciiEqualFold(cmdPart, "INSERT"):
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
if util.AsciiEqualFold(cmdPart, "INTO") {
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
}
|
||||
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||
case util.AsciiEqualFold(cmdPart, "MERGE"):
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
if util.AsciiEqualFold(cmdPart, "INTO") {
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
}
|
||||
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||
case util.AsciiEqualFold(cmdPart, "UPDATE"):
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||
case util.AsciiEqualFold(cmdPart, "DELETE"):
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
if util.AsciiEqualFold(cmdPart, "FROM") {
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
}
|
||||
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||
case util.AsciiEqualFold(cmdPart, "TRUNCATE"):
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
if util.AsciiEqualFold(cmdPart, "TABLE") {
|
||||
cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining)
|
||||
}
|
||||
fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart))
|
||||
default:
|
||||
// should either parse the table name if it changes data, or ignore it
|
||||
panic("unrecognized sql: " + sql)
|
||||
}
|
||||
_ = cmdRemaining
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (f fixturesHookStruct) AfterProcess(c *contexts.ContextHook) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitFixtures initialize test fixtures for a test database
|
||||
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||
xormEngine := util.IfZero(util.OptionalArg(engine), GetXORMEngine())
|
||||
func InitFixtures(opts FixturesOptions) (err error) {
|
||||
xormEngine := GetXORMEngine()
|
||||
fixturesLoader, err = NewFixturesLoader(xormEngine, opts)
|
||||
// fixturesLoader = NewFixturesLoaderVendor(xormEngine, opts)
|
||||
|
||||
// register the dummy hash algorithm function used in the test fixtures
|
||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||
xormEngine.AddHook(&fixturesHookStruct{})
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -11,10 +12,11 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
@@ -32,7 +34,7 @@ type FixtureItem struct {
|
||||
|
||||
type fixturesLoaderInternal struct {
|
||||
xormEngine *xorm.Engine
|
||||
xormTableNames map[string]bool
|
||||
tableSyncMap sync.Map
|
||||
db *sql.DB
|
||||
dbType schemas.DBType
|
||||
fixtures map[string]*FixtureItem
|
||||
@@ -148,25 +150,36 @@ func (f *fixturesLoaderInternal) Load() error {
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
ctx := context.WithValue(context.Background(), db.ContextKeyTestFixtures, true)
|
||||
|
||||
for _, fixture := range f.fixtures {
|
||||
if !f.xormTableNames[fixture.tableName] {
|
||||
synced, existing := f.tableSyncMap.Load(fixture.tableName)
|
||||
if synced == true || !existing {
|
||||
continue
|
||||
}
|
||||
if err := f.loadFixtures(tx, fixture); err != nil {
|
||||
return fmt.Errorf("failed to load fixtures from %s: %w", fixture.fileFullPath, err)
|
||||
}
|
||||
f.tableSyncMap.Store(fixture.tableName, true)
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
for xormTableName := range f.xormTableNames {
|
||||
if f.fixtures[xormTableName] == nil {
|
||||
_, _ = f.xormEngine.Exec("DELETE FROM `" + xormTableName + "`")
|
||||
f.tableSyncMap.Range(func(k, v any) bool {
|
||||
tableName, synced := k.(string), v.(bool)
|
||||
if !synced && f.fixtures[tableName] == nil {
|
||||
_, _ = f.xormEngine.Context(ctx).Exec("DELETE FROM `" + tableName + "`")
|
||||
}
|
||||
}
|
||||
f.tableSyncMap.Store(tableName, true)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fixturesLoaderInternal) MarkTableChanged(tableName string) {
|
||||
f.tableSyncMap.Store(tableName, false)
|
||||
}
|
||||
|
||||
func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) {
|
||||
if files != nil && len(files) == 0 {
|
||||
return nil, nil //nolint:nilnil // load nothing
|
||||
@@ -215,11 +228,12 @@ func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, er
|
||||
f.paramPlaceholder = func(idx int) string { return "?" }
|
||||
}
|
||||
|
||||
// If a model is not imported in a package (no bean is registered), the table won't exist in database.
|
||||
// So only use tables of registered models (beans).
|
||||
xormBeans, _ := db.NamesToBean()
|
||||
f.xormTableNames = map[string]bool{}
|
||||
for _, bean := range xormBeans {
|
||||
f.xormTableNames[x.TableName(bean)] = true
|
||||
beanTableName := x.TableName(bean)
|
||||
f.tableSyncMap.Store(trimTableNameQuotes(beanTableName), false)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
@@ -70,7 +70,7 @@ func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
|
||||
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
|
||||
"user.yml",
|
||||
}}
|
||||
require.NoError(t, unittest.CreateTestEngine(opts))
|
||||
require.NoError(t, unittest.CreateTestEngine(filepath.Join(t.TempDir(), "sqlite-test.db"), opts))
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestFixturesLoader(t *testing.T) {
|
||||
|
||||
func BenchmarkFixturesLoader(b *testing.B) {
|
||||
opts := prepareTestFixturesLoaders(b)
|
||||
require.NoError(b, unittest.CreateTestEngine(opts))
|
||||
require.NoError(b, unittest.CreateTestEngine(filepath.Join(b.TempDir(), "sqlite-test.db"), opts))
|
||||
loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts)
|
||||
require.NoError(b, err)
|
||||
loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts)
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// SyncFile synchronizes the two files. This is skipped if both files
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockServerOptions tweaks NewMockWebServer behavior.
|
||||
type MockServerOptions struct {
|
||||
// Routes installs extra handlers on the mux before the fixture fallback;
|
||||
// more specific patterns win.
|
||||
Routes func(mux *http.ServeMux)
|
||||
// StripPrefix is trimmed from the request path before forwarding upstream,
|
||||
// useful when the client prepends a prefix the real upstream does not use
|
||||
// (e.g. go-github prepends "/api/v3").
|
||||
StripPrefix string
|
||||
}
|
||||
|
||||
// NewMockWebServer returns a test HTTP server that records upstream responses on demand
|
||||
// and replays them from disk on subsequent runs.
|
||||
//
|
||||
// - liveMode=true: requests are forwarded to liveServerBaseURL and responses written as
|
||||
// fixture files under testDataDir.
|
||||
// - liveMode=false: responses come from existing fixture files.
|
||||
//
|
||||
// Fixture format: header lines ("Name: value"), a blank line, then the body. Before
|
||||
// replay, occurrences of liveServerBaseURL in the body are swapped for the mock URL.
|
||||
//
|
||||
// The typical switch is an env var holding an API token; fixtures ship committed so the
|
||||
// default run (no token) works offline.
|
||||
//
|
||||
// token := os.Getenv("GITEA_TOKEN")
|
||||
// mock := NewMockWebServer(t, "https://gitea.com", fixtureDir, token != "")
|
||||
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool, opts ...MockServerOptions) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
var opt MockServerOptions
|
||||
if len(opts) > 0 {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id", "set-cookie"}
|
||||
|
||||
var mockURL string
|
||||
|
||||
fallback := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqPath := r.URL.EscapedPath()
|
||||
if r.URL.RawQuery != "" {
|
||||
reqPath += "?" + r.URL.RawQuery
|
||||
}
|
||||
log.Info("mock server: %s %s", r.Method, reqPath)
|
||||
|
||||
fixturePath := fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(reqPath))
|
||||
if strings.Contains(r.URL.Path, ".git/") {
|
||||
fixturePath = fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(r.URL.Path))
|
||||
}
|
||||
|
||||
if liveMode {
|
||||
require.NoError(t, os.MkdirAll(testDataDir, 0o755))
|
||||
|
||||
liveURL := liveServerBaseURL + strings.TrimPrefix(reqPath, opt.StripPrefix)
|
||||
req, err := http.NewRequest(r.Method, liveURL, r.Body)
|
||||
require.NoError(t, err, "building upstream request to %s", liveURL)
|
||||
for name, values := range r.Header {
|
||||
if strings.EqualFold(name, "accept-encoding") {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err, "upstream request to %s failed", liveURL)
|
||||
defer resp.Body.Close()
|
||||
assert.Less(t, resp.StatusCode, 400, "upstream %s returned status %d", liveURL, resp.StatusCode)
|
||||
|
||||
out, err := os.Create(fixturePath)
|
||||
require.NoError(t, err, "creating fixture %s", fixturePath)
|
||||
defer out.Close()
|
||||
|
||||
for _, name := range slices.Sorted(maps.Keys(resp.Header)) {
|
||||
if slices.Contains(ignoredHeaders, strings.ToLower(name)) {
|
||||
continue
|
||||
}
|
||||
for _, value := range resp.Header[name] {
|
||||
_, err := fmt.Fprintf(out, "%s: %s\n", name, value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = out.WriteString("\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
require.NoError(t, err, "writing fixture body for %s", liveURL)
|
||||
require.NoError(t, out.Sync())
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(fixturePath)
|
||||
require.NoError(t, err, "missing fixture: %s", fixturePath)
|
||||
|
||||
replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL)
|
||||
headers, body, _ := strings.Cut(replayed, "\n\n")
|
||||
for line := range strings.SplitSeq(headers, "\n") {
|
||||
name, value, ok := strings.Cut(line, ": ")
|
||||
if !ok || strings.EqualFold(name, "Content-Length") {
|
||||
continue
|
||||
}
|
||||
w.Header().Set(name, value)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write([]byte(body))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
mux := http.NewServeMux()
|
||||
if opt.Routes != nil {
|
||||
opt.Routes(mux)
|
||||
}
|
||||
mux.Handle("/", fallback)
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
mockURL = server.URL
|
||||
t.Cleanup(server.Close)
|
||||
return server
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func fieldByName(v reflect.Value, field string) reflect.Value {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
f := v.FieldByName(field)
|
||||
|
||||
@@ -5,61 +5,30 @@ package unittest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/system"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/setting/config"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/tempdir"
|
||||
"gitea.dev/modules/testlogger"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/xorm"
|
||||
"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 +44,20 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
|
||||
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
||||
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||
InitSettingsForTesting()
|
||||
|
||||
tempWorkPath, tempCleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("unit-test-dir-")
|
||||
if err != nil {
|
||||
return testlogger.MainErrorf("Failed to create temp dir for unit test: %v", err)
|
||||
}
|
||||
defer tempCleanup()
|
||||
|
||||
defer setting.MockBuiltinPaths(tempWorkPath, "", "")()
|
||||
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)
|
||||
if err := CreateTestEngine(filepath.Join(tempWorkPath, "sqlite-test.db"), fixturesOpts); err != nil {
|
||||
return testlogger.MainErrorf("Error creating test database engine: %v", err)
|
||||
}
|
||||
|
||||
setting.AppURL = "https://try.gitea.io/"
|
||||
@@ -91,59 +69,28 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
||||
setting.SSH.Domain = "try.gitea.io"
|
||||
setting.Database.Type = "sqlite3"
|
||||
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
||||
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
|
||||
if err != nil {
|
||||
testlogger.Panicf("TempDir: %v\n", err)
|
||||
}
|
||||
defer cleanup1()
|
||||
|
||||
setting.RepoRootPath = repoRootPath
|
||||
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
|
||||
if err != nil {
|
||||
testlogger.Panicf("TempDir: %v\n", err)
|
||||
}
|
||||
defer cleanup2()
|
||||
|
||||
setting.AppDataPath = appDataPath
|
||||
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
|
||||
|
||||
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
|
||||
|
||||
setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs")
|
||||
|
||||
setting.Avatar.Storage.Path = filepath.Join(setting.AppDataPath, "avatars")
|
||||
|
||||
setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars")
|
||||
|
||||
setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive")
|
||||
|
||||
setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
|
||||
|
||||
setting.Actions.LogStorage.Path = filepath.Join(setting.AppDataPath, "actions_log")
|
||||
|
||||
setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home")
|
||||
|
||||
setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"
|
||||
|
||||
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
||||
|
||||
if err = cache.Init(); err != nil {
|
||||
testlogger.Panicf("cache.Init: %v\n", err)
|
||||
return testlogger.MainErrorf("cache.Init: %v", err)
|
||||
}
|
||||
if err = storage.Init(); err != nil {
|
||||
testlogger.Panicf("storage.Init: %v\n", err)
|
||||
return testlogger.MainErrorf("storage.Init: %v", err)
|
||||
}
|
||||
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
||||
testlogger.Panicf("util.SyncDirs: %v\n", err)
|
||||
return testlogger.MainErrorf("util.SyncDirs: %v", err)
|
||||
}
|
||||
|
||||
if err = git.InitFull(); err != nil {
|
||||
testlogger.Panicf("git.Init: %v\n", err)
|
||||
return testlogger.MainErrorf("git.Init: %v", err)
|
||||
}
|
||||
|
||||
if testOpts.SetUp != nil {
|
||||
if err := testOpts.SetUp(); err != nil {
|
||||
testlogger.Panicf("set up failed: %v\n", err)
|
||||
return testlogger.MainErrorf("set up failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,25 +98,121 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
||||
|
||||
if testOpts.TearDown != nil {
|
||||
if err := testOpts.TearDown(); err != nil {
|
||||
testlogger.Panicf("tear down failed: %v\n", err)
|
||||
return testlogger.MainErrorf("tear down failed: %v", err)
|
||||
}
|
||||
}
|
||||
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
|
||||
Files []string
|
||||
}
|
||||
|
||||
// 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")
|
||||
// CreateTestEngine creates a test database and loads the fixture data from fixturesDir
|
||||
func CreateTestEngine(testSQLiteFile string, opts FixturesOptions) error {
|
||||
driver, connStr, err := db.ConnStr(db.ConnOptions{Type: setting.DatabaseTypeSQLite3, SQLitePath: testSQLiteFile, SQLiteBusyTimeout: setting.DefaultSQLiteBusyTimeout})
|
||||
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{})
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -47,7 +47,7 @@ func OrderBy(orderBy string) any {
|
||||
}
|
||||
|
||||
func whereOrderConditions(e db.Engine, conditions []any) db.Engine {
|
||||
orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic
|
||||
orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic. FIXME: some tables do not have "id" column
|
||||
for _, condition := range conditions {
|
||||
switch cond := condition.(type) {
|
||||
case *testCond:
|
||||
|
||||
Reference in New Issue
Block a user