internal/httpd/builds_test.go
240 lines · 8999 bytes
1package httpd
2
3import (
4 "fmt"
5 "reflect"
6 "testing"
7
8 "gitbay.org/gitbay/internal/control"
9)
10
11// groupRuns folds consecutive same-commit builds (the list is newest
12// first, so a commit's jobs are adjacent) into one run per commit, and
13// gives the run a combined status: worst first (failure beats everything,
14// then cancelled, running, pending), success only when every job is (#224).
15func TestGroupRunsCombinesByCommit(t *testing.T) {
16 builds := []control.BuildOut{
17 {Number: 3, Job: "lint", Status: "success", SHA: "bbb", Ref: "main", CreatedAt: "t2"},
18 {Number: 2, Job: "unit", Status: "failure", SHA: "aaa", Ref: "main", CreatedAt: "t1"},
19 {Number: 1, Job: "lint", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "t1"},
20 }
21 runs := groupRuns(builds)
22 if len(runs) != 2 {
23 t.Fatalf("groupRuns returned %d runs, want 2: %+v", len(runs), runs)
24 }
25 if runs[0].SHA != "bbb" || len(runs[0].Builds) != 1 || runs[0].Status != "success" {
26 t.Errorf("first run: %+v", runs[0])
27 }
28 if runs[1].SHA != "aaa" || len(runs[1].Builds) != 2 || runs[1].Status != "failure" {
29 t.Errorf("second run: %+v", runs[1])
30 }
31 // Order within a run is preserved from the input.
32 if runs[1].Builds[0].Job != "unit" || runs[1].Builds[1].Job != "lint" {
33 t.Errorf("run builds out of order: %+v", runs[1].Builds)
34 }
35}
36
37func TestGroupRunsEmpty(t *testing.T) {
38 if runs := groupRuns(nil); len(runs) != 0 {
39 t.Errorf("groupRuns(nil) = %+v, want empty", runs)
40 }
41}
42
43// Two builds on the same sha but on different refs (a fast-forward merge
44// can leave the commit reachable from more than one branch) are not
45// adjacent unless the list happens to put them there; groupRuns only folds
46// what is actually adjacent, so this documents that a same-sha, same-ref
47// pair from one push is what gets folded, not "any build of this sha ever".
48func TestGroupRunsCombinedStatusPriority(t *testing.T) {
49 cases := []struct {
50 statuses []string
51 want string
52 }{
53 {[]string{"success"}, "success"},
54 {[]string{"success", "pending"}, "pending"},
55 {[]string{"pending", "running"}, "running"},
56 {[]string{"running", "cancelled"}, "cancelled"},
57 {[]string{"cancelled", "failure"}, "failure"},
58 {[]string{"success", "success", "failure"}, "failure"},
59 // A status outside runStatusPriority (a future state such as
60 // "skipped") is still not success: it must not fall through to
61 // the "success" default and read as green.
62 {[]string{"success", "skipped"}, "skipped"},
63 }
64 for _, tc := range cases {
65 var builds []control.BuildOut
66 for _, s := range tc.statuses {
67 builds = append(builds, control.BuildOut{SHA: "x", Status: s})
68 }
69 runs := groupRuns(builds)
70 if len(runs) != 1 || runs[0].Status != tc.want {
71 t.Errorf("statuses %v: combined %+v, want %q", tc.statuses, runs, tc.want)
72 }
73 }
74}
75
76// filterLinks builds the nav.filters row: one link that clears both status
77// and job, one per known status and one per known job, each preserving the
78// other two query parameters and marking itself active (#224).
79func TestFilterLinksPreservesOtherParamsAndMarksActive(t *testing.T) {
80 links := filterLinks(buildFilter{Ref: "main", Status: "success", Job: "lint"},
81 []control.JobOut{{Name: "lint"}, {Name: "unit"}})
82
83 byLabel := map[string]buildFilterLink{}
84 for _, l := range links {
85 byLabel[l.Label] = l
86 }
87 all, ok := byLabel["all"]
88 if !ok {
89 t.Fatal("no \"all\" link")
90 }
91 if all.Active {
92 t.Error(`"all" is active while a status/job filter is set`)
93 }
94 if all.Href != "?ref=main" {
95 t.Errorf(`"all" href = %q, want "?ref=main" (clears status and job, keeps ref)`, all.Href)
96 }
97
98 success, ok := byLabel["success"]
99 if !ok || !success.Active {
100 t.Errorf("success link: %+v, want present and active", success)
101 }
102 if success.Href != "?job=lint&ref=main&status=success" {
103 t.Errorf("success href = %q", success.Href)
104 }
105
106 lint, ok := byLabel["lint"]
107 if !ok || !lint.Active {
108 t.Errorf("lint link: %+v, want present and active", lint)
109 }
110 if lint.Href != "?job=lint&ref=main&status=success" {
111 t.Errorf("lint href = %q", lint.Href)
112 }
113
114 unit, ok := byLabel["unit"]
115 if !ok || unit.Active {
116 t.Errorf("unit link: %+v, want present and inactive", unit)
117 }
118 if unit.Href != "?job=unit&ref=main&status=success" {
119 t.Errorf("unit href = %q", unit.Href)
120 }
121}
122
123// With no filter at all, "all" is the active link.
124func TestFilterLinksAllActiveWhenUnfiltered(t *testing.T) {
125 links := filterLinks(buildFilter{}, nil)
126 for _, l := range links {
127 if l.Label == "all" && !l.Active {
128 t.Error(`"all" is not active with no filter set`)
129 }
130 }
131}
132
133// distinctRefs lists each ref once, in the order builds carry them, and
134// always includes the current filter value even if it matched nothing —
135// it powers the branch field's suggestions, not a strict "what exists" list.
136func TestDistinctRefsDedupesAndIncludesCurrent(t *testing.T) {
137 builds := []control.BuildOut{{Ref: "main"}, {Ref: "feature"}, {Ref: "main"}}
138 got := distinctRefs(builds, "release")
139 want := []string{"main", "feature", "release"}
140 if !reflect.DeepEqual(got, want) {
141 t.Errorf("distinctRefs = %v, want %v", got, want)
142 }
143}
144
145func TestDistinctRefsNoDuplicateWhenCurrentAlreadyPresent(t *testing.T) {
146 builds := []control.BuildOut{{Ref: "main"}}
147 got := distinctRefs(builds, "main")
148 want := []string{"main"}
149 if !reflect.DeepEqual(got, want) {
150 t.Errorf("distinctRefs = %v, want %v", got, want)
151 }
152}
153
154// The builds column groups the same links filterLinks makes: "all" and
155// the statuses, then the jobs, then the branches seen (desktop layout spec).
156func TestBuildFacetsGroups(t *testing.T) {
157 f := buildFilter{Ref: "main", Status: "success"}
158 groups := buildFacets(f, []control.JobOut{{Name: "lint"}}, []string{"main", "dev"})
159 if len(groups) != 3 || groups[0].Title != "Status" || groups[1].Title != "Jobs" || groups[2].Title != "Branches" {
160 t.Fatalf("groups: %+v", groups)
161 }
162 if groups[0].Items[0].Label != "all" || groups[0].Items[0].Href != "?ref=main" || groups[0].Items[0].Active {
163 t.Errorf("all: %+v", groups[0].Items[0])
164 }
165 if s := groups[0].Items[3]; s.Label != "success" || !s.Active {
166 t.Errorf("success: %+v", s)
167 }
168 if j := groups[1].Items[0]; j.Label != "lint" || j.Href != "?job=lint&ref=main&status=success" || j.Active {
169 t.Errorf("lint: %+v", j)
170 }
171 if b := groups[2].Items[0]; b.Label != "main" || !b.Active || b.Href != "?status=success" {
172 t.Errorf("active branch clears itself: %+v", b)
173 }
174 if b := groups[2].Items[1]; b.Label != "dev" || b.Active || b.Href != "?ref=dev&status=success" {
175 t.Errorf("dev: %+v", b)
176 }
177}
178
179// The branch group caps, the way topicFacets caps topics: a page of
180// builds can name dozens of refs and the column is not a branch
181// listing. The ref in force is kept whatever its position (#237).
182func TestBuildFacetsCapsBranches(t *testing.T) {
183 var refs []string
184 for i := 0; i < 30; i++ {
185 refs = append(refs, fmt.Sprintf("b%02d", i))
186 }
187 groups := buildFacets(buildFilter{}, nil, refs)
188 branches := groups[2].Items
189 if len(branches) != maxBranchFacets {
190 t.Fatalf("branches: %d", len(branches))
191 }
192 if branches[0].Label != "b00" || branches[len(branches)-1].Label != "b09" {
193 t.Errorf("kept the wrong refs: %+v", branches)
194 }
195
196 groups = buildFacets(buildFilter{Ref: "b29"}, nil, refs)
197 branches = groups[2].Items
198 if len(branches) != maxBranchFacets {
199 t.Fatalf("branches with an active ref: %d", len(branches))
200 }
201 var active *facetItem
202 for i := range branches {
203 if branches[i].Label == "b29" {
204 active = &branches[i]
205 }
206 }
207 if active == nil || !active.Active || active.Href != "?" {
208 t.Errorf("active ref past the cap: %+v", branches)
209 }
210}
211
212// A scheduled job runs against the same sha every tick for as long as the
213// branch tip does not move, so the sha alone is not the run (#240). Three
214// daily runs of "instances" on one commit are three rows, each with its own
215// timestamp and status; the push that set the tip queued lint and unit
216// together, so those stay one row.
217func TestGroupRunsSeparatesRepeatedSchedule(t *testing.T) {
218 builds := []control.BuildOut{
219 {Number: 5, Job: "instances", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "t5"},
220 {Number: 4, Job: "instances", Status: "failure", SHA: "aaa", Ref: "main", CreatedAt: "t4"},
221 {Number: 3, Job: "instances", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "t3"},
222 {Number: 2, Job: "lint", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "t2"},
223 {Number: 1, Job: "unit", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "t2"},
224 }
225 runs := groupRuns(builds)
226 if len(runs) != 4 {
227 t.Fatalf("groupRuns returned %d runs, want 4: %+v", len(runs), runs)
228 }
229 for i, want := range []struct {
230 when string
231 status string
232 jobs int
233 }{{"t5", "success", 1}, {"t4", "failure", 1}, {"t3", "success", 1}, {"t2", "success", 2}} {
234 got := runs[i]
235 if got.CreatedAt != want.when || got.Status != want.status || len(got.Builds) != want.jobs {
236 t.Errorf("run %d: %+v, want CreatedAt %q status %q with %d builds",
237 i, got, want.when, want.status, want.jobs)
238 }
239 }
240}