初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"context"
"reflect"
"strconv"
"strings"
"gitea.dev/models/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
)
const (
// these const values are copied from `models` package to prevent from cycle-import
modelsUserTypeOrganization = 1
modelsRepoWatchModeDont = 2
modelsCommentTypeComment = 0
)
var consistencyCheckMap = make(map[string]func(t TestingT, bean any))
// CheckConsistencyFor test that all matching database entries are consistent
func CheckConsistencyFor(t TestingT, beansToCheck ...any) {
for _, bean := range beansToCheck {
sliceType := reflect.SliceOf(reflect.TypeOf(bean))
sliceValue := reflect.MakeSlice(sliceType, 0, 10)
ptrToSliceValue := reflect.New(sliceType)
ptrToSliceValue.Elem().Set(sliceValue)
assert.NoError(t, db.GetEngine(context.TODO()).Table(bean).Find(ptrToSliceValue.Interface()))
sliceValue = ptrToSliceValue.Elem()
for i := 0; i < sliceValue.Len(); i++ {
entity := sliceValue.Index(i).Interface()
checkForConsistency(t, entity)
}
}
}
func checkForConsistency(t TestingT, bean any) {
tb, err := GetXORMEngine().TableInfo(bean)
assert.NoError(t, err)
f := consistencyCheckMap[tb.Name]
require.NotNil(t, f, "unknown bean type: %#v", bean)
f(t, bean)
}
func init() {
parseBool := func(v string) bool {
b, _ := strconv.ParseBool(v)
return b
}
parseInt := func(v string) int {
i, _ := strconv.Atoi(v)
return i
}
checkForUserConsistency := func(t TestingT, bean any) {
user := reflectionWrap(bean)
AssertCountByCond(t, "repository", builder.Eq{"owner_id": user.int("ID")}, user.int("NumRepos"))
AssertCountByCond(t, "star", builder.Eq{"uid": user.int("ID")}, user.int("NumStars"))
AssertCountByCond(t, "org_user", builder.Eq{"org_id": user.int("ID")}, user.int("NumMembers"))
AssertCountByCond(t, "team", builder.Eq{"org_id": user.int("ID")}, user.int("NumTeams"))
AssertCountByCond(t, "follow", builder.Eq{"user_id": user.int("ID")}, user.int("NumFollowing"))
AssertCountByCond(t, "follow", builder.Eq{"follow_id": user.int("ID")}, user.int("NumFollowers"))
if user.int("Type") != modelsUserTypeOrganization {
assert.Equal(t, 0, user.int("NumMembers"), "Unexpected number of members for user id: %d", user.int("ID"))
assert.Equal(t, 0, user.int("NumTeams"), "Unexpected number of teams for user id: %d", user.int("ID"))
}
}
checkForRepoConsistency := func(t TestingT, bean any) {
repo := reflectionWrap(bean)
assert.Equal(t, repo.str("LowerName"), strings.ToLower(repo.str("Name")), "repo: %+v", repo)
AssertCountByCond(t, "star", builder.Eq{"repo_id": repo.int("ID")}, repo.int("NumStars"))
AssertCountByCond(t, "milestone", builder.Eq{"repo_id": repo.int("ID")}, repo.int("NumMilestones"))
AssertCountByCond(t, "repository", builder.Eq{"fork_id": repo.int("ID")}, repo.int("NumForks"))
if repo.bool("IsFork") {
AssertExistsAndLoadMap(t, "repository", builder.Eq{"id": repo.int("ForkID")})
}
actual := GetCountByCond(t, "watch", builder.Eq{"repo_id": repo.int("ID")}.
And(builder.Neq{"mode": modelsRepoWatchModeDont}))
assert.EqualValues(t, repo.int("NumWatches"), actual,
"Unexpected number of watches for repo id: %d", repo.int("ID"))
actual = GetCountByCond(t, "issue", builder.Eq{"is_pull": false, "repo_id": repo.int("ID")})
assert.EqualValues(t, repo.int("NumIssues"), actual,
"Unexpected number of issues for repo id: %d", repo.int("ID"))
actual = GetCountByCond(t, "issue", builder.Eq{"is_pull": false, "is_closed": true, "repo_id": repo.int("ID")})
assert.EqualValues(t, repo.int("NumClosedIssues"), actual,
"Unexpected number of closed issues for repo id: %d", repo.int("ID"))
actual = GetCountByCond(t, "issue", builder.Eq{"is_pull": true, "repo_id": repo.int("ID")})
assert.EqualValues(t, repo.int("NumPulls"), actual,
"Unexpected number of pulls for repo id: %d", repo.int("ID"))
actual = GetCountByCond(t, "issue", builder.Eq{"is_pull": true, "is_closed": true, "repo_id": repo.int("ID")})
assert.EqualValues(t, repo.int("NumClosedPulls"), actual,
"Unexpected number of closed pulls for repo id: %d", repo.int("ID"))
actual = GetCountByCond(t, "milestone", builder.Eq{"is_closed": true, "repo_id": repo.int("ID")})
assert.EqualValues(t, repo.int("NumClosedMilestones"), actual,
"Unexpected number of closed milestones for repo id: %d", repo.int("ID"))
}
checkForIssueConsistency := func(t TestingT, bean any) {
issue := reflectionWrap(bean)
typeComment := modelsCommentTypeComment
actual := GetCountByCond(t, "comment", builder.Eq{"`type`": typeComment, "issue_id": issue.int("ID")})
assert.EqualValues(t, issue.int("NumComments"), actual, "Unexpected number of comments for issue id: %d", issue.int("ID"))
if issue.bool("IsPull") {
prRow := AssertExistsAndLoadMap(t, "pull_request", builder.Eq{"issue_id": issue.int("ID")})
assert.Equal(t, parseInt(prRow["index"]), issue.int("Index"), "Unexpected index for issue id: %d", issue.int("ID"))
}
}
checkForPullRequestConsistency := func(t TestingT, bean any) {
pr := reflectionWrap(bean)
issueRow := AssertExistsAndLoadMap(t, "issue", builder.Eq{"id": pr.int("IssueID")})
assert.True(t, parseBool(issueRow["is_pull"]))
assert.Equal(t, parseInt(issueRow["index"]), pr.int("Index"), "Unexpected index for pull request id: %d", pr.int("ID"))
}
checkForMilestoneConsistency := func(t TestingT, bean any) {
milestone := reflectionWrap(bean)
AssertCountByCond(t, "issue", builder.Eq{"milestone_id": milestone.int("ID")}, milestone.int("NumIssues"))
actual := GetCountByCond(t, "issue", builder.Eq{"is_closed": true, "milestone_id": milestone.int("ID")})
assert.EqualValues(t, milestone.int("NumClosedIssues"), actual, "Unexpected number of closed issues for milestone id: %d", milestone.int("ID"))
completeness := 0
if milestone.int("NumIssues") > 0 {
completeness = milestone.int("NumClosedIssues") * 100 / milestone.int("NumIssues")
}
assert.Equal(t, completeness, milestone.int("Completeness"))
}
checkForLabelConsistency := func(t TestingT, bean any) {
label := reflectionWrap(bean)
issueLabels, err := db.GetEngine(context.TODO()).Table("issue_label").
Where(builder.Eq{"label_id": label.int("ID")}).
Query()
assert.NoError(t, err)
assert.Len(t, issueLabels, label.int("NumIssues"), "Unexpected number of issue for label id: %d", label.int("ID"))
issueIDs := make([]int, len(issueLabels))
for i, issueLabel := range issueLabels {
issueIDs[i], _ = strconv.Atoi(string(issueLabel["issue_id"]))
}
expected := int64(0)
if len(issueIDs) > 0 {
expected = GetCountByCond(t, "issue", builder.In("id", issueIDs).And(builder.Eq{"is_closed": true}))
}
assert.EqualValues(t, expected, label.int("NumClosedIssues"), "Unexpected number of closed issues for label id: %d", label.int("ID"))
}
checkForTeamConsistency := func(t TestingT, bean any) {
team := reflectionWrap(bean)
AssertCountByCond(t, "team_user", builder.Eq{"team_id": team.int("ID")}, team.int("NumMembers"))
AssertCountByCond(t, "team_repo", builder.Eq{"team_id": team.int("ID")}, team.int("NumRepos"))
}
checkForActionConsistency := func(t TestingT, bean any) {
action := reflectionWrap(bean)
if action.int("RepoID") != 1700 { // dangling intentional
repoRow := AssertExistsAndLoadMap(t, "repository", builder.Eq{"id": action.int("RepoID")})
assert.Equal(t, parseBool(repoRow["is_private"]), action.bool("IsPrivate"), "Unexpected is_private field for action id: %d", action.int("ID"))
}
}
consistencyCheckMap["user"] = checkForUserConsistency
consistencyCheckMap["repository"] = checkForRepoConsistency
consistencyCheckMap["issue"] = checkForIssueConsistency
consistencyCheckMap["pull_request"] = checkForPullRequestConsistency
consistencyCheckMap["milestone"] = checkForMilestoneConsistency
consistencyCheckMap["label"] = checkForLabelConsistency
consistencyCheckMap["team"] = checkForTeamConsistency
consistencyCheckMap["action"] = checkForActionConsistency
}
+175
View File
@@ -0,0 +1,175 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"context"
"fmt"
"strings"
"unicode"
"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
// GetXORMEngine gets the XORM engine
func GetXORMEngine() (x *xorm.Engine) {
return db.GetXORMEngineForTesting()
}
func loadFixtureResetSeqPgsql(e *xorm.Engine) error {
results, err := e.QueryString(`SELECT 'SELECT SETVAL(' ||
quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
FROM pg_class AS S,
pg_depend AS D,
pg_class AS T,
pg_attribute AS C,
pg_tables AS PGT
WHERE S.relkind = 'S'
AND S.oid = D.objid
AND D.refobjid = T.oid
AND D.refobjid = C.attrelid
AND D.refobjsubid = C.attnum
AND T.relname = PGT.tablename
ORDER BY S.relname;`)
if err != nil {
return fmt.Errorf("failed to generate sequence update: %w", err)
}
for _, r := range results {
for _, value := range r {
_, err = e.Exec(value)
if err != nil {
return fmt.Errorf("failed to update sequence: %s, error: %w", value, err)
}
}
}
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) (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
}
// LoadFixtures load fixtures for a test database
func LoadFixtures() error {
if err := fixturesLoader.Load(); err != nil {
return err
}
// Now if we're running postgres we need to tell it to update the sequences
if GetXORMEngine().Dialect().URI().DBType == schemas.POSTGRES {
if err := loadFixtureResetSeqPgsql(GetXORMEngine()); err != nil {
return err
}
}
return nil
}
+239
View File
@@ -0,0 +1,239 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"context"
"database/sql"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"gitea.dev/models/db"
"go.yaml.in/yaml/v4"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type FixtureItem struct {
fileFullPath string
tableName string
tableNameQuoted string
sqlInserts []string
sqlInsertArgs [][]any
mssqlHasIdentityColumn bool
}
type fixturesLoaderInternal struct {
xormEngine *xorm.Engine
tableSyncMap sync.Map
db *sql.DB
dbType schemas.DBType
fixtures map[string]*FixtureItem
quoteObject func(string) string
paramPlaceholder func(idx int) string
}
func (f *fixturesLoaderInternal) mssqlTableHasIdentityColumn(db *sql.DB, tableName string) (bool, error) {
row := db.QueryRow(`SELECT COUNT(*) FROM sys.identity_columns WHERE OBJECT_ID = OBJECT_ID(?)`, tableName)
var count int
if err := row.Scan(&count); err != nil {
return false, err
}
return count > 0, nil
}
func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err error) {
for _, m := range row {
for k, v := range m {
if s, ok := v.(string); ok {
if strings.HasPrefix(s, "0x") {
if m[k], err = hex.DecodeString(s[2:]); err != nil {
return err
}
}
}
}
}
return nil
}
func (f *fixturesLoaderInternal) prepareFixtureItem(fixture *FixtureItem) (err error) {
fixture.tableNameQuoted = f.quoteObject(fixture.tableName)
if f.dbType == schemas.MSSQL {
fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName)
if err != nil {
return err
}
}
data, err := os.ReadFile(fixture.fileFullPath)
if err != nil {
return fmt.Errorf("failed to read file %q: %w", fixture.fileFullPath, err)
}
var rows []map[string]any
if err = yaml.Unmarshal(data, &rows); err != nil {
return fmt.Errorf("failed to unmarshal yaml data from %q: %w", fixture.fileFullPath, err)
}
if err = f.preprocessFixtureRow(rows); err != nil {
return fmt.Errorf("failed to preprocess fixture rows from %q: %w", fixture.fileFullPath, err)
}
var sqlBuf []byte
var sqlArguments []any
for _, row := range rows {
sqlBuf = append(sqlBuf, fmt.Sprintf("INSERT INTO %s (", fixture.tableNameQuoted)...)
for k, v := range row {
sqlBuf = append(sqlBuf, f.quoteObject(k)...)
sqlBuf = append(sqlBuf, ","...)
sqlArguments = append(sqlArguments, v)
}
sqlBuf = sqlBuf[:len(sqlBuf)-1]
sqlBuf = append(sqlBuf, ") VALUES ("...)
paramIdx := 1
for range row {
sqlBuf = append(sqlBuf, f.paramPlaceholder(paramIdx)...)
sqlBuf = append(sqlBuf, ',')
paramIdx++
}
sqlBuf[len(sqlBuf)-1] = ')'
fixture.sqlInserts = append(fixture.sqlInserts, string(sqlBuf))
fixture.sqlInsertArgs = append(fixture.sqlInsertArgs, slices.Clone(sqlArguments))
sqlBuf = sqlBuf[:0]
sqlArguments = sqlArguments[:0]
}
return nil
}
func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, fixture *FixtureItem) (err error) {
if fixture.tableNameQuoted == "" {
if err = f.prepareFixtureItem(fixture); err != nil {
return err
}
}
_, err = tx.Exec("DELETE FROM " + fixture.tableNameQuoted) // sqlite3 doesn't support truncate
if err != nil {
return err
}
if fixture.mssqlHasIdentityColumn {
_, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", fixture.tableNameQuoted))
if err != nil {
return err
}
defer func() { _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", fixture.tableNameQuoted)) }()
}
for i := range fixture.sqlInserts {
_, err = tx.Exec(fixture.sqlInserts[i], fixture.sqlInsertArgs[i]...)
}
if err != nil {
return err
}
return nil
}
func (f *fixturesLoaderInternal) Load() error {
tx, err := f.db.Begin()
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
ctx := context.WithValue(context.Background(), db.ContextKeyTestFixtures, true)
for _, fixture := range f.fixtures {
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
}
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
}
files = slices.Clone(files)
if len(files) == 0 {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, e := range entries {
files = append(files, e.Name())
}
}
fixtureItems := map[string]*FixtureItem{}
for _, file := range files {
fileFillPath := file
if !filepath.IsAbs(fileFillPath) {
fileFillPath = filepath.Join(dir, file)
}
tableName, _, _ := strings.Cut(filepath.Base(file), ".")
fixtureItems[tableName] = &FixtureItem{fileFullPath: fileFillPath, tableName: tableName}
}
return fixtureItems, nil
}
func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) {
fixtureItems, err := FixturesFileFullPaths(opts.Dir, opts.Files)
if err != nil {
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
}
f := &fixturesLoaderInternal{xormEngine: x, db: x.DB().DB, dbType: x.Dialect().URI().DBType, fixtures: fixtureItems}
switch f.dbType {
case schemas.SQLITE:
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
f.paramPlaceholder = func(idx int) string { return "?" }
case schemas.POSTGRES:
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
f.paramPlaceholder = func(idx int) string { return fmt.Sprintf(`$%d`, idx) }
case schemas.MYSQL:
f.quoteObject = func(s string) string { return fmt.Sprintf("`%s`", s) }
f.paramPlaceholder = func(idx int) string { return "?" }
case schemas.MSSQL:
f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) }
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()
for _, bean := range xormBeans {
beanTableName := x.TableName(bean)
f.tableSyncMap.Store(trimTableNameQuotes(beanTableName), false)
}
return f, nil
}
+121
View File
@@ -0,0 +1,121 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest_test
import (
"os"
"path/filepath"
"testing"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
)
var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) {
return nil, nil //nolint:nilnil // no vendor fixtures loader configured
}
/*
// the old code is kept here in case we are still interested in benchmarking the two implementations
func init() {
NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) {
return NewFixturesLoaderVendorGoTestfixtures(e, opts)
}
}
func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.FixturesOptions) (*testfixtures.Loader, error) {
files, err := unittest.FixturesFileFullPaths(opts.Dir, opts.Files)
if err != nil {
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
}
var dialect string
switch e.Dialect().URI().DBType {
case schemas.POSTGRES:
dialect = "postgres"
case schemas.MYSQL:
dialect = "mysql"
case schemas.MSSQL:
dialect = "mssql"
case schemas.SQLITE:
dialect = "sqlite3"
default:
return nil, fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType)
}
loaderOptions := []func(loader *testfixtures.Loader) error{
testfixtures.Database(e.DB().DB),
testfixtures.Dialect(dialect),
testfixtures.DangerousSkipTestDatabaseCheck(),
testfixtures.Files(files...),
}
if e.Dialect().URI().DBType == schemas.POSTGRES {
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
}
return testfixtures.New(loaderOptions...)
}
*/
func TestMain(m *testing.M) {
setting.SetupGiteaTestEnv()
os.Exit(m.Run())
}
func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
_ = user_model.User{}
giteaRoot := setting.GetGiteaTestSourceRoot()
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
"user.yml",
}}
require.NoError(t, unittest.CreateTestEngine(filepath.Join(t.TempDir(), "sqlite-test.db"), opts))
return opts
}
func TestFixturesLoader(t *testing.T) {
opts := prepareTestFixturesLoaders(t)
loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts)
require.NoError(t, err)
loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts)
require.NoError(t, err)
t.Run("Internal", func(t *testing.T) {
require.NoError(t, loaderInternal.Load())
require.NoError(t, loaderInternal.Load())
})
t.Run("Vendor", func(t *testing.T) {
if loaderVendor == nil {
t.Skip()
}
require.NoError(t, loaderVendor.Load())
require.NoError(t, loaderVendor.Load())
})
}
func BenchmarkFixturesLoader(b *testing.B) {
opts := prepareTestFixturesLoaders(b)
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)
require.NoError(b, err)
// BenchmarkFixturesLoader/Vendor
// BenchmarkFixturesLoader/Vendor-12 1696 719416 ns/op
// BenchmarkFixturesLoader/Internal
// BenchmarkFixturesLoader/Internal-12 1746 670457 ns/op
b.Run("Internal", func(b *testing.B) {
for b.Loop() {
require.NoError(b, loaderInternal.Load())
}
})
b.Run("Vendor", func(b *testing.B) {
if loaderVendor == nil {
b.Skip()
}
for b.Loop() {
require.NoError(b, loaderVendor.Load())
}
})
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"errors"
"os"
"path/filepath"
"strings"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
// SyncFile synchronizes the two files. This is skipped if both files
// exist and the size, modtime, and mode match.
func SyncFile(srcPath, destPath string) error {
dest, err := os.Stat(destPath)
if err != nil {
if os.IsNotExist(err) {
return util.CopyFile(srcPath, destPath)
}
return err
}
src, err := os.Stat(srcPath)
if err != nil {
return err
}
if src.Size() == dest.Size() &&
src.ModTime().Equal(dest.ModTime()) &&
src.Mode() == dest.Mode() {
return nil
}
return util.CopyFile(srcPath, destPath)
}
// SyncDirs synchronizes files recursively from source to target directory.
// It returns error when error occurs in underlying functions.
func SyncDirs(srcPath, destPath string) error {
destPath = filepath.Clean(destPath)
destPathAbs, err := filepath.Abs(destPath)
if err != nil {
return err
}
devDataPathAbs, err := filepath.Abs(filepath.Join(setting.GetGiteaTestSourceRoot(), "data"))
if err != nil {
return err
}
if strings.HasPrefix(destPathAbs+string(filepath.Separator), devDataPathAbs+string(filepath.Separator)) {
return errors.New("destination path should not be inside Gitea data directory, otherwise your data for dev mode will be removed")
}
err = os.MkdirAll(destPath, os.ModePerm)
if err != nil {
return err
}
// the keep file is used to keep the directory in a git repository, it doesn't need to be synced
// and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty")
const keepFile = ".keep"
// find and delete all untracked files
destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true})
if err != nil {
return err
}
for _, destFile := range destFiles {
destFilePath := filepath.Join(destPath, destFile)
shouldRemove := filepath.Base(destFilePath) == keepFile
if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil {
if os.IsNotExist(err) {
shouldRemove = true
} else {
return err
}
}
// if src file does not exist, remove dest file
if shouldRemove {
if err = os.RemoveAll(destFilePath); err != nil {
return err
}
}
}
// sync src files to dest
srcFiles, err := util.ListDirRecursively(srcPath, &util.ListDirOptions{IncludeDir: true})
if err != nil {
return err
}
for _, srcFile := range srcFiles {
destFilePath := filepath.Join(destPath, srcFile)
// util.ListDirRecursively appends a slash to the directory name
if strings.HasSuffix(srcFile, "/") {
err = os.MkdirAll(destFilePath, os.ModePerm)
} else if filepath.Base(destFilePath) != keepFile {
err = SyncFile(filepath.Join(srcPath, srcFile), destFilePath)
}
if err != nil {
return err
}
}
return nil
}
+142
View File
@@ -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
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"fmt"
"reflect"
)
func fieldByName(v reflect.Value, field string) reflect.Value {
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
f := v.FieldByName(field)
if !f.IsValid() {
panic(fmt.Errorf("can not read %s for %v", field, v))
}
return f
}
type reflectionValue struct {
v reflect.Value
}
func reflectionWrap(v any) *reflectionValue {
return &reflectionValue{v: reflect.ValueOf(v)}
}
func (rv *reflectionValue) int(field string) int {
return int(fieldByName(rv.v, field).Int())
}
func (rv *reflectionValue) str(field string) string {
return fieldByName(rv.v, field).String()
}
func (rv *reflectionValue) bool(field string) bool {
return fieldByName(rv.v, field).Bool()
}
+243
View File
@@ -0,0 +1,243 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"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"
)
// TestOptions represents test options
type TestOptions struct {
FixtureFiles []string
SetUp func() error // SetUp will be executed before all tests in this package
TearDown func() error // TearDown will be executed after all tests in this package
}
// MainTest a reusable TestMain(..) function for unit tests that need to use a
// test database. Creates the test database, and sets necessary settings.
func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
os.Exit(mainTest(m, testOptsArg...))
}
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
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(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/"
setting.Domain = "try.gitea.io"
setting.RunUser = "runuser"
setting.SSH.User = "sshuser"
setting.SSH.BuiltinServerUser = "builtinuser"
setting.SSH.Port = 3000
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"
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
if err = cache.Init(); err != nil {
return testlogger.MainErrorf("cache.Init: %v", err)
}
if err = storage.Init(); err != nil {
return testlogger.MainErrorf("storage.Init: %v", err)
}
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
return testlogger.MainErrorf("util.SyncDirs: %v", err)
}
if err = git.InitFull(); err != nil {
return testlogger.MainErrorf("git.Init: %v", err)
}
if testOpts.SetUp != nil {
if err := testOpts.SetUp(); err != nil {
return testlogger.MainErrorf("set up failed: %v", err)
}
}
exitStatus := m.Run()
if testOpts.TearDown != nil {
if err := testOpts.TearDown(); err != nil {
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 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 {
return err
}
x.SetMapper(names.GonicMapper{})
db.SetDefaultEngine(context.Background(), x)
if err = db.SyncAllTables(); err != nil {
return err
}
switch os.Getenv("GITEA_TEST_LOG_SQL") {
case "true", "1":
x.ShowSQL(true)
}
return InitFixtures(opts)
}
// PrepareTestDatabase load test fixtures into test database
func PrepareTestDatabase() error {
return LoadFixtures()
}
// PrepareTestEnv prepares the environment for unit tests. Can only be called
// by tests that use the above MainTest(..) function.
func PrepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase())
metaPath := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests", "gitea-repositories-meta")
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"context"
"fmt"
"math"
"os"
"strings"
"gitea.dev/models/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
)
// Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons.
// In the future if we can decouple CheckConsistencyFor into separate unit test code, then this file can be moved into unittest package too.
// NonexistentID an ID that will never exist
const NonexistentID = int64(math.MaxInt64)
type TestingT interface {
require.TestingT
assert.TestingT
Context() context.Context
}
type testCond struct {
query any
args []any
}
type testOrderBy string
// Cond create a condition with arguments for a test
func Cond(query any, args ...any) any {
return &testCond{query: query, args: args}
}
// OrderBy creates "ORDER BY" a test query
func OrderBy(orderBy string) any {
return testOrderBy(orderBy)
}
func whereOrderConditions(e db.Engine, conditions []any) db.Engine {
orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic
for _, condition := range conditions {
switch cond := condition.(type) {
case *testCond:
e = e.Where(cond.query, cond.args...)
case testOrderBy:
orderBy = string(cond)
default:
e = e.Where(cond)
}
}
return e.OrderBy(orderBy)
}
func getBeanIfExists(t TestingT, bean any, conditions ...any) (bool, error) {
e := db.GetEngine(t.Context())
return whereOrderConditions(e, conditions).Get(bean)
}
func GetBean[T any](t TestingT, bean T, conditions ...any) (ret T) {
exists, err := getBeanIfExists(t, bean, conditions...)
require.NoError(t, err)
if exists {
return bean
}
return ret
}
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
func AssertExistsAndLoadBean[T any](t TestingT, bean T, conditions ...any) T {
exists, err := getBeanIfExists(t, bean, conditions...)
require.NoError(t, err)
require.True(t, exists,
"Expected to find %+v (of type %T, with conditions %+v), but did not",
bean, bean, conditions)
return bean
}
// AssertExistsAndLoadMap assert that a row exists and load it from the test database
func AssertExistsAndLoadMap(t TestingT, table string, conditions ...any) map[string]string {
e := db.GetEngine(t.Context()).Table(table)
res, err := whereOrderConditions(e, conditions).Query()
assert.NoError(t, err)
assert.Len(t, res, 1,
"Expected to find one row in %s (with conditions %+v), but found %d",
table, conditions, len(res),
)
if len(res) == 1 {
rec := map[string]string{}
for k, v := range res[0] {
rec[k] = string(v)
}
return rec
}
return nil
}
// GetCount get the count of a bean
func GetCount(t TestingT, bean any, conditions ...any) int {
e := db.GetEngine(t.Context())
for _, condition := range conditions {
switch cond := condition.(type) {
case *testCond:
e = e.Where(cond.query, cond.args...)
default:
e = e.Where(cond)
}
}
count, err := e.Count(bean)
assert.NoError(t, err)
return int(count)
}
// AssertNotExistsBean assert that a bean does not exist in the test database
func AssertNotExistsBean(t TestingT, bean any, conditions ...any) {
exists, err := getBeanIfExists(t, bean, conditions...)
assert.NoError(t, err)
assert.False(t, exists)
}
// AssertCount assert the count of a bean
func AssertCount(t TestingT, bean, expected any) bool {
return assert.EqualValues(t, expected, GetCount(t, bean))
}
// AssertInt64InRange assert value is in range [low, high]
func AssertInt64InRange(t assert.TestingT, low, high, value int64) {
assert.True(t, value >= low && value <= high,
"Expected value in range [%d, %d], found %d", low, high, value)
}
// GetCountByCond get the count of database entries matching bean
func GetCountByCond(t TestingT, tableName string, cond builder.Cond) int64 {
e := db.GetEngine(t.Context())
count, err := e.Table(tableName).Where(cond).Count()
assert.NoError(t, err)
return count
}
// AssertCountByCond test the count of database entries matching bean
func AssertCountByCond(t TestingT, tableName string, cond builder.Cond, expected int) bool {
return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
}
// DumpQueryResult dumps the result of a query for debugging purpose
func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) {
x := GetXORMEngine()
goDB := x.DB().DB
sql, ok := sqlOrBean.(string)
if !ok {
sql = "SELECT * FROM " + x.TableName(sqlOrBean)
} else if !strings.Contains(sql, " ") {
sql = "SELECT * FROM " + sql
}
rows, err := goDB.Query(sql, sqlArgs...)
require.NoError(t, err)
defer rows.Close()
columns, err := rows.Columns()
require.NoError(t, err)
_, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql)
idx := 0
for rows.Next() {
row := make([]any, len(columns))
rowPointers := make([]any, len(columns))
for i := range row {
rowPointers[i] = &row[i]
}
require.NoError(t, rows.Scan(rowPointers...))
_, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx)
for i, col := range columns {
_, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i])
}
idx++
}
if idx == 0 {
_, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", "))
}
}