package main
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/events"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ingest"
"craftdesign.group/quarry/internal/lease"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/zitcli"
)
// closeRig is a real store with three open tickets, folded into a real
// index, and a registrar whose writes are counted.
type closeRig struct {
t *testing.T
dir string
cli *zitcli.Client
ix *index.Index
db *oltp.DB
rg *registrar
sync func()
writes int
ids []string
}
func newCloseRig(t *testing.T) *closeRig {
t.Helper()
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
r := &closeRig{t: t, dir: t.TempDir()}
zitIn(t, r.dir, "init", ".")
r.commit("f.txt", "first", "")
r.cli = zitcli.New(r.dir)
tok := lease.Unguarded("test: scratch store, no coordinator")
for _, title := range []string{"a", "b", "c"} {
id, _, err := r.cli.TicketNew(tok, "dan", map[string]string{"title": title, "status": "Open"})
if err != nil {
t.Fatal(err)
}
r.ids = append(r.ids, id)
}
ev, err := events.Start(filepath.Join(t.TempDir(), "nats"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
if r.ix, err = index.Open(filepath.Join(t.TempDir(), "index.db")); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { r.ix.Close() })
pipe := ingest.NewPipeline(r.cli, ev, r.ix, "test", "n1")
r.sync = func() {
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
}
r.sync()
if r.db, err = oltp.Open(filepath.Join(t.TempDir(), "o.db")); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { r.db.Close() })
repos := api.NewRepos(map[string]*api.Repo{"test": {Index: r.ix, Zit: r.cli,
Guard: func(fn func(lease.Token) error) error { r.writes++; return fn(tok) },
Sync: pipe.Sync}})
r.rg = ®istrar{odb: r.db, deps: &api.Deps{Repos: repos}}
return r
}
func (r *closeRig) commit(file, comment, branch string) {
r.t.Helper()
if err := os.WriteFile(filepath.Join(r.dir, file), []byte(comment+"\n"), 0o644); err != nil {
r.t.Fatal(err)
}
zitIn(r.t, r.dir, "add", file)
args := []string{"commit", "-m", comment, "--user", "u"}
if branch != "" {
args = append(args, "--branch", branch)
}
zitIn(r.t, r.dir, args...)
}
func (r *closeRig) status(i int) string {
r.t.Helper()
tk, ok, err := r.ix.TicketByPrefixAll(r.ids[i])
if err != nil || !ok {
r.t.Fatalf("ticket %d: %v %v", i, ok, err)
}
for _, f := range tk.Fields {
if f.Name == "status" {
return f.Value
}
}
return ""
}
// A check-in on the default branch that says it closes a ticket closes
// it; the same words pushed to another branch close nothing; the
// history the sweep first saw is never read as new.
func TestADefaultBranchCheckinClosesItsTicket(t *testing.T) {
r := newCloseRig(t)
r.rg.closeSweep("test", r.ix)
if r.writes != 0 {
t.Fatalf("FAIL: the first sweep wrote %d changes; it only baselines", r.writes)
}
r.commit("g.txt", "fix it, closes "+r.ids[0][:10], "")
r.commit("h.txt", "on a side line, closes "+r.ids[1][:10], "side")
r.sync()
r.rg.closeSweep("test", r.ix)
if r.writes != 1 {
t.Fatalf("FAIL: %d ticket changes, want 1", r.writes)
}
if got := r.status(0); got != "Closed" {
t.Errorf("FAIL: the named ticket is %q", got)
}
if got := r.status(1); got != "Open" {
t.Errorf("FAIL: a push to another branch closed a ticket: %q", got)
}
t.Log("ok: default branch closes, other branches do not")
}
// A re-fold replays history the sweep already acted on. A ticket
// somebody reopened after its closing check-in stays reopened.
func TestACheckinClosesItsTicketOnce(t *testing.T) {
r := newCloseRig(t)
first := strings.Fields(zitIn(t, r.dir, "info", "-R", filepath.Join(r.dir, ".zit"), "--", "trunk"))[0]
r.rg.closeSweep("test", r.ix)
r.commit("g.txt", "closes "+r.ids[0][:10], "")
r.sync()
r.rg.closeSweep("test", r.ix)
tok := lease.Unguarded("test")
if _, err := r.cli.TicketSet(tok, r.ids[0], "dan", map[string]string{"status": "Open"}); err != nil {
t.Fatal(err)
}
r.sync()
if err := r.db.SetCloseWatch("test", first); err != nil {
t.Fatal(err)
}
r.writes = 0
r.rg.closeSweep("test", r.ix)
if r.writes != 0 || r.status(0) != "Open" {
t.Fatalf("FAIL: the replayed check-in closed again (%d writes, %s)", r.writes, r.status(0))
}
t.Log("ok: once per check-in")
}
// A landing's merge is the landing's: its accept already closed what
// the landing named, so the merge check-in's own comment is not read.
func TestALandingMergeIsLeftToTheLanding(t *testing.T) {
r := newCloseRig(t)
r.rg.closeSweep("test", r.ix)
r.commit("g.txt", "feature work", "feat")
tok := lease.Unguarded("test")
id, err := r.cli.LandingOpen(tok, "dan", "feat", "trunk", "land feat", "")
if err != nil {
t.Fatal(err)
}
tip := strings.Fields(zitIn(t, r.dir, "info", "-R", filepath.Join(r.dir, ".zit"), "--", "feat"))[0]
if _, err := r.cli.LandingAccept(tok, id, "dan", "land feat, closes "+r.ids[2][:10], "", tip); err != nil {
t.Fatal(err)
}
r.sync()
b, ok, err := r.ix.TipOf("trunk")
if err != nil || !ok {
t.Fatal(ok, err)
}
meta, err := r.ix.CheckinsMeta([]string{b.Tip})
if err != nil || !strings.Contains(meta[b.Tip].Comment, r.ids[2][:10]) {
t.Fatalf("the fixture's merge does not carry the closing words: %q %v", meta[b.Tip].Comment, err)
}
r.rg.closeSweep("test", r.ix)
if r.writes != 0 || r.status(2) != "Open" {
t.Fatalf("FAIL: the landing merge closed a ticket (%d writes, %s)", r.writes, r.status(2))
}
t.Log("ok: a landing merge is the landing's business")
}