123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 | package render_test
import (
"bytes"
"io"
"log"
"os"
"strings"
"testing"
"code.bruce-hill.com/repo-renderer/internal/queue"
"code.bruce-hill.com/repo-renderer/internal/render"
)
var zeroSHA = strings.Repeat("0", 40)
// captureLog sends the site's log to the returned buffer as well as the
// test log.
func (e *env) captureLog() *bytes.Buffer {
var buf bytes.Buffer
e.site.Log = log.New(io.MultiWriter(&buf, testWriter{e.t}), "", 0)
return &buf
}
func (e *env) sha(rev string) string {
e.t.Helper()
return strings.TrimSpace(e.git(e.work, "rev-parse", rev))
}
// kick queues jobs the way the post-receive hook does, then drains the
// queue.
func (e *env) kick(jobs ...queue.Job) {
e.t.Helper()
for _, j := range jobs {
if err := render.Enqueue(e.site.Cfg, j); err != nil {
e.t.Fatalf("Enqueue %v: %v", j, err)
}
}
if err := e.site.Kick(); err != nil {
e.t.Fatalf("Kick: %v", err)
}
if b, err := os.ReadFile(e.site.Lay.QueuePath()); err == nil && len(b) > 0 {
e.t.Errorf("queue left with %q", b)
}
}
// refs lists a bare repository's refs.
func (e *env) refs(dir string) string {
e.t.Helper()
return e.git(dir, "for-each-ref", "--format=%(refname)")
}
// A push renders and exports its own repository and rebuilds the index;
// the other repositories are left alone. A push that changes nothing does
// none of it.
func TestKickRendersPushedRepo(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.git("", "clone", "-q", "--bare", e.site.Lay.GitDir("proj"), e.site.Lay.GitDir("other"))
logs := e.captureLog()
e.kick(queue.Job{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: e.sha("main")})
e.mustExist(e.page(""), "index.html", "git/proj.git/HEAD")
e.mustNotExist("code/other.html", "git/other.git")
// Jobs for one repository make one render, and when it changes
// nothing there is nothing to export or index.
if err := os.Remove(e.h("index.html")); err != nil {
t.Fatal(err)
}
if err := os.RemoveAll(e.h("git/proj.git")); err != nil {
t.Fatal(err)
}
logs.Reset()
same := queue.Job{Repo: "proj", Ref: "refs/heads/main", Old: e.sha("main"), New: e.sha("main")}
e.kick(same, same)
if n := strings.Count(logs.String(), "up to date"); n != 1 {
t.Errorf("proj rendered %d times, want once", n)
}
e.mustNotExist("index.html", "git/proj.git")
}
// A tag push archives the tag before the branch is rendered, since the
// render writes the download lists from the archives there are. A tag
// alone rewrites the download lists and the index.
func TestKickTagsBeforeBranch(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
e.git(e.work, "tag", "v1")
e.git(e.work, "push", "-q", "origin", "v1")
logs := e.captureLog()
e.kick(
queue.Job{Repo: "proj", Ref: "refs/heads/main", Old: e.sha("main"), New: e.sha("main")},
queue.Job{Repo: "proj", Ref: "refs/tags/v1", Old: zeroSHA, New: e.sha("v1")},
)
out := logs.String()
tag, render := strings.Index(out, "archiving tag v1"), strings.Index(out, "up to date")
if tag < 0 || render < 0 || tag > render {
t.Errorf("tag not archived before the render:\n%s", out)
}
e.mustExist("archive/proj/proj-v1.tar.gz")
if err := os.Remove(e.h("index.html")); err != nil {
t.Fatal(err)
}
e.git(e.work, "tag", "v2")
e.git(e.work, "push", "-q", "origin", "v2")
logs.Reset()
e.kick(queue.Job{Repo: "proj", Ref: "refs/tags/v2", Old: zeroSHA, New: e.sha("v2")})
mustNotContain(t, "log", logs.String(), "up to date")
e.mustExist("archive/proj/proj-v2.tar.gz", "index.html", "git/proj.git/HEAD")
mustContain(t, "download list", e.read("iframes/proj/downloads.html"), "proj-v1.tar.gz", "proj-v2.tar.gz")
}
// A push to a branch the site does not show renders nothing and says why;
// one to an allowlisted branch renders.
func TestKickSkipsUnshownBranch(t *testing.T) {
e := newEnv(t, "proj", "repos:\n proj:\n branches: [dev]\n")
e.seed()
for _, b := range []string{"dev", "feature"} {
e.git(e.work, "branch", b)
e.git(e.work, "push", "-q", "origin", b)
}
logs := e.captureLog()
e.kick(queue.Job{Repo: "proj", Ref: "refs/heads/feature", Old: zeroSHA, New: e.sha("feature")})
mustContain(t, "log", logs.String(), "push to feature not shown")
e.mustNotExist(e.page(""), "index.html")
e.kick(queue.Job{Repo: "proj", Ref: "refs/heads/dev", Old: zeroSHA, New: e.sha("dev")})
e.mustExist(e.page(""), "code/proj@dev.html", "index.html")
}
// A malformed line in the queue is logged and skipped; the jobs around it
// still run.
func TestKickIgnoresMalformedJobs(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
logs := e.captureLog()
sha := e.sha("main")
if err := os.MkdirAll(e.site.Lay.QueueDir, 0o755); err != nil {
t.Fatal(err)
}
lines := []queue.Job{
{Repo: "../proj", Ref: "refs/heads/main", Old: zeroSHA, New: sha},
{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: "main"},
{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: sha},
}
var q strings.Builder
for _, j := range lines {
q.WriteString(j.Format())
}
if err := os.WriteFile(e.site.Lay.QueuePath(), []byte(q.String()), 0o644); err != nil {
t.Fatal(err)
}
if err := e.site.Kick(); err != nil {
t.Fatal(err)
}
if n := strings.Count(logs.String(), "queue: ignoring job"); n != 2 {
t.Errorf("%d jobs ignored, want 2:\n%s", n, logs.String())
}
e.mustExist(e.page(""), "index.html")
if _, err := os.Stat(e.site.Lay.QueuePath()); err == nil {
t.Error("queue left behind")
}
}
// Enqueue refuses jobs that could not have come from a push, and queues
// the rest.
func TestEnqueue(t *testing.T) {
e := newEnv(t, "proj", "")
sha := strings.Repeat("ab", 20)
for _, j := range []queue.Job{
{Repo: "../proj", Ref: "refs/heads/main", Old: zeroSHA, New: sha},
{Repo: "", Ref: "refs/heads/main", Old: zeroSHA, New: sha},
{Repo: "proj", Ref: "refs/heads/a\tb", Old: zeroSHA, New: sha},
{Repo: "proj", Ref: "refs/heads/a\nb", Old: zeroSHA, New: sha},
{Repo: "proj", Ref: "refs/heads/main", Old: "HEAD", New: sha},
{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: "main~1"},
{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: sha[:39]},
} {
if err := render.Enqueue(e.site.Cfg, j); err == nil {
t.Errorf("Enqueue accepted %q", j.Format())
}
}
if _, err := os.Stat(e.site.Lay.QueuePath()); err == nil {
t.Errorf("rejected jobs queued: %q", mustReadFile(t, e.site.Lay.QueuePath()))
}
jobs := []queue.Job{
{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: sha},
{Repo: "proj", Ref: "refs/tags/v1", Old: sha, New: zeroSHA},
}
for _, j := range jobs {
if err := render.Enqueue(e.site.Cfg, j); err != nil {
t.Errorf("Enqueue %q: %v", j.Format(), err)
}
}
if got, want := mustReadFile(t, e.site.Lay.QueuePath()), jobs[0].Format()+jobs[1].Format(); got != want {
t.Errorf("queue = %q, want %q", got, want)
}
}
|