internal/control/buildlog_test.go

main
gitbay/internal/control/buildlog_test.go history · blame · raw

131 lines · 4483 bytes

6 symbols in this file
  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}