internal/control/buildlog_test.go
131 lines · 4483 bytes
1package control
2
3import (
4 "bytes"
5 "fmt"
6 "reflect"
7 "regexp"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15func TestSplitBuildLog(t *testing.T) {
16 log := "$ git clone ssh://x/a.git (abc)\n" +
17 "$ go build ./...\n" +
18 "built\n" +
19 "$ go test ./...\n" +
20 "--- FAIL: TestX\n" +
21 "step 2/2 failed: exit 1\n"
22 got := SplitBuildLog(log, []string{"go build ./...", "go test ./..."})
23 want := []LogSection{
24 {N: 0, Text: "$ git clone ssh://x/a.git (abc)\n"},
25 {N: 1, Step: "go build ./...", Text: "built\n"},
26 {N: 2, Step: "go test ./...", Text: "--- FAIL: TestX\nstep 2/2 failed: exit 1\n"},
27 }
28 if !reflect.DeepEqual(got, want) {
29 t.Fatalf("got %+v\nwant %+v", got, want)
30 }
31}
32
33// A step's line inside other output, not at a line start, does not cut;
34// a build that stopped before a step has no section for it; an empty
35// setup is left out.
36func TestSplitBuildLogStopsAtMissingStep(t *testing.T) {
37 got := SplitBuildLog("$ make\nrunning: $ make test\nerror\n", []string{"make", "make test"})
38 want := []LogSection{{N: 1, Step: "make", Text: "running: $ make test\nerror\n"}}
39 if !reflect.DeepEqual(got, want) {
40 t.Fatalf("got %+v\nwant %+v", got, want)
41 }
42}
43
44func TestTailLines(t *testing.T) {
45 for _, tc := range []struct {
46 in string
47 n int
48 want string
49 }{
50 {"a\nb\nc\n", 2, "b\nc\n"},
51 {"a\nb\nc\n", 5, "a\nb\nc\n"},
52 {"a\nb", 1, "b"},
53 } {
54 if got := string(tailLines([]byte(tc.in), tc.n)); got != tc.want {
55 t.Errorf("tailLines(%q, %d) = %q, want %q", tc.in, tc.n, got, tc.want)
56 }
57 }
58}
59
60// failedBuild is a finished failure whose second of two steps failed,
61// having run 10m56s.
62func failedBuild(t *testing.T) (*store.Store, store.Repo, int64, int64) {
63 t.Helper()
64 st, repo, uid := newQueueTestRepo(t)
65 n, err := st.CreateBuild(repo.ID, "unit", "abc", "main", `["go build ./...","go test ./..."]`, "", "", true)
66 if err != nil {
67 t.Fatal(err)
68 }
69 b, ok, err := st.ClaimBuild([]int64{repo.ID}, false)
70 if err != nil || !ok {
71 t.Fatalf("claim: ok=%v err=%v", ok, err)
72 }
73 st.AppendBuildLog(b.ID, []byte("$ git clone x (abc)\n$ go build ./...\nok\n$ go test ./...\none\n--- FAIL: TestX\nstep 2/2 failed: exit 1\n"))
74 if err := st.SetBuildFailure(b.ID, 2, "exit 1"); err != nil {
75 t.Fatal(err)
76 }
77 if err := st.FinishBuild(b.ID, "failure"); err != nil {
78 t.Fatal(err)
79 }
80 if _, err := st.DB.Exec(`UPDATE builds SET started_at = '2026-09-27T10:00:00Z', finished_at = '2026-09-27T10:10:56Z' WHERE id = ?`, b.ID); err != nil {
81 t.Fatal(err)
82 }
83 return st, repo, uid, n
84}
85
86func TestBuildLogStepAndTail(t *testing.T) {
87 st, repo, uid, n := failedBuild(t)
88 run := func(args ...string) (string, int) {
89 t.Helper()
90 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid})
91 code := Dispatch(c, append([]string{"build", "log", repo.Path(), fmt.Sprint(n)}, args...))
92 return c.Stdout.(*bytes.Buffer).String() + errOut.String(), code
93 }
94 if out, _ := run("--step", "1"); out != "ok\n" {
95 t.Errorf("--step 1: %q", out)
96 }
97 if out, _ := run("--step", "failed", "--tail", "2"); out != "--- FAIL: TestX\nstep 2/2 failed: exit 1\n" {
98 t.Errorf("--step failed --tail 2: %q", out)
99 }
100 if out, _ := run("--tail", "1"); out != "step 2/2 failed: exit 1\n" {
101 t.Errorf("--tail 1: %q", out)
102 }
103 if _, code := run("--step", "3"); code != protocol.ExitUsage {
104 t.Errorf("--step past the job: exit %d", code)
105 }
106 if _, code := run("--follow", "--tail", "1"); code != protocol.ExitUsage {
107 t.Errorf("--follow with --tail: exit %d", code)
108 }
109}
110
111func TestBuildShowNamesFailedStepAndDuration(t *testing.T) {
112 st, repo, uid, n := failedBuild(t)
113 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid})
114 if code := Dispatch(c, []string{"build", "show", repo.Path(), fmt.Sprint(n)}); code != protocol.ExitOK {
115 t.Fatalf("exit %d: %s", code, errOut)
116 }
117 out := c.Stdout.(*bytes.Buffer).String()
118 for _, re := range []string{`failed step\s+2/2 go test \./\.\.\. \(exit 1\)`, `duration\s+10m56s`} {
119 if !regexp.MustCompile(re).MatchString(out) {
120 t.Errorf("build show missing %s:\n%s", re, out)
121 }
122 }
123 c, _ = pruneCtx(st, t.TempDir(), store.User{ID: uid})
124 c.JSON = true
125 Dispatch(c, []string{"build", "show", repo.Path(), fmt.Sprint(n)})
126 for _, want := range []string{`"failed_step":2`, `"failed_reason":"exit 1"`, `"duration_s":656`, `"steps":["go build ./...","go test ./..."]`} {
127 if !strings.Contains(c.Stdout.(*bytes.Buffer).String(), want) {
128 t.Errorf("build show --json missing %s", want)
129 }
130 }
131}