internal/httpd/buildpages_test.go
256 lines · 9482 bytes
1package httpd
2
3import (
4 "gitbay.org/gitbay/internal/control"
5 "strings"
6 "testing"
7
8 "gitbay.org/gitbay/internal/store"
9 "gitbay.org/gitbay/internal/web"
10)
11
12func testRepoPage() repoPage {
13 var p repoPage
14 p.Repo = store.Repo{OwnerName: "krz", Name: "gitbay", DefaultBranch: "main"}
15 p.Host = "gitbay.org"
16 return p
17}
18
19// The build pages read their data from the build commands' JSON now, not
20// from store.Build. A renamed field would be a blank cell rather than a
21// compile error, so render both pages and look for the values.
22func TestBuildsPageRendersCommandOutput(t *testing.T) {
23 var sb strings.Builder
24 builds := []control.BuildOut{{
25 Number: 60, Job: "build", Status: "success",
26 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b",
27 Ref: "cli-coverage", CreatedAt: "2026-08-28T04:42:54Z",
28 }}
29 jobs := []control.JobOut{{Name: "build"}, {Name: "nightly", Schedule: "0 3 * * *"}}
30 filter := buildFilter{}
31 err := web.Render(&sb, "builds.html", struct {
32 repoPage
33 Builds []control.BuildOut
34 Jobs []control.JobOut
35 Runs []buildRun
36 Filter buildFilter
37 Facets []facetGroup
38 Refs []string
39 Older string
40 CanWrite bool
41 Notice string
42 }{
43 testRepoPage(), builds, jobs, groupRuns(builds), filter, nil,
44 distinctRefs(builds, filter.Ref), "", true, "",
45 })
46 if err != nil {
47 t.Fatalf("render: %v", err)
48 }
49 out := sb.String()
50 for _, want := range []string{
51 "build", "success", "cli-coverage", "ff6271a9d4",
52 `value="build"`, `value="nightly"`, "schedule 0 3 * * *",
53 } {
54 if !strings.Contains(out, want) {
55 t.Errorf("builds.html missing %q", want)
56 }
57 }
58}
59
60func TestBuildPageRendersCommandOutput(t *testing.T) {
61 var sb strings.Builder
62 err := web.Render(&sb, "build.html", buildView{
63 repoPage: testRepoPage(),
64 Build: control.BuildOut{
65 Number: 60, Job: "build", Status: "success",
66 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b", Ref: "cli-coverage",
67 CreatedAt: "2026-08-28T04:42:54Z", FinishedAt: "2026-08-28T04:43:06Z",
68 },
69 Log: "step 1 ok",
70 CanWrite: true,
71 })
72 if err != nil {
73 t.Fatalf("render: %v", err)
74 }
75 out := sb.String()
76 for _, want := range []string{
77 "Build 60", "success", "build on cli-coverage", "ff6271a9d4",
78 "2026-08-28 04:42", "2026-08-28 04:43", "step 1 ok",
79 } {
80 if !strings.Contains(out, want) {
81 t.Errorf("build.html missing %q", want)
82 }
83 }
84 // A finished build offers no cancel control, even to a writer.
85 if strings.Contains(out, "/cancel") {
86 t.Error("build.html offers cancel on a finished build")
87 }
88}
89
90// The count under the heading said "15 runs" while listing 23 builds
91// (#240). It names both numbers now, so neither is mistaken for the other.
92func TestBuildsPageCountsBuildsAndRuns(t *testing.T) {
93 builds := []control.BuildOut{
94 {Number: 4, Job: "instances", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-20T06:00:00Z"},
95 {Number: 3, Job: "instances", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"},
96 {Number: 2, Job: "lint", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-12T11:20:03Z"},
97 {Number: 1, Job: "unit", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-12T11:20:03Z"},
98 }
99 var sb strings.Builder
100 filter := buildFilter{}
101 err := web.Render(&sb, "builds.html", struct {
102 repoPage
103 Builds []control.BuildOut
104 Jobs []control.JobOut
105 Runs []buildRun
106 Filter buildFilter
107 Facets []facetGroup
108 Refs []string
109 Older string
110 CanWrite bool
111 Notice string
112 }{
113 testRepoPage(), builds, nil, groupRuns(builds), filter, nil,
114 distinctRefs(builds, filter.Ref), "", true, "",
115 })
116 if err != nil {
117 t.Fatalf("render: %v", err)
118 }
119 if !strings.Contains(sb.String(), "4 builds in 3 runs") {
120 t.Errorf("builds.html count line: %q", sb.String())
121 }
122}
123
124// A run row led with a ten-character sha and nothing said what the commit
125// was (#241). The subject leads now, the sha follows as metadata, and a
126// build whose commit is gone falls back to the sha alone.
127func TestBuildsPageLeadsWithTheSubject(t *testing.T) {
128 builds := []control.BuildOut{
129 {Number: 2, Job: "unit", Status: "success", SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b",
130 Ref: "main", CreatedAt: "2026-09-20T06:00:00Z", Subject: "runner: cap a build's container"},
131 {Number: 1, Job: "unit", Status: "failure", SHA: "aa11bb22cc33dd44ee55ff6677889900aabbccdd",
132 Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"},
133 }
134 out := renderBuilds(t, builds, buildFilter{}, "?cursor=abc")
135 if !strings.Contains(out, "runner: cap a build's container") {
136 t.Errorf("builds.html does not lead with the subject:\n%s", out)
137 }
138 if !strings.Contains(out, "<code>ff6271a9d4</code>") {
139 t.Errorf("builds.html drops the sha from the metadata line:\n%s", out)
140 }
141 // The commit is gone, so the sha is all there is to name the row by.
142 if !strings.Contains(out, ">aa11bb22cc</a>") {
143 t.Errorf("a subjectless run does not fall back to its sha:\n%s", out)
144 }
145}
146
147// Filtering appeared to raise the run count because the list was capped
148// at 50 with the window unstated (#244). The page is paged now: the count
149// says it counts this page, and the link to the next one carries every
150// filter.
151func TestBuildsPagePagesAndSaysSo(t *testing.T) {
152 builds := []control.BuildOut{{Number: 1, Job: "unit", Status: "failure", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"}}
153 out := renderBuilds(t, builds, buildFilter{Status: "failure"}, "?cursor=abc&status=failure")
154 if !strings.Contains(out, "1 build in 1 run on this page") {
155 t.Errorf("count line does not name the page:\n%s", out)
156 }
157 if !strings.Contains(out, `href="?cursor=abc&status=failure"`) {
158 t.Errorf("pager link missing or drops the filter:\n%s", out)
159 }
160 // With everything on one page the count is the whole count.
161 if out := renderBuilds(t, builds, buildFilter{}, ""); strings.Contains(out, "on this page") {
162 t.Errorf("an unpaged listing still hedges the count:\n%s", out)
163 }
164}
165
166func TestOlderBuildsCarriesFilters(t *testing.T) {
167 if got := olderBuilds(buildFilter{Ref: "main"}, ""); got != "" {
168 t.Errorf("no next cursor should mean no link, got %q", got)
169 }
170 got := olderBuilds(buildFilter{Ref: "feature/x", Status: "failure", Job: "unit"}, "c1")
171 for _, want := range []string{"cursor=c1", "ref=feature%2Fx", "status=failure", "job=unit"} {
172 if !strings.Contains(got, want) {
173 t.Errorf("olderBuilds = %q, missing %q", got, want)
174 }
175 }
176}
177
178func renderBuilds(t *testing.T, builds []control.BuildOut, filter buildFilter, older string) string {
179 t.Helper()
180 var sb strings.Builder
181 err := web.Render(&sb, "builds.html", struct {
182 repoPage
183 Builds []control.BuildOut
184 Jobs []control.JobOut
185 Runs []buildRun
186 Filter buildFilter
187 Facets []facetGroup
188 Refs []string
189 Older string
190 CanWrite bool
191 Notice string
192 }{
193 testRepoPage(), builds, nil, groupRuns(builds), filter, nil,
194 distinctRefs(builds, filter.Ref), older, true, "",
195 })
196 if err != nil {
197 t.Fatalf("render: %v", err)
198 }
199 return sb.String()
200}
201
202// A failed build's page folds its log by step, opens the step that
203// failed and links to it; no JavaScript (#266).
204func TestBuildPageFoldsStepsAndOpensFailure(t *testing.T) {
205 b := control.BuildOut{Number: 61, Job: "test", Status: "failure",
206 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b", Ref: "main",
207 CreatedAt: "2026-08-28T04:42:54Z", FinishedAt: "2026-08-28T04:53:50Z", DurationS: 656,
208 Steps: []string{"go build ./...", "go test ./..."}, FailedStep: 2, FailedReason: "exit 1"}
209 log := "$ git clone x (ff6271a9d4)\n$ go build ./...\n$ go test ./...\n--- FAIL: TestCLI\nstep 2/2 failed: exit 1\n"
210 v := buildView{repoPage: testRepoPage(), Build: b, Log: log, Duration: "10m56s"}
211 v.Steps, v.Failed = logSteps(log, b)
212 var sb strings.Builder
213 if err := web.Render(&sb, "build.html", v); err != nil {
214 t.Fatalf("render: %v", err)
215 }
216 out := sb.String()
217 for _, want := range []string{
218 `<details class="difffold buildstep" id="failed" open>`,
219 "step 2/2", "<code>go test ./...</code>", `href="#failed"`, "Jump to failure",
220 "ran 10m56s", "--- FAIL: TestCLI",
221 } {
222 if !strings.Contains(out, want) {
223 t.Errorf("build.html missing %q", want)
224 }
225 }
226 if n := strings.Count(out, `class="difffold buildstep"`); n != 3 {
227 t.Errorf("%d step folds, want 3 (setup and two steps)", n)
228 }
229 if n := strings.Count(out, `id="failed"`); n != 1 {
230 t.Errorf("%d failed anchors, want 1", n)
231 }
232}
233
234// A multi-line step's fold summary shows only its first line, as build
235// show does; the whole step is in the log (#266).
236func TestBuildPageStepSummaryIsFirstLine(t *testing.T) {
237 step := "set -e\ngo test ./..."
238 b := control.BuildOut{Number: 62, Job: "test", Status: "failure",
239 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b", Ref: "main",
240 CreatedAt: "2026-08-28T04:42:54Z", FinishedAt: "2026-08-28T04:43:54Z", DurationS: 60,
241 Steps: []string{step}, FailedStep: 1, FailedReason: "exit 1"}
242 log := "$ git clone x (ff6271a9d4)\n$ " + step + "\n--- FAIL: TestCLI\nstep 1/1 failed: exit 1\n"
243 v := buildView{repoPage: testRepoPage(), Build: b, Log: log, Duration: "1m0s"}
244 v.Steps, v.Failed = logSteps(log, b)
245 var sb strings.Builder
246 if err := web.Render(&sb, "build.html", v); err != nil {
247 t.Fatalf("render: %v", err)
248 }
249 out := sb.String()
250 if !strings.Contains(out, "<code>set -e</code>") {
251 t.Errorf("summary does not show the step's first line:\n%s", out)
252 }
253 if strings.Contains(out, "<code>set -e\ngo test") {
254 t.Errorf("summary shows the whole multi-line step")
255 }
256}