snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:41:59 +05:00
commit 52ae0c5802
6280 changed files with 624193 additions and 0 deletions

87
modules/test/file.go Normal file
View File

@@ -0,0 +1,87 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package test
import (
"os"
"path/filepath"
"gitea.dev/modules/util"
)
type FileHelper struct {
baseDir string
}
func NewFileHelper(baseDir string) *FileHelper {
return &FileHelper{baseDir: baseDir}
}
func (h *FileHelper) fullPath(path string) string {
return filepath.Join(h.baseDir, path)
}
func (h *FileHelper) AssertExists(t TestingT, path string, expected bool) {
t.Helper()
_, err := os.Stat(h.fullPath(path))
errIsNotExist := os.IsNotExist(err)
if expected != !errIsNotExist {
t.Fatalf("file %s existence expected %v", path, expected)
}
}
func (h *FileHelper) AssertFileExists(t TestingT, path string, expected *string) {
t.Helper()
if expected == nil {
h.AssertExists(t, path, false)
return
}
h.AssertFileContent(t, path, *expected)
}
func (h *FileHelper) AssertFileContent(t TestingT, path, expected string) {
t.Helper()
data, err := os.ReadFile(h.fullPath(path))
if err != nil {
t.Fatalf("failed to read file %s: %v", path, err)
}
if string(data) != expected {
t.Fatalf("file %s expected %q but got %q", path, expected, string(data))
}
}
func (h *FileHelper) AssertSymLink(t TestingT, path, expected string) {
t.Helper()
link, err := os.Readlink(h.fullPath(path))
if err != nil {
t.Fatalf("failed to read symlink %s: %v", path, err)
}
if link != h.fullPath(expected) {
t.Fatalf("symlink %s expected %q but got %q", path, expected, link)
}
}
func (h *FileHelper) WriteFile(t TestingT, path, content string, optMode ...os.FileMode) {
t.Helper()
err := os.WriteFile(h.fullPath(path), []byte(content), util.OptionalArg(optMode, 0o644))
if err != nil {
t.Fatalf("failed to write file %s: %v", path, err)
}
}
func (h *FileHelper) MkdirAll(t TestingT, path string, optMode ...os.FileMode) {
t.Helper()
err := os.MkdirAll(h.fullPath(path), util.OptionalArg(optMode, 0o755))
if err != nil {
t.Fatalf("failed to mkdir all %s: %v", path, err)
}
}
func (h *FileHelper) Symlink(t TestingT, oldName, newName string) {
t.Helper()
err := os.Symlink(h.fullPath(oldName), h.fullPath(newName))
if err != nil {
t.Fatalf("failed to symlink from %s to %s: %v", oldName, newName, err)
}
}

105
modules/test/logchecker.go Normal file
View File

@@ -0,0 +1,105 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package test
import (
"context"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"gitea.dev/modules/log"
)
type LogChecker struct {
*log.EventWriterBaseImpl
filterMessages []string
filtered []bool
stopMark string
stopped bool
mu sync.Mutex
}
func (lc *LogChecker) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case event, ok := <-lc.Queue:
if !ok {
return
}
lc.checkLogEvent(event)
}
}
}
func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) {
lc.mu.Lock()
defer lc.mu.Unlock()
for i, msg := range lc.filterMessages {
if strings.Contains(event.Origin.MsgSimpleText, msg) {
lc.filtered[i] = true
}
}
if strings.Contains(event.Origin.MsgSimpleText, lc.stopMark) {
lc.stopped = true
}
}
var checkerIndex atomic.Int64
func NewLogChecker(namePrefix string) (logChecker *LogChecker, cancel func()) {
logger := log.GetManager().GetLogger(namePrefix)
newCheckerIndex := checkerIndex.Add(1)
writerName := namePrefix + "-" + strconv.FormatInt(newCheckerIndex, 10)
lc := &LogChecker{}
lc.EventWriterBaseImpl = log.NewEventWriterBase(writerName, "test-log-checker", log.WriterMode{})
logger.AddWriters(lc)
return lc, func() { _ = logger.RemoveWriter(writerName) }
}
// Filter will make the `Check` function to check if these logs are outputted.
func (lc *LogChecker) Filter(msgs ...string) *LogChecker {
lc.mu.Lock()
defer lc.mu.Unlock()
lc.filterMessages = make([]string, len(msgs))
copy(lc.filterMessages, msgs)
lc.filtered = make([]bool, len(lc.filterMessages))
return lc
}
func (lc *LogChecker) StopMark(msg string) *LogChecker {
lc.mu.Lock()
defer lc.mu.Unlock()
lc.stopMark = msg
lc.stopped = false
return lc
}
// Check returns the filtered slice and whether the stop mark is reached.
func (lc *LogChecker) Check(d time.Duration) (filtered []bool, stopped bool) {
stop := time.Now().Add(d)
for {
lc.mu.Lock()
stopped = lc.stopped
lc.mu.Unlock()
if time.Now().After(stop) || stopped {
lc.mu.Lock()
f := make([]bool, len(lc.filtered))
copy(f, lc.filtered)
lc.mu.Unlock()
return f, stopped
}
time.Sleep(10 * time.Millisecond)
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package test
import (
"testing"
"time"
"gitea.dev/modules/log"
"github.com/stretchr/testify/assert"
)
func TestLogChecker(t *testing.T) {
lc, cleanup := NewLogChecker(log.DEFAULT)
defer cleanup()
lc.Filter("First", "Third").StopMark("End")
log.Info("test")
filtered, stopped := lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{false, false}, filtered)
assert.False(t, stopped)
log.Info("First")
filtered, stopped = lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{true, false}, filtered)
assert.False(t, stopped)
log.Info("Second")
filtered, stopped = lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{true, false}, filtered)
assert.False(t, stopped)
log.Info("Third")
filtered, stopped = lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{true, true}, filtered)
assert.False(t, stopped)
log.Info("End")
filtered, stopped = lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{true, true}, filtered)
assert.True(t, stopped)
}

72
modules/test/redis.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package test
import (
"net"
"os"
"os/exec"
"path/filepath"
"time"
)
const testRedisAddr = "127.0.0.1:6379"
// waitRedisReady reports whether redis accepts connections on addr within dur.
// Redis binds its listener last during startup, so a successful dial means it
// can serve. A plain dial, not a redis PING: the client retries its pool on a
// refused connect, which makes the "is one already running" probe take ~1s.
func waitRedisReady(network, addr string, dur time.Duration) bool {
for start := time.Now(); ; time.Sleep(5 * time.Millisecond) {
conn, err := net.DialTimeout(network, addr, time.Second)
if err == nil {
_ = conn.Close()
return true
}
if time.Since(start) > dur {
return false
}
}
}
// PrepareTestRedis returns a connection string to a running redis, reusing one
// already listening on the well-known port, otherwise starting one for the
// duration of the test.
func PrepareTestRedis(t TestingT) string {
if waitRedisReady("tcp", testRedisAddr, 0) {
return "redis://" + testRedisAddr + "/0"
}
redisServerProg, err := exec.LookPath("redis-server")
if err != nil {
if AllowSkipExternalService() {
t.Skipf("redis-server command not found, skipped")
} else {
t.Fatalf("no redis server or command, but skipping is not allowed")
}
return ""
}
// listen on a socket of our own rather than a port, so that packages running
// in parallel can neither reach nor tear down each other's server
dir := t.TempDir()
socket := filepath.Join(dir, "redis.sock")
redisServer := &exec.Cmd{
Path: redisServerProg,
Args: []string{redisServerProg, "--port", "0", "--unixsocket", socket},
Dir: dir,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
if err := redisServer.Start(); err != nil {
t.Fatalf("failed to start redis-server: %v", err)
}
t.Cleanup(func() {
_ = redisServer.Process.Signal(os.Interrupt)
_ = redisServer.Wait()
})
if !waitRedisReady("unix", socket, 5*time.Second) {
t.Fatalf("failed to start redis-server")
}
return "redis+socket://" + socket
}

250
modules/test/utils.go Normal file
View File

@@ -0,0 +1,250 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package test
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"gitea.dev/modules/json"
"gitea.dev/modules/util"
"golang.org/x/net/html"
)
// RedirectURL returns the redirect URL of a http response.
// It also works for JSONRedirect: `{"redirect": "..."}`
// FIXME: it should separate the logic of checking from header and JSON body
func RedirectURL(resp http.ResponseWriter) string {
loc := resp.Header().Get("Location")
if loc != "" {
return loc
}
if r, ok := resp.(*httptest.ResponseRecorder); ok {
m := map[string]any{}
err := json.Unmarshal(r.Body.Bytes(), &m)
if err == nil {
if loc, ok := m["redirect"].(string); ok {
return loc
}
}
}
return ""
}
func ParseJSONError(buf []byte) (ret struct {
ErrorMessage string `json:"errorMessage"`
RenderFormat string `json:"renderFormat"`
},
) {
_ = json.Unmarshal(buf, &ret)
return ret
}
func ParseJSONRedirect(buf []byte) (ret struct {
Redirect *string `json:"redirect"`
},
) {
_ = json.Unmarshal(buf, &ret)
return ret
}
func IsNormalPageCompleted(s string) bool {
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
}
func MockVariableValue[T any](p *T, v ...T) (reset func()) {
old := *p
if len(v) > 0 {
*p = v[0]
}
return func() { *p = old }
}
func ReadAllTarGzContent(r io.Reader) (map[string]string, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
content := make(map[string]string)
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
buf, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
content[hd.Name] = string(buf)
}
return content, nil
}
func WriteZipArchive(files map[string]string) *bytes.Buffer {
buf := &bytes.Buffer{}
zw := zip.NewWriter(buf)
for name, content := range files {
w, _ := zw.Create(name)
_, _ = w.Write([]byte(content))
}
_ = zw.Close()
return buf
}
func WriteTarArchive(files map[string]string) *bytes.Buffer {
return WriteTarCompression(func(w io.Writer) io.WriteCloser { return util.NopCloser{Writer: w} }, files)
}
func WriteTarCompression[WC io.WriteCloser, F func(io.Writer) WC](compression F, files map[string]string) *bytes.Buffer {
// TODO: to support xz.NewWriter which returns (*Writer, error), it needs a separate function due to the limit of Golang generic
buf := &bytes.Buffer{}
cw := compression(buf)
tw := tar.NewWriter(cw)
for name, content := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(content)),
}
_ = tw.WriteHeader(hdr)
_, _ = tw.Write([]byte(content))
}
_ = tw.Close()
_ = cw.Close()
return buf
}
func CompressGzip(content string) *bytes.Buffer {
buf := &bytes.Buffer{}
cw := gzip.NewWriter(buf)
_, _ = cw.Write([]byte(content))
_ = cw.Close()
return buf
}
var AllowSkipExternalService = sync.OnceValue(func() bool {
isLocalTesting := os.Getenv("CI") == ""
ciSkipExternal, _ := strconv.ParseBool(os.Getenv("GITEA_TEST_CI_SKIP_EXTERNAL"))
return isLocalTesting || ciSkipExternal
})
type TestingT interface {
Cleanup(func())
Context() context.Context
Errorf(format string, args ...any)
Fatalf(format string, args ...any)
Helper()
Skipf(format string, args ...any)
TempDir() string
}
var externalServiceCheckResult sync.Map
func ExternalServiceHTTP(t TestingT, envVarName, def string) string {
t.Helper()
extSvc := util.IfZero(os.Getenv(envVarName), def)
if extSvc == "" {
if AllowSkipExternalService() {
t.Skipf("skipping test because %s is not set", envVarName)
} else {
t.Fatalf("%s is not set, but skipping is not allowed in CI", envVarName)
}
}
// only need to check once, if there are saved check result, just use it
lastCheckErrAny, lastCheckExists := externalServiceCheckResult.Load(extSvc)
var lastCheckErr error
if !lastCheckExists {
{
// minio's endpoint is "host:port" pattern
testURL := util.Iif(strings.Contains(extSvc, "://"), extSvc, "http://"+extSvc)
// do a quick check with short timeout
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(testURL)
if err == nil {
_ = resp.Body.Close()
}
lastCheckErr = err
}
externalServiceCheckResult.Store(extSvc, lastCheckErr)
} else {
lastCheckErr, _ = lastCheckErrAny.(error)
}
if lastCheckErr != nil {
if AllowSkipExternalService() {
t.Skipf("skipping test because %s is not ready", extSvc)
} else {
t.Fatalf("%s is not ready, but skipping is not allowed in CI", extSvc)
}
}
return extSvc
}
var normalizeHTMLSpacesRegexp = sync.OnceValue(func() (ret struct {
afterRt, beforeLt *regexp.Regexp
},
) {
ret.afterRt = regexp.MustCompile(`>\s*`)
ret.beforeLt = regexp.MustCompile(`\s*<`)
return ret
})
func NormalizeHTMLSpaces(s string) string {
vars := normalizeHTMLSpacesRegexp()
s = vars.afterRt.ReplaceAllString(s, ">\n")
s = vars.beforeLt.ReplaceAllString(s, "\n<")
return strings.TrimSpace(s)
}
func NormalizeHTMLAttributes(t TestingT, s string) string {
nodes, err := html.Parse(strings.NewReader(s))
if err != nil {
t.Errorf("failed to parse expected HTML: %v", err)
return ""
}
var normalize func(n *html.Node)
normalize = func(n *html.Node) {
slices.SortFunc(n.Attr, func(a, b html.Attribute) int {
if cmp := strings.Compare(a.Namespace, b.Namespace); cmp != 0 {
return cmp
}
if cmp := strings.Compare(a.Key, b.Key); cmp != 0 {
return cmp
}
return strings.Compare(a.Val, b.Val)
})
for c := n.FirstChild; c != nil; c = c.NextSibling {
normalize(c)
}
}
var sb strings.Builder
if err = html.Render(&sb, nodes); err != nil {
t.Errorf("failed to render HTML: %v", err)
}
return sb.String()
}