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>tokarchuk/v1.17
parent
750ac52db2
commit
39ef6f83d5
@ -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" |
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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") |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue