code.gitea.io/gitea@v1.22.3/models/unittest/testdb.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package unittest
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	"code.gitea.io/gitea/models/system"
    16  	"code.gitea.io/gitea/modules/auth/password/hash"
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/cache"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/setting"
    22  	"code.gitea.io/gitea/modules/setting/config"
    23  	"code.gitea.io/gitea/modules/storage"
    24  	"code.gitea.io/gitea/modules/util"
    25  
    26  	"github.com/stretchr/testify/assert"
    27  	"xorm.io/xorm"
    28  	"xorm.io/xorm/names"
    29  )
    30  
    31  // giteaRoot a path to the gitea root
    32  var (
    33  	giteaRoot   string
    34  	fixturesDir string
    35  )
    36  
    37  // FixturesDir returns the fixture directory
    38  func FixturesDir() string {
    39  	return fixturesDir
    40  }
    41  
    42  func fatalTestError(fmtStr string, args ...any) {
    43  	_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
    44  	os.Exit(1)
    45  }
    46  
    47  // InitSettings initializes config provider and load common settings for tests
    48  func InitSettings() {
    49  	setting.IsInTesting = true
    50  	log.OsExiter = func(code int) {
    51  		if code != 0 {
    52  			// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
    53  			panic(fmt.Errorf("non-zero exit code during testing: %d", code))
    54  		}
    55  		os.Exit(0)
    56  	}
    57  	if setting.CustomConf == "" {
    58  		setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
    59  		_ = os.Remove(setting.CustomConf)
    60  	}
    61  	setting.InitCfgProvider(setting.CustomConf)
    62  	setting.LoadCommonSettings()
    63  
    64  	if err := setting.PrepareAppDataPath(); err != nil {
    65  		log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
    66  	}
    67  	// register the dummy hash algorithm function used in the test fixtures
    68  	_ = hash.Register("dummy", hash.NewDummyHasher)
    69  
    70  	setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
    71  }
    72  
    73  // TestOptions represents test options
    74  type TestOptions struct {
    75  	FixtureFiles []string
    76  	SetUp        func() error // SetUp will be executed before all tests in this package
    77  	TearDown     func() error // TearDown will be executed after all tests in this package
    78  }
    79  
    80  // MainTest a reusable TestMain(..) function for unit tests that need to use a
    81  // test database. Creates the test database, and sets necessary settings.
    82  func MainTest(m *testing.M, testOpts ...*TestOptions) {
    83  	searchDir, _ := os.Getwd()
    84  	for searchDir != "" {
    85  		if _, err := os.Stat(filepath.Join(searchDir, "go.mod")); err == nil {
    86  			break // The "go.mod" should be the one for Gitea repository
    87  		}
    88  		if dir := filepath.Dir(searchDir); dir == searchDir {
    89  			searchDir = "" // reaches the root of filesystem
    90  		} else {
    91  			searchDir = dir
    92  		}
    93  	}
    94  	if searchDir == "" {
    95  		panic("The tests should run in a Gitea repository, there should be a 'go.mod' in the root")
    96  	}
    97  
    98  	giteaRoot = searchDir
    99  	setting.CustomPath = filepath.Join(giteaRoot, "custom")
   100  	InitSettings()
   101  
   102  	fixturesDir = filepath.Join(giteaRoot, "models", "fixtures")
   103  	var opts FixturesOptions
   104  	if len(testOpts) == 0 || len(testOpts[0].FixtureFiles) == 0 {
   105  		opts.Dir = fixturesDir
   106  	} else {
   107  		for _, f := range testOpts[0].FixtureFiles {
   108  			if len(f) != 0 {
   109  				opts.Files = append(opts.Files, filepath.Join(fixturesDir, f))
   110  			}
   111  		}
   112  	}
   113  
   114  	if err := CreateTestEngine(opts); err != nil {
   115  		fatalTestError("Error creating test engine: %v\n", err)
   116  	}
   117  
   118  	setting.IsInTesting = true
   119  	setting.AppURL = "https://try.gitea.io/"
   120  	setting.RunUser = "runuser"
   121  	setting.SSH.User = "sshuser"
   122  	setting.SSH.BuiltinServerUser = "builtinuser"
   123  	setting.SSH.Port = 3000
   124  	setting.SSH.Domain = "try.gitea.io"
   125  	setting.Database.Type = "sqlite3"
   126  	setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
   127  	repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos")
   128  	if err != nil {
   129  		fatalTestError("TempDir: %v\n", err)
   130  	}
   131  	setting.RepoRootPath = repoRootPath
   132  	appDataPath, err := os.MkdirTemp(os.TempDir(), "appdata")
   133  	if err != nil {
   134  		fatalTestError("TempDir: %v\n", err)
   135  	}
   136  	setting.AppDataPath = appDataPath
   137  	setting.AppWorkPath = giteaRoot
   138  	setting.StaticRootPath = giteaRoot
   139  	setting.GravatarSource = "https://secure.gravatar.com/avatar/"
   140  
   141  	setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
   142  
   143  	setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs")
   144  
   145  	setting.Avatar.Storage.Path = filepath.Join(setting.AppDataPath, "avatars")
   146  
   147  	setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars")
   148  
   149  	setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive")
   150  
   151  	setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
   152  
   153  	setting.Actions.LogStorage.Path = filepath.Join(setting.AppDataPath, "actions_log")
   154  
   155  	setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home")
   156  
   157  	setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"
   158  
   159  	config.SetDynGetter(system.NewDatabaseDynKeyGetter())
   160  
   161  	if err = cache.Init(); err != nil {
   162  		fatalTestError("cache.Init: %v\n", err)
   163  	}
   164  	if err = storage.Init(); err != nil {
   165  		fatalTestError("storage.Init: %v\n", err)
   166  	}
   167  	if err = util.RemoveAll(repoRootPath); err != nil {
   168  		fatalTestError("util.RemoveAll: %v\n", err)
   169  	}
   170  	if err = CopyDir(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
   171  		fatalTestError("util.CopyDir: %v\n", err)
   172  	}
   173  
   174  	if err = git.InitFull(context.Background()); err != nil {
   175  		fatalTestError("git.Init: %v\n", err)
   176  	}
   177  	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
   178  	if err != nil {
   179  		fatalTestError("unable to read the new repo root: %v\n", err)
   180  	}
   181  	for _, ownerDir := range ownerDirs {
   182  		if !ownerDir.Type().IsDir() {
   183  			continue
   184  		}
   185  		repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
   186  		if err != nil {
   187  			fatalTestError("unable to read the new repo root: %v\n", err)
   188  		}
   189  		for _, repoDir := range repoDirs {
   190  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
   191  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
   192  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
   193  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
   194  		}
   195  	}
   196  
   197  	if len(testOpts) > 0 && testOpts[0].SetUp != nil {
   198  		if err := testOpts[0].SetUp(); err != nil {
   199  			fatalTestError("set up failed: %v\n", err)
   200  		}
   201  	}
   202  
   203  	exitStatus := m.Run()
   204  
   205  	if len(testOpts) > 0 && testOpts[0].TearDown != nil {
   206  		if err := testOpts[0].TearDown(); err != nil {
   207  			fatalTestError("tear down failed: %v\n", err)
   208  		}
   209  	}
   210  
   211  	if err = util.RemoveAll(repoRootPath); err != nil {
   212  		fatalTestError("util.RemoveAll: %v\n", err)
   213  	}
   214  	if err = util.RemoveAll(appDataPath); err != nil {
   215  		fatalTestError("util.RemoveAll: %v\n", err)
   216  	}
   217  	os.Exit(exitStatus)
   218  }
   219  
   220  // FixturesOptions fixtures needs to be loaded options
   221  type FixturesOptions struct {
   222  	Dir   string
   223  	Files []string
   224  }
   225  
   226  // CreateTestEngine creates a memory database and loads the fixture data from fixturesDir
   227  func CreateTestEngine(opts FixturesOptions) error {
   228  	x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
   229  	if err != nil {
   230  		if strings.Contains(err.Error(), "unknown driver") {
   231  			return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
   232  		}
   233  		return err
   234  	}
   235  	x.SetMapper(names.GonicMapper{})
   236  	db.SetDefaultEngine(context.Background(), x)
   237  
   238  	if err = db.SyncAllTables(); err != nil {
   239  		return err
   240  	}
   241  	switch os.Getenv("GITEA_UNIT_TESTS_LOG_SQL") {
   242  	case "true", "1":
   243  		x.ShowSQL(true)
   244  	}
   245  
   246  	return InitFixtures(opts)
   247  }
   248  
   249  // PrepareTestDatabase load test fixtures into test database
   250  func PrepareTestDatabase() error {
   251  	return LoadFixtures()
   252  }
   253  
   254  // PrepareTestEnv prepares the environment for unit tests. Can only be called
   255  // by tests that use the above MainTest(..) function.
   256  func PrepareTestEnv(t testing.TB) {
   257  	assert.NoError(t, PrepareTestDatabase())
   258  	assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
   259  	metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
   260  	assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath))
   261  	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
   262  	assert.NoError(t, err)
   263  	for _, ownerDir := range ownerDirs {
   264  		if !ownerDir.Type().IsDir() {
   265  			continue
   266  		}
   267  		repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
   268  		assert.NoError(t, err)
   269  		for _, repoDir := range repoDirs {
   270  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
   271  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
   272  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
   273  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
   274  		}
   275  	}
   276  
   277  	base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
   278  }