repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/refscan/refscan_test.go
Rawpackage 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])
}