Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/refscan/refscan_test.go

Raw
package refscan

import (
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"bugabinga.net/luci/internal/inbox"
	"bugabinga.net/luci/internal/trigger"
)

func TestPollWritesBaselineWithoutEventsThenEmitsFutureRepoPush(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}

	ids, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")})
	if err != nil {
		t.Fatalf("Poll() baseline error = %v", err)
	}
	if len(ids) != 0 {
		t.Fatalf("baseline emitted %d events, want 0", len(ids))
	}

	bare := createBareRepo(t, root, "newrepo")
	rev := gitOut(t, root, "--git-dir", bare, "rev-parse", "refs/heads/main")
	ids, err = Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")})
	if err != nil {
		t.Fatalf("Poll() new repo error = %v", err)
	}
	if len(ids) != 1 {
		t.Fatalf("new repo emitted %d events, want 1", len(ids))
	}
	files, err := filepath.Glob(filepath.Join(data, "inbox", "*.json"))
	if err != nil {
		t.Fatal(err)
	}
	if len(files) != 1 {
		t.Fatalf("inbox files = %d, want 1", len(files))
	}
	dataBytes, err := os.ReadFile(files[0])
	if err != nil {
		t.Fatal(err)
	}
	want := `{"kind":"push","repo":"newrepo","old":"0000000000000000000000000000000000000000","new":"` + rev + `","ref":"refs/heads/main"}` + "\n"
	if string(dataBytes) != want {
		t.Fatalf("event = %s, want %s", dataBytes, want)
	}
}

func TestPollEmitsUpdatedRefOnce(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	bare := createBareRepo(t, root, "repo")
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")}); err != nil {
		t.Fatalf("baseline poll error = %v", err)
	}
	old := gitOut(t, root, "--git-dir", bare, "rev-parse", "refs/heads/main")
	newRev := addCommitAndPush(t, root, "repo")

	ids, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")})
	if err != nil {
		t.Fatalf("update poll error = %v", err)
	}
	if len(ids) != 1 {
		t.Fatalf("update emitted %d events, want 1", len(ids))
	}
	ids, err = Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")})
	if err != nil {
		t.Fatalf("repeat poll error = %v", err)
	}
	if len(ids) != 0 {
		t.Fatalf("repeat emitted %d events, want 0", len(ids))
	}

	files, err := filepath.Glob(filepath.Join(data, "inbox", "*.json"))
	if err != nil {
		t.Fatal(err)
	}
	if len(files) != 1 {
		t.Fatalf("inbox files = %d, want 1", len(files))
	}
	dataBytes, err := os.ReadFile(files[0])
	if err != nil {
		t.Fatal(err)
	}
	want := `{"kind":"push","repo":"repo","old":"` + old + `","new":"` + newRev + `","ref":"refs/heads/main"}` + "\n"
	if string(dataBytes) != want {
		t.Fatalf("event = %s, want %s", dataBytes, want)
	}
}

func TestPollEmitsTagRef(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	createBareRepo(t, root, "repo")
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")}); err != nil {
		t.Fatalf("baseline poll error = %v", err)
	}
	rev := tagAndPush(t, root, "repo", "v1.0.0")

	ids, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")})
	if err != nil {
		t.Fatalf("tag poll error = %v", err)
	}
	if len(ids) != 1 {
		t.Fatalf("tag emitted %d events, want 1", len(ids))
	}
	files, err := filepath.Glob(filepath.Join(data, "inbox", "*.json"))
	if err != nil {
		t.Fatal(err)
	}
	if len(files) != 1 {
		t.Fatalf("inbox files = %d, want 1", len(files))
	}
	dataBytes, err := os.ReadFile(files[0])
	if err != nil {
		t.Fatal(err)
	}
	want := `{"kind":"push","repo":"repo","old":"0000000000000000000000000000000000000000","new":"` + rev + `","ref":"refs/tags/v1.0.0"}` + "\n"
	if string(dataBytes) != want {
		t.Fatalf("event = %s, want %s", dataBytes, want)
	}
}

func TestPollReplaysDurableOutboxWithStableEventID(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	bare := createBareRepo(t, root, "repo")
	cfg := PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")}
	if _, err := Poll(cfg); err != nil {
		t.Fatal(err)
	}
	addCommitAndPush(t, root, "repo")
	oldSave := saveRefs
	saveRefs = func(string, []Ref) error { return os.ErrPermission }
	defer func() { saveRefs = oldSave }()
	ids, err := Poll(cfg)
	if !errors.Is(err, os.ErrPermission) || len(ids) != 1 {
		t.Fatalf("ids=%v err=%v", ids, err)
	}
	if _, err := os.Stat(filepath.Join(data, "state", "refs.outbox.json")); err != nil {
		t.Fatalf("durable outbox missing: %v", err)
	}
	envelope, err := inbox.ClaimOne(cfg.InboxDir)
	if err != nil || envelope == nil || envelope.ID != ids[0] {
		t.Fatalf("envelope=%#v err=%v", envelope, err)
	}
	if err := inbox.Ack(*envelope); err != nil {
		t.Fatal(err)
	}
	saveRefs = oldSave
	ids, err = Poll(cfg)
	if err != nil || len(ids) != 1 || ids[0] != envelope.ID {
		t.Fatalf("replayed ids=%v err=%v", ids, err)
	}
	if got := gitOut(t, root, "--git-dir", bare, "rev-parse", "refs/heads/main"); got == "" {
		t.Fatal("repository unexpectedly lost main")
	}
}

func TestPollKeepsRepeatedRefTransitionsDistinct(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	bare := createBareRepo(t, root, "repo")
	cfg := PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")}
	if _, err := Poll(cfg); err != nil {
		t.Fatal(err)
	}
	old := gitOut(t, root, "--git-dir", bare, "rev-parse", "refs/heads/main")
	new := addCommitAndPush(t, root, "repo")
	var ids []string
	for _, target := range []string{new, old, new} {
		if target != new || len(ids) != 0 {
			runGit(t, root, "--git-dir", bare, "update-ref", "refs/heads/main", target)
		}
		got, err := Poll(cfg)
		if err != nil || len(got) != 1 {
			t.Fatalf("target=%s ids=%v err=%v", target, got, err)
		}
		ids = append(ids, got[0])
	}
	seen := map[string]bool{}
	for _, id := range ids {
		if seen[id] {
			t.Fatalf("reused occurrence ID %q", id)
		}
		seen[id] = true
	}
	var transitions []trigger.PushEvent
	for _, id := range ids {
		data, err := os.ReadFile(filepath.Join(cfg.InboxDir, id+".json"))
		if err != nil {
			t.Fatal(err)
		}
		event, err := trigger.Decode(data)
		if err != nil {
			t.Fatal(err)
		}
		transitions = append(transitions, event.(trigger.PushEvent))
	}
	if transitions[0].Old != old || transitions[0].New != new || transitions[1].Old != new || transitions[1].New != old || transitions[2].Old != old || transitions[2].New != new {
		t.Fatalf("transitions=%#v", transitions)
	}
}

func TestScanSortsReposAndSkipsUnsafeEntries(t *testing.T) {
	root := t.TempDir()
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(root, "ignored"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(repos, "file.git"), []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(repos, ".git"), 0o755); err != nil {
		t.Fatal(err)
	}
	createBareRepo(t, root, "zeta")
	createBareRepo(t, root, "alpha")
	if err := os.MkdirAll(filepath.Join(repos, "notgit.git"), 0o755); err != nil {
		t.Fatal(err)
	}
	refs, err := Scan([]string{filepath.Join(root, "missing"), repos})
	if err != nil {
		t.Fatalf("Scan() error = %v", err)
	}
	if len(refs) != 2 || refs[0].Repo != "alpha" || refs[1].Repo != "zeta" {
		t.Fatalf("refs = %#v", refs)
	}
}

func TestScanRejectsBadRootsAndRepos(t *testing.T) {
	root := t.TempDir()
	file := filepath.Join(root, "file")
	if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Scan([]string{file}); err == nil {
		t.Fatal("file root accepted")
	}
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(filepath.Join(repos, "bad.git"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(repos, "bad.git", "HEAD"), []byte("ref: refs/heads/main\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Scan([]string{repos}); err == nil {
		t.Fatal("bad git repo accepted")
	}
}

func TestPollContinuesHealthyReposWhenOneRepoIsBroken(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(filepath.Join(data, "state"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(data, "state", "refs.json"), []byte("[]"), 0o644); err != nil {
		t.Fatal(err)
	}
	createBareRepo(t, root, "good")
	if err := os.MkdirAll(filepath.Join(repos, "bad.git"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(repos, "bad.git", "HEAD"), []byte("ref: refs/heads/main\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	ids, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")})
	if err == nil {
		t.Fatal("broken repo error hidden")
	}
	if len(ids) != 1 {
		t.Fatalf("healthy repo emitted %d events, want 1: %v", len(ids), err)
	}
}

func TestPollPreservesPreviousRefsForBrokenRepo(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	repos := filepath.Join(root, "repos")
	createBareRepo(t, root, "good")
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")}); err != nil {
		t.Fatal(err)
	}
	refs, _, err := load(filepath.Join(data, "state", "refs.json"))
	if err != nil {
		t.Fatal(err)
	}
	refs = append(refs, Ref{Repo: "bad", Name: "refs/heads/main", Rev: "deadbeef"})
	if err := save(filepath.Join(data, "state", "refs.json"), refs); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(repos, "bad.git"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(repos, "bad.git", "HEAD"), []byte("ref: refs/heads/main\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	addCommitAndPush(t, root, "good")
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(data, "inbox")}); err == nil {
		t.Fatal("broken repo error hidden")
	}
	current, _, err := load(filepath.Join(data, "state", "refs.json"))
	if err != nil {
		t.Fatal(err)
	}
	for _, ref := range current {
		if ref.Repo == "bad" && ref.Rev == "deadbeef" {
			return
		}
	}
	t.Fatalf("broken repo state lost: %#v", current)
}

func TestScanRepoSkipsBlankOutput(t *testing.T) {
	bin := t.TempDir()
	git := filepath.Join(bin, "git")
	if err := os.WriteFile(git, []byte("#!/bin/sh\nprintf '\\n'\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
	refs, err := scanRepo("/repo.git", "repo")
	if err != nil {
		t.Fatal(err)
	}
	if len(refs) != 0 {
		t.Fatalf("refs = %#v", refs)
	}
}

func TestScanRepoRejectsBadRefLines(t *testing.T) {
	bin := t.TempDir()
	git := filepath.Join(bin, "git")
	if err := os.WriteFile(git, []byte("#!/bin/sh\nprintf 'badline\\n'\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
	if _, err := scanRepo("/repo.git", "repo"); err == nil {
		t.Fatal("bad ref line accepted")
	}
}

func TestLoadSaveAndPollErrorPaths(t *testing.T) {
	root := t.TempDir()
	file := filepath.Join(root, "file")
	if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, _, err := load(filepath.Join(file, "refs.json")); err == nil {
		t.Fatal("load under file accepted")
	}
	if err := save(filepath.Join(file, "refs.json"), nil); err == nil {
		t.Fatal("save under file accepted")
	}
	path := filepath.Join(root, "state", "refs.json")
	if err := os.MkdirAll(path, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := save(path, nil); err == nil {
		t.Fatal("save over directory accepted")
	}
	writeErrPath := filepath.Join(root, "write-err", "refs.json")
	if err := os.MkdirAll(writeErrPath+".tmp", 0o755); err != nil {
		t.Fatal(err)
	}
	if err := save(writeErrPath, nil); err == nil {
		t.Fatal("save wrote over tmp directory")
	}
	if _, err := Poll(PollConfig{DataDir: file, RepoRoots: []string{filepath.Join(root, "missing")}, InboxDir: filepath.Join(root, "inbox")}); err == nil {
		t.Fatal("poll save under file accepted")
	}
}

func TestPollPropagatesScanAndInboxErrors(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	if err := os.MkdirAll(filepath.Join(data, "state"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(data, "state", "refs.json"), []byte(`[]`), 0o644); err != nil {
		t.Fatal(err)
	}
	file := filepath.Join(root, "file")
	if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{file}, InboxDir: filepath.Join(data, "inbox")}); err == nil {
		t.Fatal("poll scan error accepted")
	}
	repos := filepath.Join(root, "repos")
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	createBareRepo(t, root, "repo")
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{repos}, InboxDir: filepath.Join(file, "inbox")}); err == nil {
		t.Fatal("poll inbox error accepted")
	}
}

func TestSortRefsOrdersNamesWithinRepo(t *testing.T) {
	refs := []Ref{{Repo: "repo", Name: "refs/heads/z"}, {Repo: "repo", Name: "refs/heads/a"}}
	sortRefs(refs)
	if refs[0].Name != "refs/heads/a" {
		t.Fatalf("refs = %#v", refs)
	}
}

func TestPollRejectsCorruptState(t *testing.T) {
	root := t.TempDir()
	data := filepath.Join(root, "data")
	if err := os.MkdirAll(filepath.Join(data, "state"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(data, "state", "refs.json"), []byte(`{`), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Poll(PollConfig{DataDir: data, RepoRoots: []string{filepath.Join(root, "repos")}, InboxDir: filepath.Join(data, "inbox")}); err == nil {
		t.Fatal("corrupt state accepted")
	}
}

func createBareRepo(t *testing.T, root string, name string) string {
	t.Helper()
	work := filepath.Join(root, name+"-work")
	runGit(t, root, "init", work)
	runGit(t, work, "config", "user.email", "test@example.invalid")
	runGit(t, work, "config", "user.name", "Test")
	if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("hello\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runGit(t, work, "add", "README.md")
	runGit(t, work, "commit", "-m", "initial")
	runGit(t, work, "branch", "-M", "main")
	bare := filepath.Join(root, "repos", name+".git")
	runGit(t, root, "clone", "--bare", work, bare)
	runGit(t, work, "remote", "add", "origin", bare)
	return bare
}

func addCommitAndPush(t *testing.T, root string, name string) string {
	t.Helper()
	work := filepath.Join(root, name+"-work")
	if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("hello again\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runGit(t, work, "add", "README.md")
	runGit(t, work, "commit", "-m", "update")
	runGit(t, work, "push", "origin", "main")
	return gitOut(t, work, "rev-parse", "HEAD")
}

func tagAndPush(t *testing.T, root string, name string, tag string) string {
	t.Helper()
	work := filepath.Join(root, name+"-work")
	runGit(t, work, "tag", "-a", tag, "-m", tag)
	runGit(t, work, "push", "origin", tag)
	return gitOut(t, work, "rev-parse", tag+"^{commit}")
}

func runGit(t *testing.T, dir string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("git %v failed: %v: %s", args, err, out)
	}
}

func gitOut(t *testing.T, dir string, args ...string) string {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git %v failed: %v: %s", args, err, out)
	}
	return string(out[:len(out)-1])
}