perf(pr,issue): load the interactive create form data concurrently

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-18 18:59:59 +02:00
1 parent 517d164d8a
commit 9360f39cc8
7 files changed
+239 -38

No files matched your search

@@ -0,0 +1,136 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package services
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPullCreateChoicesFetchRepositoryOnce(t *testing.T) {
for _, push := range []bool{false, true} {
t.Run(fmt.Sprintf("push=%t", push), func(t *testing.T) {
var mu sync.Mutex
calls := map[string]int{}
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
calls[r.URL.Path]++
mu.Unlock()
switch {
case r.URL.Path == "/api/v1/orgs/alice":
w.WriteHeader(http.StatusNotFound)
case r.URL.Path == "/api/v1/repos/alice/project":
_, _ = fmt.Fprintf(w, `{"full_name":"alice/project","default_branch":"trunk","permissions":{"push":%t}}`, push)
case strings.HasPrefix(r.URL.Path, "/api/v1/repos/alice/project/raw/"):
assert.Equal(t, "trunk", r.URL.Query().Get("ref"))
if r.URL.Path != "/api/v1/repos/alice/project/raw/.gitea/PULL_REQUEST_TEMPLATE.md" {
w.WriteHeader(http.StatusNotFound)
return
}
_, _ = w.Write([]byte("## Checklist\n"))
case r.URL.Path == "/api/v1/repos/alice/project/assignees":
_, _ = w.Write([]byte(`[{"login":"alice"}]`))
case r.URL.Path == "/api/v1/repos/alice/project/milestones" && r.URL.Query().Get("page") == "1":
_, _ = w.Write([]byte(`[{"id":1,"title":"v1"}]`))
case r.URL.Path == "/api/v1/repos/alice/project/labels" && r.URL.Query().Get("page") == "1":
_, _ = w.Write([]byte(`[{"id":2,"name":"bug"}]`))
case r.URL.Path == "/api/v1/repos/alice/project/milestones", r.URL.Path == "/api/v1/repos/alice/project/labels":
_, _ = w.Write([]byte(`[]`))
default:
t.Errorf("unexpected path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
choices, err := (Pulls{Client: client, Repository: "alice/project"}).CreateChoices(context.Background())
require.NoError(t, err)
require.Equal(t, "alice/project", choices.Repository.FullName)
require.Equal(t, "trunk", choices.Repository.DefaultBranch)
require.Equal(t, "## Checklist\n", choices.Template)
want := CreateSelectables{Assignees: []string{}, Milestones: []Milestone{}, Labels: []Label{}}
if push {
want = CreateSelectables{
CanPush: true,
Assignees: []string{"alice"},
Milestones: []Milestone{{ID: 1, Title: "v1"}},
Labels: []Label{{ID: 2, Name: "bug"}},
}
}
require.Equal(t, want, choices.CreateSelectables)
require.Equal(t, 1, calls["/api/v1/repos/alice/project"])
if !push {
require.Zero(t, calls["/api/v1/repos/alice/project/assignees"])
}
})
}
}
func TestPullCreateChoicesLoadConcurrently(t *testing.T) {
// The template, assignees, milestones, and labels requests only complete once all four are in flight.
paths := []string{"raw/PULL_REQUEST_TEMPLATE.md", "assignees", "milestones", "labels"}
arrived := map[string]chan struct{}{}
for _, path := range paths {
arrived[path] = make(chan struct{})
}
var once sync.Map
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/repos/alice/project/")
if r.URL.Path == "/api/v1/orgs/alice" {
w.WriteHeader(http.StatusNotFound)
return
}
if r.URL.Path == "/api/v1/repos/alice/project" {
_, _ = w.Write([]byte(`{"full_name":"alice/project","default_branch":"main","permissions":{"push":true}}`))
return
}
if ch, ok := arrived[path]; ok {
if _, loaded := once.LoadOrStore(path, true); !loaded {
close(ch)
}
for _, other := range paths {
select {
case <-arrived[other]:
case <-time.After(5 * time.Second):
t.Errorf("%s did not run concurrently with %s", other, path)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}
if strings.HasPrefix(path, "raw/") {
_, _ = w.Write([]byte("template"))
return
}
_, _ = w.Write([]byte(`[]`))
}))
choices, err := (Pulls{Client: client, Repository: "alice/project"}).CreateChoices(context.Background())
require.NoError(t, err)
require.Equal(t, "template", choices.Template)
}
func TestPullCreateChoicesReportFailures(t *testing.T) {
for _, failing := range []string{"/api/v1/repos/alice/project", "/api/v1/repos/alice/project/labels", "/api/v1/repos/alice/project/raw/PULL_REQUEST_TEMPLATE.md"} {
t.Run(failing, func(t *testing.T) {
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == failing:
w.WriteHeader(http.StatusInternalServerError)
case r.URL.Path == "/api/v1/repos/alice/project":
_, _ = w.Write([]byte(`{"full_name":"alice/project","default_branch":"main","permissions":{"push":true}}`))
default:
_, _ = w.Write([]byte(`[]`))
}
}))
_, err := (Pulls{Client: client, Repository: "alice/project"}).CreateChoices(context.Background())
require.ErrorContains(t, err, "500")
})
}
}