mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Create Proper Migration Tests (#15116)
* Create Proper Migration tests Unfortunately our testing regime has so far meant that migrations do not get proper testing. This PR begins the process of creating migration tests for this. * Add test for v176 * fix mssql drop db Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		
							
								
								
									
										37
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								Makefile
									
									
									
									
									
								
							| @@ -89,7 +89,7 @@ LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(G | ||||
|  | ||||
| LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 | ||||
|  | ||||
| GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations/migration-test,$(filter-out code.gitea.io/gitea/integrations,$(shell $(GO) list -mod=vendor ./... | grep -v /vendor/))) | ||||
| GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/models/migrations code.gitea.io/gitea/integrations/migration-test code.gitea.io/gitea/integrations,$(shell $(GO) list -mod=vendor ./... | grep -v /vendor/)) | ||||
|  | ||||
| FOMANTIC_CONFIGS := semantic.json web_src/fomantic/theme.config.less web_src/fomantic/_site/globals/site.variables | ||||
| FOMANTIC_DEST := web_src/fomantic/build/semantic.js web_src/fomantic/build/semantic.css | ||||
| @@ -399,8 +399,9 @@ test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*) | ||||
|  | ||||
| .PHONY: test-sqlite-migration | ||||
| test-sqlite-migration:  migrations.sqlite.test generate-ini-sqlite | ||||
| test-sqlite-migration:  migrations.sqlite.test migrations.individual.sqlite.test generate-ini-sqlite | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.sqlite.test | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test | ||||
|  | ||||
| generate-ini-mysql: | ||||
| 	sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \ | ||||
| @@ -419,8 +420,9 @@ test-mysql\#%: integrations.mysql.test generate-ini-mysql | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*) | ||||
|  | ||||
| .PHONY: test-mysql-migration | ||||
| test-mysql-migration: migrations.mysql.test generate-ini-mysql | ||||
| test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test generate-ini-mysql | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./migrations.mysql.test | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./migrations.individual.mysql.test | ||||
|  | ||||
| generate-ini-mysql8: | ||||
| 	sed -e 's|{{TEST_MYSQL8_HOST}}|${TEST_MYSQL8_HOST}|g' \ | ||||
| @@ -439,8 +441,9 @@ test-mysql8\#%: integrations.mysql8.test generate-ini-mysql8 | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./integrations.mysql8.test -test.run $(subst .,/,$*) | ||||
|  | ||||
| .PHONY: test-mysql8-migration | ||||
| test-mysql8-migration: migrations.mysql8.test generate-ini-mysql8 | ||||
| test-mysql8-migration: migrations.mysql8.test migrations.individual.mysql8.test generate-ini-mysql8 | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./migrations.mysql8.test | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./migrations.individual.mysql8.test | ||||
|  | ||||
| generate-ini-pgsql: | ||||
| 	sed -e 's|{{TEST_PGSQL_HOST}}|${TEST_PGSQL_HOST}|g' \ | ||||
| @@ -460,8 +463,9 @@ test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*) | ||||
|  | ||||
| .PHONY: test-pgsql-migration | ||||
| test-pgsql-migration: migrations.pgsql.test generate-ini-pgsql | ||||
| test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test generate-ini-pgsql | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./migrations.pgsql.test | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./migrations.individual.pgsql.test | ||||
|  | ||||
| generate-ini-mssql: | ||||
| 	sed -e 's|{{TEST_MSSQL_HOST}}|${TEST_MSSQL_HOST}|g' \ | ||||
| @@ -480,8 +484,9 @@ test-mssql\#%: integrations.mssql.test generate-ini-mssql | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*) | ||||
|  | ||||
| .PHONY: test-mssql-migration | ||||
| test-mssql-migration: migrations.mssql.test generate-ini-mssql | ||||
| test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test generate-ini-mssql | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./migrations.mssql.test -test.failfast | ||||
| 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./migrations.individual.mssql.test -test.failfast | ||||
|  | ||||
| .PHONY: bench-sqlite | ||||
| bench-sqlite: integrations.sqlite.test generate-ini-sqlite | ||||
| @@ -541,6 +546,26 @@ migrations.mssql.test: $(GO_SOURCES) | ||||
| migrations.sqlite.test: $(GO_SOURCES) | ||||
| 	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)' | ||||
|  | ||||
| .PHONY: migrations.individual.mysql.test | ||||
| migrations.individual.mysql.test: $(GO_SOURCES) | ||||
| 	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.mysql.test | ||||
|  | ||||
| .PHONY: migrations.individual.mysql8.test | ||||
| migrations.individual.mysql8.test: $(GO_SOURCES) | ||||
| 	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.mysql8.test | ||||
|  | ||||
| .PHONY: migrations.individual.pgsql.test | ||||
| migrations.individual.pgsql.test: $(GO_SOURCES) | ||||
| 	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.pgsql.test | ||||
|  | ||||
| .PHONY: migrations.individual.mssql.test | ||||
| migrations.individual.mssql.test: $(GO_SOURCES) | ||||
| 	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.mssql.test | ||||
|  | ||||
| .PHONY: migrations.individual.sqlite.test | ||||
| migrations.individual.sqlite.test: $(GO_SOURCES) | ||||
| 	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.sqlite.test -tags '$(TEST_TAGS)' | ||||
|  | ||||
| .PHONY: check | ||||
| check: test | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,29 @@ | ||||
| # Issue_Label 1 should not be deleted | ||||
| - | ||||
|   id: 1 | ||||
|   issue_id: 1 | ||||
|   label_id: 1 | ||||
|  | ||||
| # Issue_label 2 should be deleted | ||||
| - | ||||
|   id: 2 | ||||
|   issue_id: 5 | ||||
|   label_id: 99 | ||||
|  | ||||
| # Issue_Label 3 should not be deleted | ||||
| - | ||||
|   id: 3 | ||||
|   issue_id: 2 | ||||
|   label_id: 1 | ||||
|  | ||||
| # Issue_Label 4 should not be deleted | ||||
| - | ||||
|   id: 4 | ||||
|   issue_id: 2 | ||||
|   label_id: 4 | ||||
|  | ||||
| - | ||||
|   id: 5 | ||||
|   issue_id: 2 | ||||
|   label_id: 87 | ||||
|  | ||||
| @@ -0,0 +1,43 @@ | ||||
| - | ||||
|   id: 1 | ||||
|   repo_id: 1 | ||||
|   org_id: 0 | ||||
|   name: label1 | ||||
|   color: '#abcdef' | ||||
|   num_issues: 2 | ||||
|   num_closed_issues: 0 | ||||
|  | ||||
| - | ||||
|   id: 2 | ||||
|   repo_id: 1 | ||||
|   org_id: 0 | ||||
|   name: label2 | ||||
|   color: '#000000' | ||||
|   num_issues: 1 | ||||
|   num_closed_issues: 1 | ||||
| - | ||||
|   id: 3 | ||||
|   repo_id: 0 | ||||
|   org_id:  3 | ||||
|   name: orglabel3 | ||||
|   color: '#abcdef' | ||||
|   num_issues: 0 | ||||
|   num_closed_issues: 0 | ||||
|  | ||||
| - | ||||
|   id: 4 | ||||
|   repo_id: 0 | ||||
|   org_id: 3 | ||||
|   name: orglabel4 | ||||
|   color: '#000000' | ||||
|   num_issues: 1 | ||||
|   num_closed_issues: 0 | ||||
|  | ||||
| - | ||||
|   id: 5 | ||||
|   repo_id: 10 | ||||
|   org_id: 0 | ||||
|   name: pull-test-label | ||||
|   color: '#000000' | ||||
|   num_issues: 0 | ||||
|   num_closed_issues: 0 | ||||
| @@ -0,0 +1,52 @@ | ||||
| # type Comment struct { | ||||
| #   ID      int64 `xorm:"pk autoincr"` | ||||
| #   Type    int   `xorm:"INDEX"` | ||||
| #   IssueID int64 `xorm:"INDEX"` | ||||
| #   LabelID int64 | ||||
| # } | ||||
| # | ||||
| # we are only interested in type 7 | ||||
| # | ||||
|  | ||||
| - | ||||
|   id: 1 # Should remain | ||||
|   type: 6 | ||||
|   issue_id: 1 | ||||
|   label_id: 0 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 2 # Should remain | ||||
|   type: 7 | ||||
|   issue_id: 1 # repo_id: 1 | ||||
|   label_id: 1 # repo_id: 1 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 3 # Should remain | ||||
|   type: 7 | ||||
|   issue_id: 2 # repo_id: 2 owner_id: 1 | ||||
|   label_id: 2 # org_id: 1 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 4 # Should be DELETED | ||||
|   type: 7 | ||||
|   issue_id: 1 # repo_id: 1 | ||||
|   label_id: 3 # repo_id: 2 | ||||
|   should_remain: false | ||||
| - | ||||
|   id: 5 # Should remain | ||||
|   type: 7 | ||||
|   issue_id: 3 # repo_id: 1 | ||||
|   label_id: 1 # repo_id: 1 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 6 # Should be DELETED | ||||
|   type: 7 | ||||
|   issue_id: 3 # repo_id: 1 owner_id: 2 | ||||
|   label_id: 2 # org_id: 1 | ||||
|   should_remain: false | ||||
| - | ||||
|   id: 7 # Should be DELETED | ||||
|   type: 7 | ||||
|   issue_id: 3 # repo_id: 1 owner_id: 2 | ||||
|   label_id: 5 # repo_id: 3 | ||||
|   should_remain: false | ||||
| @@ -0,0 +1,21 @@ | ||||
| # type Issue struct { | ||||
| #   ID     int64 `xorm:"pk autoincr"` | ||||
| #   RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` | ||||
| #   Index  int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. | ||||
| # } | ||||
| - | ||||
|   id: 1 | ||||
|   repo_id: 1 | ||||
|   index: 1 | ||||
| - | ||||
|   id: 2 | ||||
|   repo_id: 2 | ||||
|   index: 1 | ||||
| - | ||||
|   id: 3 | ||||
|   repo_id: 1 | ||||
|   index: 2 | ||||
| - | ||||
|   id: 4 | ||||
|   repo_id: 3 | ||||
|   index: 1 | ||||
| @@ -0,0 +1,35 @@ | ||||
| # type IssueLabel struct { | ||||
| #   ID      int64 `xorm:"pk autoincr"` | ||||
| #   IssueID int64 `xorm:"UNIQUE(s)"` | ||||
| #   LabelID int64 `xorm:"UNIQUE(s)"` | ||||
| # } | ||||
| - | ||||
|   id: 1 # Should remain - matches comment 2 | ||||
|   issue_id: 1 | ||||
|   label_id: 1 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 2 # Should remain | ||||
|   issue_id: 2 | ||||
|   label_id: 2 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 3 # Should be deleted | ||||
|   issue_id: 1 | ||||
|   label_id: 3 | ||||
|   should_remain: false | ||||
| - | ||||
|   id: 4 # Should remain | ||||
|   issue_id: 3 | ||||
|   label_id: 1 | ||||
|   should_remain: true | ||||
| - | ||||
|   id: 5 # Should be deleted | ||||
|   issue_id: 3 | ||||
|   label_id: 2 | ||||
|   should_remain: false | ||||
| - | ||||
|   id: 6 # Should be deleted | ||||
|   issue_id: 3 | ||||
|   label_id: 5 | ||||
|   should_remain: false | ||||
| @@ -0,0 +1,26 @@ | ||||
| # type Label struct { | ||||
| #   ID     int64 `xorm:"pk autoincr"` | ||||
| #   RepoID int64 `xorm:"INDEX"` | ||||
| #   OrgID  int64 `xorm:"INDEX"` | ||||
| # } | ||||
| - | ||||
|   id: 1 | ||||
|   repo_id: 1 | ||||
|   org_id: 0 | ||||
| - | ||||
|   id: 2 | ||||
|   repo_id: 0 | ||||
|   org_id: 1 | ||||
| - | ||||
|   id: 3 | ||||
|   repo_id: 2 | ||||
|   org_id: 0 | ||||
| - | ||||
|   id: 4 | ||||
|   repo_id: 1 | ||||
|   org_id: 0 | ||||
| - | ||||
|   id: 5 | ||||
|   repo_id: 3 | ||||
|   org_id: 0 | ||||
|  | ||||
| @@ -0,0 +1,17 @@ | ||||
| # type Repository struct { | ||||
| #   ID        int64  `xorm:"pk autoincr"` | ||||
| #   OwnerID   int64  `xorm:"UNIQUE(s) index"` | ||||
| #   LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` | ||||
| # } | ||||
| - | ||||
|   id: 1 | ||||
|   owner_id: 2 | ||||
|   lower_name: "repo1" | ||||
| - | ||||
|   id: 2 | ||||
|   owner_id: 1 | ||||
|   lower_name: "repo2" | ||||
| - | ||||
|   id: 3 | ||||
|   owner_id: 2 | ||||
|   lower_name: "repo3" | ||||
| @@ -819,9 +819,24 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin | ||||
| 		} | ||||
| 		for _, constraint := range constraints { | ||||
| 			if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP CONSTRAINT `%s`", tableName, constraint)); err != nil { | ||||
| 				return fmt.Errorf("Drop table `%s` constraint `%s`: %v", tableName, constraint, err) | ||||
| 				return fmt.Errorf("Drop table `%s` default constraint `%s`: %v", tableName, constraint, err) | ||||
| 			} | ||||
| 		} | ||||
| 		sql = fmt.Sprintf("SELECT DISTINCT Name FROM SYS.INDEXES INNER JOIN SYS.INDEX_COLUMNS ON INDEXES.INDEX_ID = INDEX_COLUMNS.INDEX_ID AND INDEXES.OBJECT_ID = INDEX_COLUMNS.OBJECT_ID WHERE INDEXES.OBJECT_ID = OBJECT_ID('%[1]s') AND INDEX_COLUMNS.COLUMN_ID IN (SELECT column_id FROM sys.columns WHERE lower(NAME) IN (%[2]s) AND object_id = OBJECT_ID('%[1]s'))", | ||||
| 			tableName, strings.ReplaceAll(cols, "`", "'")) | ||||
| 		constraints = make([]string, 0) | ||||
| 		if err := sess.SQL(sql).Find(&constraints); err != nil { | ||||
| 			return fmt.Errorf("Find constraints: %v", err) | ||||
| 		} | ||||
| 		for _, constraint := range constraints { | ||||
| 			if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP CONSTRAINT IF EXISTS `%s`", tableName, constraint)); err != nil { | ||||
| 				return fmt.Errorf("Drop table `%s` index constraint `%s`: %v", tableName, constraint, err) | ||||
| 			} | ||||
| 			if _, err := sess.Exec(fmt.Sprintf("DROP INDEX IF EXISTS `%[2]s` ON `%[1]s`", tableName, constraint)); err != nil { | ||||
| 				return fmt.Errorf("Drop index `%[2]s` on `%[1]s`: %v", tableName, constraint, err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN %s", tableName, cols)); err != nil { | ||||
| 			return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										338
									
								
								models/migrations/migrations_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								models/migrations/migrations_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,338 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package migrations | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/unknwon/com" | ||||
| 	"xorm.io/xorm" | ||||
| 	"xorm.io/xorm/names" | ||||
| ) | ||||
|  | ||||
| func TestMain(m *testing.M) { | ||||
| 	giteaRoot := base.SetupGiteaRoot() | ||||
| 	if giteaRoot == "" { | ||||
| 		fmt.Println("Environment variable $GITEA_ROOT not set") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	giteaBinary := "gitea" | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		giteaBinary += ".exe" | ||||
| 	} | ||||
| 	setting.AppPath = path.Join(giteaRoot, giteaBinary) | ||||
| 	if _, err := os.Stat(setting.AppPath); err != nil { | ||||
| 		fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	giteaConf := os.Getenv("GITEA_CONF") | ||||
| 	if giteaConf == "" { | ||||
| 		giteaConf = path.Join(filepath.Dir(setting.AppPath), "integrations/sqlite.ini") | ||||
| 		fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) | ||||
| 	} | ||||
|  | ||||
| 	if !path.IsAbs(giteaConf) { | ||||
| 		setting.CustomConf = path.Join(giteaRoot, giteaConf) | ||||
| 	} else { | ||||
| 		setting.CustomConf = giteaConf | ||||
| 	} | ||||
|  | ||||
| 	setting.SetCustomPathAndConf("", "", "") | ||||
| 	setting.NewContext() | ||||
| 	setting.CheckLFSVersion() | ||||
| 	setting.InitDBConfig() | ||||
| 	setting.NewLogServices(true) | ||||
|  | ||||
| 	exitStatus := m.Run() | ||||
|  | ||||
| 	if err := removeAllWithRetry(setting.RepoRootPath); err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) | ||||
| 	} | ||||
| 	if err := removeAllWithRetry(setting.AppDataPath); err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) | ||||
| 	} | ||||
| 	os.Exit(exitStatus) | ||||
| } | ||||
|  | ||||
| func removeAllWithRetry(dir string) error { | ||||
| 	var err error | ||||
| 	for i := 0; i < 20; i++ { | ||||
| 		err = os.RemoveAll(dir) | ||||
| 		if err == nil { | ||||
| 			break | ||||
| 		} | ||||
| 		time.Sleep(100 * time.Millisecond) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // SetEngine sets the xorm.Engine | ||||
| func SetEngine() (*xorm.Engine, error) { | ||||
| 	x, err := models.GetNewEngine() | ||||
| 	if err != nil { | ||||
| 		return x, fmt.Errorf("Failed to connect to database: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	x.SetMapper(names.GonicMapper{}) | ||||
| 	// WARNING: for serv command, MUST remove the output to os.stdout, | ||||
| 	// so use log file to instead print to stdout. | ||||
| 	x.SetLogger(models.NewXORMLogger(setting.Database.LogSQL)) | ||||
| 	x.ShowSQL(setting.Database.LogSQL) | ||||
| 	x.SetMaxOpenConns(setting.Database.MaxOpenConns) | ||||
| 	x.SetMaxIdleConns(setting.Database.MaxIdleConns) | ||||
| 	x.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) | ||||
| 	return x, nil | ||||
| } | ||||
|  | ||||
| func deleteDB() error { | ||||
| 	switch { | ||||
| 	case setting.Database.UseSQLite3: | ||||
| 		if err := util.Remove(setting.Database.Path); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) | ||||
|  | ||||
| 	case setting.Database.UseMySQL: | ||||
| 		db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", | ||||
| 			setting.Database.User, setting.Database.Passwd, setting.Database.Host)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer db.Close() | ||||
|  | ||||
| 		if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", setting.Database.Name)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return nil | ||||
| 	case setting.Database.UsePostgreSQL: | ||||
| 		db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", | ||||
| 			setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer db.Close() | ||||
|  | ||||
| 		if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", setting.Database.Name)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		db.Close() | ||||
|  | ||||
| 		// Check if we need to setup a specific schema | ||||
| 		if len(setting.Database.Schema) != 0 { | ||||
| 			db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", | ||||
| 				setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			defer db.Close() | ||||
|  | ||||
| 			schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			defer schrows.Close() | ||||
|  | ||||
| 			if !schrows.Next() { | ||||
| 				// Create and setup a DB schema | ||||
| 				_, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s", setting.Database.Schema)) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Make the user's default search path the created schema; this will affect new connections | ||||
| 			_, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			return nil | ||||
| 		} | ||||
| 	case setting.Database.UseMSSQL: | ||||
| 		host, port := setting.ParseMSSQLHostPort(setting.Database.Host) | ||||
| 		db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", | ||||
| 			host, port, "master", setting.Database.User, setting.Database.Passwd)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer db.Close() | ||||
|  | ||||
| 		if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // prepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. | ||||
| // Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. | ||||
| // | ||||
| // fixtures in `models/migrations/fixtures/<TestName>` will be loaded automatically | ||||
| func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.Engine, func()) { | ||||
| 	t.Helper() | ||||
| 	ourSkip := 2 | ||||
| 	ourSkip += skip | ||||
| 	deferFn := PrintCurrentTest(t, ourSkip) | ||||
| 	assert.NoError(t, os.RemoveAll(setting.RepoRootPath)) | ||||
|  | ||||
| 	assert.NoError(t, com.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), | ||||
| 		setting.RepoRootPath)) | ||||
|  | ||||
| 	if err := deleteDB(); err != nil { | ||||
| 		t.Errorf("unable to reset database: %v", err) | ||||
| 		return nil, deferFn | ||||
| 	} | ||||
|  | ||||
| 	x, err := SetEngine() | ||||
| 	assert.NoError(t, err) | ||||
| 	if x != nil { | ||||
| 		oldDefer := deferFn | ||||
| 		deferFn = func() { | ||||
| 			oldDefer() | ||||
| 			if err := x.Close(); err != nil { | ||||
| 				t.Errorf("error during close: %v", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return x, deferFn | ||||
| 	} | ||||
|  | ||||
| 	if len(syncModels) > 0 { | ||||
| 		if err := x.Sync2(syncModels...); err != nil { | ||||
| 			t.Errorf("error during sync: %v", err) | ||||
| 			return x, deferFn | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	fixturesDir := filepath.Join(filepath.Dir(setting.AppPath), "models", "migrations", "fixtures", t.Name()) | ||||
|  | ||||
| 	if _, err := os.Stat(fixturesDir); err == nil { | ||||
| 		t.Logf("initializing fixtures from: %s", fixturesDir) | ||||
| 		if err := models.InitFixtures(fixturesDir, x); err != nil { | ||||
| 			t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) | ||||
| 			return x, deferFn | ||||
| 		} | ||||
| 		if err := models.LoadFixtures(x); err != nil { | ||||
| 			t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) | ||||
| 			return x, deferFn | ||||
| 		} | ||||
| 	} else if !os.IsNotExist(err) { | ||||
| 		t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) | ||||
| 	} else { | ||||
| 		t.Logf("no fixtures found in: %s", fixturesDir) | ||||
| 	} | ||||
|  | ||||
| 	return x, deferFn | ||||
| } | ||||
|  | ||||
| func Test_dropTableColumns(t *testing.T) { | ||||
| 	x, deferable := prepareTestEnv(t, 0) | ||||
| 	if x == nil || t.Failed() { | ||||
| 		defer deferable() | ||||
| 		return | ||||
| 	} | ||||
| 	defer deferable() | ||||
|  | ||||
| 	type DropTest struct { | ||||
| 		ID            int64 `xorm:"pk autoincr"` | ||||
| 		FirstColumn   string | ||||
| 		ToDropColumn  string `xorm:"unique"` | ||||
| 		AnotherColumn int64 | ||||
| 		CreatedUnix   timeutil.TimeStamp `xorm:"INDEX created"` | ||||
| 		UpdatedUnix   timeutil.TimeStamp `xorm:"INDEX updated"` | ||||
| 	} | ||||
|  | ||||
| 	columns := []string{ | ||||
| 		"first_column", | ||||
| 		"to_drop_column", | ||||
| 		"another_column", | ||||
| 		"created_unix", | ||||
| 		"updated_unix", | ||||
| 	} | ||||
|  | ||||
| 	for i := range columns { | ||||
| 		x.SetMapper(names.GonicMapper{}) | ||||
| 		if err := x.Sync2(new(DropTest)); err != nil { | ||||
| 			t.Errorf("unable to create DropTest table: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 		sess := x.NewSession() | ||||
| 		if err := sess.Begin(); err != nil { | ||||
| 			sess.Close() | ||||
| 			t.Errorf("unable to begin transaction: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if err := dropTableColumns(sess, "drop_test", columns[i:]...); err != nil { | ||||
| 			sess.Close() | ||||
| 			t.Errorf("Unable to drop columns[%d:]: %s from drop_test: %v", i, columns[i:], err) | ||||
| 			return | ||||
| 		} | ||||
| 		if err := sess.Commit(); err != nil { | ||||
| 			sess.Close() | ||||
| 			t.Errorf("unable to commit transaction: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 		sess.Close() | ||||
| 		if err := x.DropTables(new(DropTest)); err != nil { | ||||
| 			t.Errorf("unable to drop table: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 		for j := range columns[i+1:] { | ||||
| 			x.SetMapper(names.GonicMapper{}) | ||||
| 			if err := x.Sync2(new(DropTest)); err != nil { | ||||
| 				t.Errorf("unable to create DropTest table: %v", err) | ||||
| 				return | ||||
| 			} | ||||
| 			dropcols := append([]string{columns[i]}, columns[j+i+1:]...) | ||||
| 			sess := x.NewSession() | ||||
| 			if err := sess.Begin(); err != nil { | ||||
| 				sess.Close() | ||||
| 				t.Errorf("unable to begin transaction: %v", err) | ||||
| 				return | ||||
| 			} | ||||
| 			if err := dropTableColumns(sess, "drop_test", dropcols...); err != nil { | ||||
| 				sess.Close() | ||||
| 				t.Errorf("Unable to drop columns: %s from drop_test: %v", dropcols, err) | ||||
| 				return | ||||
| 			} | ||||
| 			if err := sess.Commit(); err != nil { | ||||
| 				sess.Close() | ||||
| 				t.Errorf("unable to commit transaction: %v", err) | ||||
| 				return | ||||
| 			} | ||||
| 			sess.Close() | ||||
| 			if err := x.DropTables(new(DropTest)); err != nil { | ||||
| 				t.Errorf("unable to drop table: %v", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										188
									
								
								models/migrations/testlogger_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								models/migrations/testlogger_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package migrations | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/queue" | ||||
| 	jsoniter "github.com/json-iterator/go" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	prefix    string | ||||
| 	slowTest  = 10 * time.Second | ||||
| 	slowFlush = 5 * time.Second | ||||
| ) | ||||
|  | ||||
| // TestLogger is a logger which will write to the testing log | ||||
| type TestLogger struct { | ||||
| 	log.WriterLogger | ||||
| } | ||||
|  | ||||
| var writerCloser = &testLoggerWriterCloser{} | ||||
|  | ||||
| type testLoggerWriterCloser struct { | ||||
| 	sync.RWMutex | ||||
| 	t []*testing.TB | ||||
| } | ||||
|  | ||||
| func (w *testLoggerWriterCloser) setT(t *testing.TB) { | ||||
| 	w.Lock() | ||||
| 	w.t = append(w.t, t) | ||||
| 	w.Unlock() | ||||
| } | ||||
|  | ||||
| func (w *testLoggerWriterCloser) Write(p []byte) (int, error) { | ||||
| 	w.RLock() | ||||
| 	var t *testing.TB | ||||
| 	if len(w.t) > 0 { | ||||
| 		t = w.t[len(w.t)-1] | ||||
| 	} | ||||
| 	w.RUnlock() | ||||
| 	if t != nil && *t != nil { | ||||
| 		if len(p) > 0 && p[len(p)-1] == '\n' { | ||||
| 			p = p[:len(p)-1] | ||||
| 		} | ||||
|  | ||||
| 		defer func() { | ||||
| 			err := recover() | ||||
| 			if err == nil { | ||||
| 				return | ||||
| 			} | ||||
| 			var errString string | ||||
| 			errErr, ok := err.(error) | ||||
| 			if ok { | ||||
| 				errString = errErr.Error() | ||||
| 			} else { | ||||
| 				errString, ok = err.(string) | ||||
| 			} | ||||
| 			if !ok { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 			if !strings.HasPrefix(errString, "Log in goroutine after ") { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 		}() | ||||
|  | ||||
| 		(*t).Log(string(p)) | ||||
| 		return len(p), nil | ||||
| 	} | ||||
| 	return len(p), nil | ||||
| } | ||||
|  | ||||
| func (w *testLoggerWriterCloser) Close() error { | ||||
| 	w.Lock() | ||||
| 	if len(w.t) > 0 { | ||||
| 		w.t = w.t[:len(w.t)-1] | ||||
| 	} | ||||
| 	w.Unlock() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // PrintCurrentTest prints the current test to os.Stdout | ||||
| func PrintCurrentTest(t testing.TB, skip ...int) func() { | ||||
| 	start := time.Now() | ||||
| 	actualSkip := 1 | ||||
| 	if len(skip) > 0 { | ||||
| 		actualSkip = skip[0] | ||||
| 	} | ||||
| 	_, filename, line, _ := runtime.Caller(actualSkip) | ||||
|  | ||||
| 	if log.CanColorStdout { | ||||
| 		fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", fmt.Formatter(log.NewColoredValue(t.Name())), strings.TrimPrefix(filename, prefix), line) | ||||
| 	} else { | ||||
| 		fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", t.Name(), strings.TrimPrefix(filename, prefix), line) | ||||
| 	} | ||||
| 	writerCloser.setT(&t) | ||||
| 	return func() { | ||||
| 		took := time.Since(start) | ||||
| 		if took > slowTest { | ||||
| 			if log.CanColorStdout { | ||||
| 				fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgYellow)), fmt.Formatter(log.NewColoredValue(took, log.Bold, log.FgYellow))) | ||||
| 			} else { | ||||
| 				fmt.Fprintf(os.Stdout, "+++ %s is a slow tets (took %v)\n", t.Name(), took) | ||||
| 			} | ||||
| 		} | ||||
| 		timer := time.AfterFunc(slowFlush, func() { | ||||
| 			if log.CanColorStdout { | ||||
| 				fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), slowFlush) | ||||
| 			} else { | ||||
| 				fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", t.Name(), slowFlush) | ||||
| 			} | ||||
| 		}) | ||||
| 		if err := queue.GetManager().FlushAll(context.Background(), -1); err != nil { | ||||
| 			t.Errorf("Flushing queues failed with error %v", err) | ||||
| 		} | ||||
| 		timer.Stop() | ||||
| 		flushTook := time.Since(start) - took | ||||
| 		if flushTook > slowFlush { | ||||
| 			if log.CanColorStdout { | ||||
| 				fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), fmt.Formatter(log.NewColoredValue(flushTook, log.Bold, log.FgRed))) | ||||
| 			} else { | ||||
| 				fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", t.Name(), flushTook) | ||||
| 			} | ||||
| 		} | ||||
| 		_ = writerCloser.Close() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Printf takes a format and args and prints the string to os.Stdout | ||||
| func Printf(format string, args ...interface{}) { | ||||
| 	if log.CanColorStdout { | ||||
| 		for i := 0; i < len(args); i++ { | ||||
| 			args[i] = log.NewColoredValue(args[i]) | ||||
| 		} | ||||
| 	} | ||||
| 	fmt.Fprintf(os.Stdout, "\t"+format, args...) | ||||
| } | ||||
|  | ||||
| // NewTestLogger creates a TestLogger as a log.LoggerProvider | ||||
| func NewTestLogger() log.LoggerProvider { | ||||
| 	logger := &TestLogger{} | ||||
| 	logger.Colorize = log.CanColorStdout | ||||
| 	logger.Level = log.TRACE | ||||
| 	return logger | ||||
| } | ||||
|  | ||||
| // Init inits connection writer with json config. | ||||
| // json config only need key "level". | ||||
| func (log *TestLogger) Init(config string) error { | ||||
| 	json := jsoniter.ConfigCompatibleWithStandardLibrary | ||||
| 	err := json.Unmarshal([]byte(config), log) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	log.NewWriterLogger(writerCloser) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Flush when log should be flushed | ||||
| func (log *TestLogger) Flush() { | ||||
| } | ||||
|  | ||||
| //ReleaseReopen does nothing | ||||
| func (log *TestLogger) ReleaseReopen() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetName returns the default name for this implementation | ||||
| func (log *TestLogger) GetName() string { | ||||
| 	return "test" | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	log.Register("test", NewTestLogger) | ||||
| 	_, filename, _, _ := runtime.Caller(0) | ||||
| 	prefix = strings.TrimSuffix(filename, "integrations/testlogger.go") | ||||
| } | ||||
| @@ -8,6 +8,9 @@ import ( | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
|  | ||||
| // removeInvalidLabels looks through the database to look for comments and issue_labels | ||||
| // that refer to labels do not belong to the repository or organization that repository | ||||
| // that the issue is in | ||||
| func removeInvalidLabels(x *xorm.Engine) error { | ||||
| 	type Comment struct { | ||||
| 		ID      int64 `xorm:"pk autoincr"` | ||||
|   | ||||
							
								
								
									
										128
									
								
								models/migrations/v176_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								models/migrations/v176_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package migrations | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func Test_removeInvalidLabels(t *testing.T) { | ||||
| 	// Models used by the migration | ||||
| 	type Comment struct { | ||||
| 		ID           int64 `xorm:"pk autoincr"` | ||||
| 		Type         int   `xorm:"INDEX"` | ||||
| 		IssueID      int64 `xorm:"INDEX"` | ||||
| 		LabelID      int64 | ||||
| 		ShouldRemain bool // <- Flag for testing the migration | ||||
| 	} | ||||
|  | ||||
| 	type Issue struct { | ||||
| 		ID     int64 `xorm:"pk autoincr"` | ||||
| 		RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` | ||||
| 		Index  int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. | ||||
| 	} | ||||
|  | ||||
| 	type Repository struct { | ||||
| 		ID        int64  `xorm:"pk autoincr"` | ||||
| 		OwnerID   int64  `xorm:"UNIQUE(s) index"` | ||||
| 		LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` | ||||
| 	} | ||||
|  | ||||
| 	type Label struct { | ||||
| 		ID     int64 `xorm:"pk autoincr"` | ||||
| 		RepoID int64 `xorm:"INDEX"` | ||||
| 		OrgID  int64 `xorm:"INDEX"` | ||||
| 	} | ||||
|  | ||||
| 	type IssueLabel struct { | ||||
| 		ID           int64 `xorm:"pk autoincr"` | ||||
| 		IssueID      int64 `xorm:"UNIQUE(s)"` | ||||
| 		LabelID      int64 `xorm:"UNIQUE(s)"` | ||||
| 		ShouldRemain bool  // <- Flag for testing the migration | ||||
| 	} | ||||
|  | ||||
| 	// load and prepare the test database | ||||
| 	x, deferable := prepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) | ||||
| 	if x == nil || t.Failed() { | ||||
| 		defer deferable() | ||||
| 		return | ||||
| 	} | ||||
| 	defer deferable() | ||||
|  | ||||
| 	var issueLabels []*IssueLabel | ||||
| 	ilPreMigration := map[int64]*IssueLabel{} | ||||
| 	ilPostMigration := map[int64]*IssueLabel{} | ||||
|  | ||||
| 	var comments []*Comment | ||||
| 	comPreMigration := map[int64]*Comment{} | ||||
| 	comPostMigration := map[int64]*Comment{} | ||||
|  | ||||
| 	// Get pre migration values | ||||
| 	if err := x.Find(&issueLabels); err != nil { | ||||
| 		t.Errorf("Unable to find issueLabels: %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, issueLabel := range issueLabels { | ||||
| 		ilPreMigration[issueLabel.ID] = issueLabel | ||||
| 	} | ||||
| 	if err := x.Find(&comments); err != nil { | ||||
| 		t.Errorf("Unable to find comments: %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, comment := range comments { | ||||
| 		comPreMigration[comment.ID] = comment | ||||
| 	} | ||||
|  | ||||
| 	// Run the migration | ||||
| 	if err := removeInvalidLabels(x); err != nil { | ||||
| 		t.Errorf("unable to RemoveInvalidLabels: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Get the post migration values | ||||
| 	issueLabels = issueLabels[:0] | ||||
| 	if err := x.Find(&issueLabels); err != nil { | ||||
| 		t.Errorf("Unable to find issueLabels: %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, issueLabel := range issueLabels { | ||||
| 		ilPostMigration[issueLabel.ID] = issueLabel | ||||
| 	} | ||||
| 	comments = comments[:0] | ||||
| 	if err := x.Find(&comments); err != nil { | ||||
| 		t.Errorf("Unable to find comments: %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, comment := range comments { | ||||
| 		comPostMigration[comment.ID] = comment | ||||
| 	} | ||||
|  | ||||
| 	// Finally test results of the migration | ||||
| 	for id, comment := range comPreMigration { | ||||
| 		post, ok := comPostMigration[id] | ||||
| 		if ok { | ||||
| 			if !comment.ShouldRemain { | ||||
| 				t.Errorf("Comment[%d] remained but should have been deleted", id) | ||||
| 			} | ||||
| 			assert.Equal(t, comment, post) | ||||
| 		} else if comment.ShouldRemain { | ||||
| 			t.Errorf("Comment[%d] was deleted but should have remained", id) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for id, il := range ilPreMigration { | ||||
| 		post, ok := ilPostMigration[id] | ||||
| 		if ok { | ||||
| 			if !il.ShouldRemain { | ||||
| 				t.Errorf("IssueLabel[%d] remained but should have been deleted", id) | ||||
| 			} | ||||
| 			assert.Equal(t, il, post) | ||||
| 		} else if il.ShouldRemain { | ||||
| 			t.Errorf("IssueLabel[%d] was deleted but should have remained", id) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
|  | ||||
| // deleteOrphanedIssueLabels looks through the database for issue_labels where the label no longer exists and deletes them. | ||||
| func deleteOrphanedIssueLabels(x *xorm.Engine) error { | ||||
| 	type IssueLabel struct { | ||||
| 		ID      int64 `xorm:"pk autoincr"` | ||||
|   | ||||
							
								
								
									
										88
									
								
								models/migrations/v177_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								models/migrations/v177_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package migrations | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func Test_deleteOrphanedIssueLabels(t *testing.T) { | ||||
| 	// Create the models used in the migration | ||||
| 	type IssueLabel struct { | ||||
| 		ID      int64 `xorm:"pk autoincr"` | ||||
| 		IssueID int64 `xorm:"UNIQUE(s)"` | ||||
| 		LabelID int64 `xorm:"UNIQUE(s)"` | ||||
| 	} | ||||
|  | ||||
| 	type Label struct { | ||||
| 		ID              int64 `xorm:"pk autoincr"` | ||||
| 		RepoID          int64 `xorm:"INDEX"` | ||||
| 		OrgID           int64 `xorm:"INDEX"` | ||||
| 		Name            string | ||||
| 		Description     string | ||||
| 		Color           string `xorm:"VARCHAR(7)"` | ||||
| 		NumIssues       int | ||||
| 		NumClosedIssues int | ||||
| 		CreatedUnix     timeutil.TimeStamp `xorm:"INDEX created"` | ||||
| 		UpdatedUnix     timeutil.TimeStamp `xorm:"INDEX updated"` | ||||
| 	} | ||||
|  | ||||
| 	// Prepare and load the testing database | ||||
| 	x, deferable := prepareTestEnv(t, 0, new(IssueLabel), new(Label)) | ||||
| 	if x == nil || t.Failed() { | ||||
| 		defer deferable() | ||||
| 		return | ||||
| 	} | ||||
| 	defer deferable() | ||||
|  | ||||
| 	var issueLabels []*IssueLabel | ||||
| 	preMigration := map[int64]*IssueLabel{} | ||||
| 	postMigration := map[int64]*IssueLabel{} | ||||
|  | ||||
| 	// Load issue labels that exist in the database pre-migration | ||||
| 	if err := x.Find(&issueLabels); err != nil { | ||||
| 		assert.NoError(t, err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, issueLabel := range issueLabels { | ||||
| 		preMigration[issueLabel.ID] = issueLabel | ||||
| 	} | ||||
|  | ||||
| 	// Run the migration | ||||
| 	if err := deleteOrphanedIssueLabels(x); err != nil { | ||||
| 		assert.NoError(t, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Load the remaining issue-labels | ||||
| 	issueLabels = issueLabels[:0] | ||||
| 	if err := x.Find(&issueLabels); err != nil { | ||||
| 		assert.NoError(t, err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, issueLabel := range issueLabels { | ||||
| 		postMigration[issueLabel.ID] = issueLabel | ||||
| 	} | ||||
|  | ||||
| 	// Now test what is left | ||||
| 	if _, ok := postMigration[2]; ok { | ||||
| 		t.Errorf("Orphaned Label[2] survived the migration") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if _, ok := postMigration[5]; ok { | ||||
| 		t.Errorf("Orphaned Label[5] survived the migration") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for id, post := range postMigration { | ||||
| 		pre := preMigration[id] | ||||
| 		assert.Equal(t, pre, post, "migration changed issueLabel %d", id) | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @@ -142,7 +142,8 @@ func init() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getEngine() (*xorm.Engine, error) { | ||||
| // GetNewEngine returns a new xorm engine from the configuration | ||||
| func GetNewEngine() (*xorm.Engine, error) { | ||||
| 	connStr, err := setting.DBConnStr() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -172,7 +173,7 @@ func getEngine() (*xorm.Engine, error) { | ||||
|  | ||||
| // NewTestEngine sets a new test xorm.Engine | ||||
| func NewTestEngine() (err error) { | ||||
| 	x, err = getEngine() | ||||
| 	x, err = GetNewEngine() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Connect to database: %v", err) | ||||
| 	} | ||||
| @@ -185,7 +186,7 @@ func NewTestEngine() (err error) { | ||||
|  | ||||
| // SetEngine sets the xorm.Engine | ||||
| func SetEngine() (err error) { | ||||
| 	x, err = getEngine() | ||||
| 	x, err = GetNewEngine() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Failed to connect to database: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -10,16 +10,22 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/go-testfixtures/testfixtures/v3" | ||||
| 	"xorm.io/xorm" | ||||
| 	"xorm.io/xorm/schemas" | ||||
| ) | ||||
|  | ||||
| var fixtures *testfixtures.Loader | ||||
|  | ||||
| // InitFixtures initialize test fixtures for a test database | ||||
| func InitFixtures(dir string) (err error) { | ||||
| func InitFixtures(dir string, engine ...*xorm.Engine) (err error) { | ||||
| 	e := x | ||||
| 	if len(engine) == 1 { | ||||
| 		e = engine[0] | ||||
| 	} | ||||
|  | ||||
| 	testfiles := testfixtures.Directory(dir) | ||||
| 	dialect := "unknown" | ||||
| 	switch x.Dialect().URI().DBType { | ||||
| 	switch e.Dialect().URI().DBType { | ||||
| 	case schemas.POSTGRES: | ||||
| 		dialect = "postgres" | ||||
| 	case schemas.MYSQL: | ||||
| @@ -33,13 +39,13 @@ func InitFixtures(dir string) (err error) { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	loaderOptions := []func(loader *testfixtures.Loader) error{ | ||||
| 		testfixtures.Database(x.DB().DB), | ||||
| 		testfixtures.Database(e.DB().DB), | ||||
| 		testfixtures.Dialect(dialect), | ||||
| 		testfixtures.DangerousSkipTestDatabaseCheck(), | ||||
| 		testfiles, | ||||
| 	} | ||||
|  | ||||
| 	if x.Dialect().URI().DBType == schemas.POSTGRES { | ||||
| 	if e.Dialect().URI().DBType == schemas.POSTGRES { | ||||
| 		loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) | ||||
| 	} | ||||
|  | ||||
| @@ -52,7 +58,11 @@ func InitFixtures(dir string) (err error) { | ||||
| } | ||||
|  | ||||
| // LoadFixtures load fixtures for a test database | ||||
| func LoadFixtures() error { | ||||
| func LoadFixtures(engine ...*xorm.Engine) error { | ||||
| 	e := x | ||||
| 	if len(engine) == 1 { | ||||
| 		e = engine[0] | ||||
| 	} | ||||
| 	var err error | ||||
| 	// Database transaction conflicts could occur and result in ROLLBACK | ||||
| 	// As a simple workaround, we just retry 20 times. | ||||
| @@ -67,8 +77,8 @@ func LoadFixtures() error { | ||||
| 		fmt.Printf("LoadFixtures failed after retries: %v\n", err) | ||||
| 	} | ||||
| 	// Now if we're running postgres we need to tell it to update the sequences | ||||
| 	if x.Dialect().URI().DBType == schemas.POSTGRES { | ||||
| 		results, err := x.QueryString(`SELECT 'SELECT SETVAL(' || | ||||
| 	if e.Dialect().URI().DBType == schemas.POSTGRES { | ||||
| 		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)|| ';' | ||||
| @@ -90,7 +100,7 @@ func LoadFixtures() error { | ||||
| 		} | ||||
| 		for _, r := range results { | ||||
| 			for _, value := range r { | ||||
| 				_, err = x.Exec(value) | ||||
| 				_, err = e.Exec(value) | ||||
| 				if err != nil { | ||||
| 					fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err) | ||||
| 					return err | ||||
|   | ||||
		Reference in New Issue
	
	Block a user