133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"net/http"
133728e barerepo 1mo
7
"net/http/httptest"
133728e barerepo 1mo
8
"os/exec"
133728e barerepo 1mo
9
"sort"
133728e barerepo 1mo
10
"strings"
133728e barerepo 1mo
11
"testing"
133728e barerepo 1mo
12
"time"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/cache"
133728e barerepo 1mo
15
"github.com/barerepo/server/internal/gitread"
133728e barerepo 1mo
16
"github.com/barerepo/server/internal/httpd"
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/repocfg"
133728e barerepo 1mo
18
runpkg "github.com/barerepo/server/internal/run"
133728e barerepo 1mo
19
"github.com/barerepo/server/internal/search"
133728e barerepo 1mo
20
"github.com/barerepo/server/internal/thread"
133728e barerepo 1mo
21
"github.com/barerepo/server/internal/transport"
133728e barerepo 1mo
24
// Chapter 25's budget as build-failing thresholds, in server-side render time only.
133728e barerepo 1mo
25
var budget = []struct {
133728e barerepo 1mo
26
page string
133728e barerepo 1mo
27
path string
133728e barerepo 1mo
28
time time.Duration
133728e barerepo 1mo
29
payload int
133728e barerepo 1mo
31
{"file tree", "/john/big/files/master/src", 10 * time.Millisecond, 15 << 10},
133728e barerepo 1mo
32
{"log", "/john/big", 20 * time.Millisecond, 30 << 10},
133728e barerepo 1mo
33
{"file view with blame", "/john/big/file/master/src/large.go", 20 * time.Millisecond, 40 << 10},
133728e barerepo 1mo
34
{"threads list", "/john/big/threads", 10 * time.Millisecond, 15 << 10},
133728e barerepo 1mo
35
{"one thread", "/john/big/thread/1", 10 * time.Millisecond, 15 << 10},
133728e barerepo 1mo
36
{"releases", "/john/big/releases", 10 * time.Millisecond, 15 << 10},
133728e barerepo 1mo
37
{"search", "/search?q=file0500", 10 * time.Millisecond, 15 << 10},
133728e barerepo 1mo
38
{"runs", "/john/big/runs", 10 * time.Millisecond, 15 << 10},
133728e barerepo 1mo
39
// The run, commit and profile pages are over in SPEC-NOTES and are not listed yet, because a row that fails on purpose tells the suite nothing.
133728e barerepo 1mo
42
// TestBudget fails on purpose, because a threshold raised to match the code measures nothing.
133728e barerepo 1mo
43
func TestBudget(t *testing.T) {
133728e barerepo 1mo
44
if testing.Short() {
133728e barerepo 1mo
45
t.Skip("building a large repository takes a while")
133728e barerepo 1mo
47
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
48
t.Skip("git is not installed")
133728e barerepo 1mo
50
in := newInstance(t)
133728e barerepo 1mo
51
in.account("john")
133728e barerepo 1mo
52
dir := buildBig(t, in, "john", "big", 1000, 200)
133728e barerepo 1mo
53
buildThreads(t, dir, 50)
133728e barerepo 1mo
54
buildTags(t, dir, 100)
133728e barerepo 1mo
55
// The sweep gcs every repository, which packs its refs, so that is the state to measure in.
133728e barerepo 1mo
56
run(t, dir, "git", "pack-refs", "--all")
133728e barerepo 1mo
57
indexBig(t, in, dir, "john", "big")
133728e barerepo 1mo
58
// A build that said a great deal, because a quiet one measures the chrome and not the page.
133728e barerepo 1mo
59
head := strings.TrimSpace(run(t, dir, "git", "rev-parse", "master"))
133728e barerepo 1mo
60
buildRuns(t, in, dir, "john/big", head)
133728e barerepo 1mo
61
for i := range budget {
133728e barerepo 1mo
62
budget[i].path = strings.ReplaceAll(budget[i].path, "HEADSHA", head)
133728e barerepo 1mo
65
shared := cache.New(in.cfg.Paths.Cache)
133728e barerepo 1mo
66
gitread.Cache = shared
133728e barerepo 1mo
67
repocfg.Cache = shared
133728e barerepo 1mo
68
thread.Cache = shared
133728e barerepo 1mo
69
httpd.Cache = shared
133728e barerepo 1mo
70
srv := &httpd.Server{Cfg: in.cfg, DB: in.db,
133728e barerepo 1mo
71
Transport: &transport.Server{Cfg: in.cfg, DB: in.db, Bin: binary}}
133728e barerepo 1mo
72
handler := srv.Handler()
133728e barerepo 1mo
74
for _, b := range budget {
133728e barerepo 1mo
75
// Warm once, because the budget is what a reader waits for, not a cold start.
133728e barerepo 1mo
76
record(handler, b.path)
133728e barerepo 1mo
78
var best time.Duration
133728e barerepo 1mo
79
var size int
133728e barerepo 1mo
80
for i := 0; i < 5; i++ {
133728e barerepo 1mo
81
start := time.Now()
133728e barerepo 1mo
82
rec := record(handler, b.path)
133728e barerepo 1mo
83
took := time.Since(start)
133728e barerepo 1mo
84
if i == 0 || took < best {
133728e barerepo 1mo
85
best = took
133728e barerepo 1mo
87
size = rec.Body.Len()
133728e barerepo 1mo
88
if rec.Code != http.StatusOK {
133728e barerepo 1mo
89
t.Fatalf("%s: %s answered %d", b.page, b.path, rec.Code)
133728e barerepo 1mo
92
if best > b.time {
133728e barerepo 1mo
93
t.Errorf("%s took %s, over the %s budget in chapter 25", b.page, best.Round(time.Millisecond), b.time)
133728e barerepo 1mo
95
if size > b.payload {
133728e barerepo 1mo
96
t.Errorf("%s sent %dkb, over the %dkb budget", b.page, size>>10, b.payload>>10)
133728e barerepo 1mo
98
t.Logf("%-22s %6s %5dkb (budget %s, %dkb)",
133728e barerepo 1mo
99
b.page, best.Round(100*time.Microsecond), size>>10, b.time, b.payload>>10)
133728e barerepo 1mo
100
}
133728e barerepo 1mo
103
func record(h http.Handler, path string) *httptest.ResponseRecorder {
133728e barerepo 1mo
104
req := httptest.NewRequest(http.MethodGet, path, nil)
133728e barerepo 1mo
105
rec := httptest.NewRecorder()
133728e barerepo 1mo
106
h.ServeHTTP(rec, req)
133728e barerepo 1mo
107
return rec
133728e barerepo 1mo
110
// buildBig uses plumbing, because a working tree would measure git's index rather than barerepo.
133728e barerepo 1mo
111
func buildBig(t *testing.T, in *instance, owner, name string, files, commits int) string {
133728e barerepo 1mo
112
t.Helper()
133728e barerepo 1mo
113
work := t.TempDir()
133728e barerepo 1mo
114
run(t, work, "git", "init", "-q", "--bare", "-b", "master")
133728e barerepo 1mo
116
// One blob reused, so the setup is about tree and commit count, not a thousand files.
133728e barerepo 1mo
117
var entries strings.Builder
133728e barerepo 1mo
118
for i := 0; i < files; i++ {
133728e barerepo 1mo
119
body := fmt.Sprintf("package src\n\n// file %d\nfunc F%d() int {\n\treturn %d\n}\n", i, i, i)
133728e barerepo 1mo
120
entries.WriteString("100644 blob " + hashObject(t, work, body) + "\t" + fmt.Sprintf("file%04d.go", i) + "\n")
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
// One file long enough to measure blame on, since a page of six lines measures the chrome.
133728e barerepo 1mo
123
var large strings.Builder
133728e barerepo 1mo
124
large.WriteString("package src\n\n")
133728e barerepo 1mo
125
for i := 0; i < 800; i++ {
133728e barerepo 1mo
126
fmt.Fprintf(&large, "func Large%d() string { return \"line %d of a file somebody reads\" }\n", i, i)
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
entries.WriteString("100644 blob " + hashObject(t, work, large.String()) + "\tlarge.go\n")
133728e barerepo 1mo
129
srcTree := mktree(t, work, entries.String())
133728e barerepo 1mo
130
config := hashObject(t, work, "[repo]\nvisibility = \"public\"\n")
133728e barerepo 1mo
132
// current carries every blob forward, or a commit reverts what the one before it wrote.
133728e barerepo 1mo
133
current := map[string]string{}
133728e barerepo 1mo
134
for _, line := range strings.Split(strings.TrimRight(entriesOf(t, work, srcTree), "\n"), "\n") {
133728e barerepo 1mo
135
if line == "" {
133728e barerepo 1mo
136
continue
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
current[line[strings.LastIndex(line, "\t")+1:]] = strings.Fields(line)[2]
133728e barerepo 1mo
139
}
133728e barerepo 1mo
141
parent := ""
133728e barerepo 1mo
142
for c := 0; c < commits; c++ {
133728e barerepo 1mo
143
// Three files change by ten lines each, which is under the per-commit collapse rule.
133728e barerepo 1mo
144
for k := 0; k < 3; k++ {
133728e barerepo 1mo
145
name := fmt.Sprintf("file%04d.go", (c*3+k)%files)
133728e barerepo 1mo
146
var body strings.Builder
133728e barerepo 1mo
147
fmt.Fprintf(&body, "package src\n\n// %s\n", name)
133728e barerepo 1mo
148
for line := 0; line < 40; line++ {
133728e barerepo 1mo
149
value := line
133728e barerepo 1mo
150
if line >= 10 && line < 20 {
133728e barerepo 1mo
151
value = line * (c + 1)
133728e barerepo 1mo
152
}
133728e barerepo 1mo
153
fmt.Fprintf(&body, "func F%d() int { return %d }\n", line, value)
133728e barerepo 1mo
154
}
133728e barerepo 1mo
155
current[name] = hashObject(t, work, body.String())
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
names := make([]string, 0, len(current))
133728e barerepo 1mo
158
for name := range current {
133728e barerepo 1mo
159
names = append(names, name)
133728e barerepo 1mo
160
}
133728e barerepo 1mo
161
sort.Strings(names)
133728e barerepo 1mo
162
var e strings.Builder
133728e barerepo 1mo
163
for _, name := range names {
133728e barerepo 1mo
164
e.WriteString("100644 blob " + current[name] + "\t" + name + "\n")
133728e barerepo 1mo
165
}
133728e barerepo 1mo
166
tree := mktree(t, work,
133728e barerepo 1mo
167
"040000 tree "+mktree(t, work, e.String())+"\tsrc\n"+
133728e barerepo 1mo
168
"040000 tree "+mktree(t, work, "100644 blob "+config+"\tconfig\n")+"\t.barerepo\n")
133728e barerepo 1mo
169
args := []string{"commit-tree", tree, "-m", fmt.Sprintf("change %d", c)}
133728e barerepo 1mo
170
if parent != "" {
133728e barerepo 1mo
171
args = append(args, "-p", parent)
133728e barerepo 1mo
172
}
133728e barerepo 1mo
173
parent = run(t, work, "git", args...)
133728e barerepo 1mo
174
}
133728e barerepo 1mo
175
run(t, work, "git", "update-ref", "refs/heads/master", parent)
133728e barerepo 1mo
177
dst := repoDir(t, in, owner, name)
133728e barerepo 1mo
178
run(t, "", "git", "clone", "-q", "--bare", work, dst)
133728e barerepo 1mo
179
run(t, dst, "git", "symbolic-ref", "HEAD", "refs/heads/master")
133728e barerepo 1mo
180
if _, err := in.db.ExecContext(t.Context(),
133728e barerepo 1mo
181
`INSERT INTO repos (owner, name, created_at) VALUES (?, ?, ?)`, owner, name, 0); err != nil {
133728e barerepo 1mo
182
t.Fatal(err)
133728e barerepo 1mo
183
}
133728e barerepo 1mo
184
return dst
133728e barerepo 1mo
187
func entriesOf(t *testing.T, dir, tree string) string {
133728e barerepo 1mo
188
t.Helper()
133728e barerepo 1mo
189
return run(t, dir, "git", "ls-tree", tree)
133728e barerepo 1mo
192
// buildThreads writes enough discussion to measure, since the old cost was per thread.
133728e barerepo 1mo
193
func buildThreads(t *testing.T, dir string, count int) {
133728e barerepo 1mo
194
t.Helper()
133728e barerepo 1mo
195
ctx := context.Background()
133728e barerepo 1mo
196
tip := strings.TrimSpace(run(t, dir, "git", "rev-parse", "master"))
133728e barerepo 1mo
197
for n := 1; n <= count; n++ {
133728e barerepo 1mo
198
meta := thread.Meta{
133728e barerepo 1mo
199
Title: fmt.Sprintf("thread number %d", n), State: thread.Open,
133728e barerepo 1mo
200
Author: "lisa", Opened: time.Now(),
133728e barerepo 1mo
201
}
133728e barerepo 1mo
202
if err := thread.Write(ctx, dir, n, "open", func(tr *thread.Tree) { tr.Meta = meta }); err != nil {
133728e barerepo 1mo
203
t.Fatal(err)
133728e barerepo 1mo
204
}
133728e barerepo 1mo
205
// The first thread is the one the budget reads, so it carries a discussion and not a stub.
133728e barerepo 1mo
206
replies := 3
133728e barerepo 1mo
207
if n == 1 {
133728e barerepo 1mo
208
replies = 40
133728e barerepo 1mo
209
}
133728e barerepo 1mo
210
for i := 0; i < replies; i++ {
133728e barerepo 1mo
211
c := thread.Comment{Author: "mark", Time: time.Now(), Body: fmt.Sprintf(