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:
authored and GitHub committed 2026-10-26 11:22:44 +00:00
1 parent e221551c7b
commit 85211296dd
6 files changed
+57 -26

No files matched your search

+2
View File
@@ -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
+1 -1
View File
@@ -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
} }
+11
View File
@@ -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())
} }
+5 -6
View File
@@ -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)
})
}) })
} }
+15 -4
View File
@@ -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")
+23 -15
View File
@@ -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)
} }