mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-28 16:47:01 +09:00
test: speed up go tests (#39914)
- Skip race instrumentation of the transpiled SQLite engine `modernc.org/sqlite/lib` when `GOTEST_FLAGS` contains `-race`, Gitea code and the driver boundary stay instrumented. `models/issues` drops from ~10s to ~5s. - Hash OAuth2 client secrets with `bcrypt.MinCost` in tests, like user passwords already use the `dummy` hasher. `TestOAuth2` drops from ~22s to ~2s on the PostgreSQL job. - Stop the Redis locker's extension loop directly on `Close` instead of waiting for its next tick, and run the memory locker tests under `testing/synctest`. `modules/globallock` drops from ~12s to ~4s. Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
6 files changed
+57
-26
No files matched your search
@@ -68,6 +68,8 @@ endif
|
|||||||
|
|
||||||
# GOTEST_FLAGS is for unit test and integration test
|
# GOTEST_FLAGS is for unit test and integration test
|
||||||
GOTEST_FLAGS ?= -timeout 40m
|
GOTEST_FLAGS ?= -timeout 40m
|
||||||
|
# disable race detection for the packages that slow down the tests
|
||||||
|
GOTEST_FLAGS := -gcflags=modernc.org/sqlite/lib=-race=false $(GOTEST_FLAGS)
|
||||||
|
|
||||||
STORED_VERSION_FILE := VERSION
|
STORED_VERSION_FILE := VERSION
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ func (app *OAuth2Application) GenerateClientSecret(ctx context.Context) (string,
|
|||||||
// for code scanners to grab sensitive tokens.
|
// for code scanners to grab sensitive tokens.
|
||||||
clientSecret := "gto_" + base32Lower.EncodeToString(rBytes)
|
clientSecret := "gto_" + base32Lower.EncodeToString(rBytes)
|
||||||
|
|
||||||
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost)
|
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), util.Iif(setting.IsInTesting, bcrypt.MinCost, bcrypt.DefaultCost))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import (
|
|||||||
|
|
||||||
auth_model "gitea.dev/models/auth"
|
auth_model "gitea.dev/models/auth"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,6 +150,13 @@ func TestOAuth2ApplicationAndGrant(t *testing.T) {
|
|||||||
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret})
|
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret})
|
||||||
assert.True(t, app.ValidateClientSecret([]byte(secret)))
|
assert.True(t, app.ValidateClientSecret([]byte(secret)))
|
||||||
assert.False(t, app.ValidateClientSecret([]byte("fewijfowejgfiowjeoifew")))
|
assert.False(t, app.ValidateClientSecret([]byte("fewijfowejgfiowjeoifew")))
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.IsInTesting, false)()
|
||||||
|
_, err = app.GenerateClientSecret(t.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
cost, err := bcrypt.Cost([]byte(app.ClientSecret))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, bcrypt.DefaultCost, cost)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("CreateOAuth2Application", func(t *testing.T) {
|
t.Run("CreateOAuth2Application", func(t *testing.T) {
|
||||||
@@ -161,6 +171,7 @@ func TestOAuth2ApplicationAndGrant(t *testing.T) {
|
|||||||
func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) {
|
func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) {
|
||||||
assert.NoError(b, unittest.PrepareTestDatabase())
|
assert.NoError(b, unittest.PrepareTestDatabase())
|
||||||
app := unittest.AssertExistsAndLoadBean(b, &auth_model.OAuth2Application{ID: 1})
|
app := unittest.AssertExistsAndLoadBean(b, &auth_model.OAuth2Application{ID: 1})
|
||||||
|
defer test.MockVariableValue(&setting.IsInTesting, false)()
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
_, _ = app.GenerateClientSecret(b.Context())
|
_, _ = app.GenerateClientSecret(b.Context())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"testing/synctest"
|
||||||
|
|
||||||
"gitea.dev/modules/test"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -17,7 +15,6 @@ import (
|
|||||||
|
|
||||||
func TestLockAndDo(t *testing.T) {
|
func TestLockAndDo(t *testing.T) {
|
||||||
t.Run("redis", func(t *testing.T) {
|
t.Run("redis", func(t *testing.T) {
|
||||||
defer test.MockVariableValue(&redisLockExpiry, 5*time.Second)() // Close waits for the extend goroutine's next tick
|
|
||||||
locker := newTestRedisLocker(t)
|
locker := newTestRedisLocker(t)
|
||||||
defaultLocker.Store(new(locker))
|
defaultLocker.Store(new(locker))
|
||||||
testLockAndDo(t)
|
testLockAndDo(t)
|
||||||
@@ -26,8 +23,10 @@ func TestLockAndDo(t *testing.T) {
|
|||||||
require.NoError(t, rl.Close())
|
require.NoError(t, rl.Close())
|
||||||
})
|
})
|
||||||
t.Run("memory", func(t *testing.T) {
|
t.Run("memory", func(t *testing.T) {
|
||||||
defaultLocker.Store(new(NewMemoryLocker()))
|
synctest.Test(t, func(t *testing.T) {
|
||||||
testLockAndDo(t)
|
defaultLocker.Store(new(NewMemoryLocker()))
|
||||||
|
testLockAndDo(t)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/test"
|
"gitea.dev/modules/test"
|
||||||
@@ -41,7 +42,17 @@ func TestLocker(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testLocker(t *testing.T, locker Locker) {
|
func testLocker(t *testing.T, locker Locker) {
|
||||||
t.Run("lock", func(t *testing.T) {
|
runTest := func(name string, run func(*testing.T)) {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if _, memory := locker.(*memoryLocker); memory {
|
||||||
|
synctest.Test(t, run)
|
||||||
|
} else {
|
||||||
|
run(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest("lock", func(t *testing.T) {
|
||||||
parentCtx := t.Context()
|
parentCtx := t.Context()
|
||||||
release, err := locker.Lock(parentCtx, "test")
|
release, err := locker.Lock(parentCtx, "test")
|
||||||
defer release()
|
defer release()
|
||||||
@@ -67,7 +78,7 @@ func testLocker(t *testing.T, locker Locker) {
|
|||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("try lock", func(t *testing.T) {
|
runTest("try lock", func(t *testing.T) {
|
||||||
parentCtx := t.Context()
|
parentCtx := t.Context()
|
||||||
ok, release, err := locker.TryLock(parentCtx, "test")
|
ok, release, err := locker.TryLock(parentCtx, "test")
|
||||||
defer release()
|
defer release()
|
||||||
@@ -95,7 +106,7 @@ func testLocker(t *testing.T, locker Locker) {
|
|||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("wait and acquired", func(t *testing.T) {
|
runTest("wait and acquired", func(t *testing.T) {
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
release, err := locker.Lock(ctx, "test")
|
release, err := locker.Lock(ctx, "test")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -115,7 +126,7 @@ func testLocker(t *testing.T, locker Locker) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("multiple release", func(t *testing.T) {
|
runTest("multiple release", func(t *testing.T) {
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
release1, err := locker.Lock(ctx, "test")
|
release1, err := locker.Lock(ctx, "test")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/nosql"
|
"gitea.dev/modules/nosql"
|
||||||
@@ -27,9 +26,10 @@ type redisLocker struct {
|
|||||||
conn redis.UniversalClient
|
conn redis.UniversalClient
|
||||||
rs *redsync.Redsync
|
rs *redsync.Redsync
|
||||||
|
|
||||||
mutexM sync.Map
|
mutexM sync.Map
|
||||||
closed atomic.Bool
|
closeCtx context.Context
|
||||||
extendWg sync.WaitGroup
|
closeCancel context.CancelFunc
|
||||||
|
extendWg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Locker = &redisLocker{}
|
var _ Locker = &redisLocker{}
|
||||||
@@ -40,7 +40,7 @@ func NewRedisLocker(connection string) Locker {
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
rs: redsync.New(goredis.NewPool(conn)),
|
rs: redsync.New(goredis.NewPool(conn)),
|
||||||
}
|
}
|
||||||
l.extendWg.Add(1)
|
l.closeCtx, l.closeCancel = context.WithCancel(context.Background())
|
||||||
l.startExtend()
|
l.startExtend()
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
@@ -65,15 +65,14 @@ func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, ReleaseFun
|
|||||||
// It will stop extending the locks and refuse to acquire new locks.
|
// It will stop extending the locks and refuse to acquire new locks.
|
||||||
// In actual use, it is not necessary to call this function.
|
// In actual use, it is not necessary to call this function.
|
||||||
// But it's useful in tests to release resources.
|
// But it's useful in tests to release resources.
|
||||||
// It could take some time since it waits for the extending goroutine to finish.
|
|
||||||
func (l *redisLocker) Close() error {
|
func (l *redisLocker) Close() error {
|
||||||
l.closed.Store(true)
|
l.closeCancel()
|
||||||
l.extendWg.Wait()
|
l.extendWg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *redisLocker) lock(ctx context.Context, key string, tries int) (ReleaseFunc, error) {
|
func (l *redisLocker) lock(ctx context.Context, key string, tries int) (ReleaseFunc, error) {
|
||||||
if l.closed.Load() {
|
if l.closeCtx.Err() != nil {
|
||||||
return func() {}, errors.New("locker is closed")
|
return func() {}, errors.New("locker is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,11 +103,22 @@ func (l *redisLocker) lock(ctx context.Context, key string, tries int) (ReleaseF
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *redisLocker) startExtend() {
|
func (l *redisLocker) startExtend() {
|
||||||
if l.closed.Load() {
|
timer := time.NewTimer(redisLockExpiry / 2)
|
||||||
l.extendWg.Done()
|
l.extendWg.Go(func() {
|
||||||
return
|
defer timer.Stop()
|
||||||
}
|
for {
|
||||||
|
select {
|
||||||
|
case <-l.closeCtx.Done():
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
l.extendLocks()
|
||||||
|
timer.Reset(redisLockExpiry / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *redisLocker) extendLocks() {
|
||||||
toExtend := make([]*redsync.Mutex, 0)
|
toExtend := make([]*redsync.Mutex, 0)
|
||||||
l.mutexM.Range(func(_, value any) bool {
|
l.mutexM.Range(func(_, value any) bool {
|
||||||
m := value.(*redsync.Mutex) //nolint:forcetypeassert // mutexM only ever holds *redsync.Mutex
|
m := value.(*redsync.Mutex) //nolint:forcetypeassert // mutexM only ever holds *redsync.Mutex
|
||||||
@@ -126,8 +136,6 @@ func (l *redisLocker) startExtend() {
|
|||||||
})
|
})
|
||||||
for _, v := range toExtend {
|
for _, v := range toExtend {
|
||||||
// If it failed to extend, it will be released automatically after the lock expires.
|
// If it failed to extend, it will be released automatically after the lock expires.
|
||||||
_, _ = v.Extend()
|
_, _ = v.ExtendContext(l.closeCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.AfterFunc(redisLockExpiry/2, l.startExtend)
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user