Decouple unit test, remove intermediate unittestbridge package (#17662)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
wxiaoguang 2021-11-16 16:53:21 +08:00 committed by GitHub
parent 23bd7b1211
commit 81926d61db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
151 changed files with 1719 additions and 1781 deletions

View file

@ -1,54 +0,0 @@
// 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 unittest
import (
"code.gitea.io/gitea/modules/unittestbridge"
"github.com/stretchr/testify/assert"
)
// For legacy code only, please refer to the `unittestbridge` package.
// TestifyAsserter uses "stretchr/testify/assert" to do assert
type TestifyAsserter struct {
t unittestbridge.Tester
}
// Errorf assert Errorf
func (ta TestifyAsserter) Errorf(format string, args ...interface{}) {
ta.t.Errorf(format, args)
}
// NoError assert NoError
func (ta TestifyAsserter) NoError(err error, msgAndArgs ...interface{}) bool {
return assert.NoError(ta, err, msgAndArgs...)
}
// EqualValues assert EqualValues
func (ta TestifyAsserter) EqualValues(expected, actual interface{}, msgAndArgs ...interface{}) bool {
return assert.EqualValues(ta, expected, actual, msgAndArgs...)
}
// Equal assert Equal
func (ta TestifyAsserter) Equal(expected, actual interface{}, msgAndArgs ...interface{}) bool {
return assert.Equal(ta, expected, actual, msgAndArgs...)
}
// True assert True
func (ta TestifyAsserter) True(value bool, msgAndArgs ...interface{}) bool {
return assert.True(ta, value, msgAndArgs...)
}
// False assert False
func (ta TestifyAsserter) False(value bool, msgAndArgs ...interface{}) bool {
return assert.False(ta, value, msgAndArgs...)
}
// InitUnitTestBridge init the unit test bridge. eg: models.CheckConsistencyFor can use testing and assert frameworks
func InitUnitTestBridge() {
unittestbridge.SetNewAsserterFunc(func(t unittestbridge.Tester) unittestbridge.Asserter {
return &TestifyAsserter{t: t}
})
}

View file

@ -0,0 +1,190 @@
// 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 unittest
import (
"reflect"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
"github.com/stretchr/testify/assert"
"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 assert.TestingT, bean interface{}))
// CheckConsistencyFor test that all matching database entries are consistent
func CheckConsistencyFor(t assert.TestingT, beansToCheck ...interface{}) {
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(db.DefaultContext).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 assert.TestingT, bean interface{}) {
tb, err := db.TableInfo(bean)
assert.NoError(t, err)
f := consistencyCheckMap[tb.Name]
if f == nil {
assert.Fail(t, "unknown bean type: %#v", bean)
return
}
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 assert.TestingT, bean interface{}) {
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.EqualValues(t, 0, user.int("NumMembers"))
assert.EqualValues(t, 0, user.int("NumTeams"))
}
}
checkForRepoConsistency := func(t assert.TestingT, bean interface{}) {
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 %+v", repo)
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 %+v", repo)
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 %+v", repo)
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 %+v", repo)
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 %+v", repo)
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 %+v", repo)
}
checkForIssueConsistency := func(t assert.TestingT, bean interface{}) {
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 %+v", issue)
if issue.bool("IsPull") {
prRow := AssertExistsAndLoadMap(t, "pull_request", builder.Eq{"issue_id": issue.int("ID")})
assert.EqualValues(t, parseInt(prRow["index"]), issue.int("Index"))
}
}
checkForPullRequestConsistency := func(t assert.TestingT, bean interface{}) {
pr := reflectionWrap(bean)
issueRow := AssertExistsAndLoadMap(t, "issue", builder.Eq{"id": pr.int("IssueID")})
assert.True(t, parseBool(issueRow["is_pull"]))
assert.EqualValues(t, parseInt(issueRow["index"]), pr.int("Index"))
}
checkForMilestoneConsistency := func(t assert.TestingT, bean interface{}) {
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 %+v", milestone)
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 assert.TestingT, bean interface{}) {
label := reflectionWrap(bean)
issueLabels, err := db.GetEngine(db.DefaultContext).Table("issue_label").
Where(builder.Eq{"label_id": label.int("ID")}).
Query()
assert.NoError(t, err)
assert.EqualValues(t, label.int("NumIssues"), len(issueLabels), "Unexpected number of issue for label %+v", label)
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 %+v", label)
}
checkForTeamConsistency := func(t assert.TestingT, bean interface{}) {
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 assert.TestingT, bean interface{}) {
action := reflectionWrap(bean)
repoRow := AssertExistsAndLoadMap(t, "repository", builder.Eq{"id": action.int("RepoID")})
assert.Equal(t, parseBool(repoRow["is_private"]), action.bool("IsPrivate"), "action: %+v", action)
}
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
}

View file

@ -0,0 +1,41 @@
// 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 unittest
import (
"log"
"reflect"
)
func fieldByName(v reflect.Value, field string) reflect.Value {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
f := v.FieldByName(field)
if !f.IsValid() {
log.Panicf("can not read %s for %v", field, v)
}
return f
}
type reflectionValue struct {
v reflect.Value
}
func reflectionWrap(v interface{}) *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()
}

View file

@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
@ -43,7 +42,7 @@ func fatalTestError(fmtStr string, args ...interface{}) {
// test database. Creates the test database, and sets necessary settings.
func MainTest(m *testing.M, pathToGiteaRoot string, fixtureFiles ...string) {
var err error
InitUnitTestBridge()
giteaRoot = pathToGiteaRoot
fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures")
@ -125,7 +124,7 @@ func CreateTestEngine(opts FixturesOptions) error {
return err
}
x.SetMapper(names.GonicMapper{})
db.SetUnitTestEngine(x)
db.SetEngine(x)
if err = db.SyncAllTables(); err != nil {
return err

View file

@ -0,0 +1,139 @@
// Copyright 2016 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 unittest
import (
"math"
"code.gitea.io/gitea/models/db"
"github.com/stretchr/testify/assert"
"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 testCond struct {
query interface{}
args []interface{}
}
// Cond create a condition with arguments for a test
func Cond(query interface{}, args ...interface{}) interface{} {
return &testCond{query: query, args: args}
}
func whereConditions(e db.Engine, conditions []interface{}) db.Engine {
for _, condition := range conditions {
switch cond := condition.(type) {
case *testCond:
e = e.Where(cond.query, cond.args...)
default:
e = e.Where(cond)
}
}
return e
}
// LoadBeanIfExists loads beans from fixture database if exist
func LoadBeanIfExists(bean interface{}, conditions ...interface{}) (bool, error) {
e := db.GetEngine(db.DefaultContext)
return whereConditions(e, conditions).Get(bean)
}
// BeanExists for testing, check if a bean exists
func BeanExists(t assert.TestingT, bean interface{}, conditions ...interface{}) bool {
exists, err := LoadBeanIfExists(bean, conditions...)
assert.NoError(t, err)
return exists
}
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
func AssertExistsAndLoadBean(t assert.TestingT, bean interface{}, conditions ...interface{}) interface{} {
exists, err := LoadBeanIfExists(bean, conditions...)
assert.NoError(t, err)
assert.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 assert.TestingT, table string, conditions ...interface{}) map[string]string {
e := db.GetEngine(db.DefaultContext).Table(table)
res, err := whereConditions(e, conditions).Query()
assert.NoError(t, err)
assert.True(t, len(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 assert.TestingT, bean interface{}, conditions ...interface{}) int {
e := db.GetEngine(db.DefaultContext)
count, err := whereConditions(e, conditions).Count(bean)
assert.NoError(t, err)
return int(count)
}
// AssertNotExistsBean assert that a bean does not exist in the test database
func AssertNotExistsBean(t assert.TestingT, bean interface{}, conditions ...interface{}) {
exists, err := LoadBeanIfExists(bean, conditions...)
assert.NoError(t, err)
assert.False(t, exists)
}
// AssertExistsIf asserts that a bean exists or does not exist, depending on
// what is expected.
func AssertExistsIf(t assert.TestingT, expected bool, bean interface{}, conditions ...interface{}) {
exists, err := LoadBeanIfExists(bean, conditions...)
assert.NoError(t, err)
assert.Equal(t, expected, exists)
}
// AssertSuccessfulInsert assert that beans is successfully inserted
func AssertSuccessfulInsert(t assert.TestingT, beans ...interface{}) {
err := db.Insert(db.DefaultContext, beans...)
assert.NoError(t, err)
}
// AssertCount assert the count of a bean
func AssertCount(t assert.TestingT, bean, expected interface{}) {
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 assert.TestingT, tableName string, cond builder.Cond) int64 {
e := db.GetEngine(db.DefaultContext)
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 assert.TestingT, tableName string, cond builder.Cond, expected int) {
assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
}