package main
import (
"errors"
"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"
)
type premergeRequired struct{}
func (premergeRequired) RequiredChecks(string, string) []string { return []string{"ci"} }
func (premergeRequired) PremergeMode(string, string) bool { return true }
// greenPairs reports every premerge pair as passed.
type greenPairs struct{}
func (greenPairs) CIRuns(string, int) ([]api.CIRun, error) { return nil, nil }
func (greenPairs) CIRunsPage(string, int, int) ([]api.CIRun, error) { return nil, nil }
func (greenPairs) CIRunsFor(string, string) ([]api.CIRun, error) { return nil, nil }
func (greenPairs) CIRunsForPair(_, pair string) ([]api.CIRun, error) {
return []api.CIRun{{Checkin: pair, Workflow: "ci", Pipeline: "ci", Status: "success"}}, nil
}
func zitIn(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("zit", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("zit %v: %v\n%s", args, err, out)
}
return string(out)
}
// acceptedLanding is a real store where feat landed on trunk through
// an accepted landing, folded into a real index: the merge the
// carry-forward writes a verdict onto.
func acceptedLanding(t *testing.T) (dir string, ix *index.Index) {
t.Helper()
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir = t.TempDir()
zitIn(t, dir, "init", ".")
for _, step := range [][2]string{{"f.txt", "trunk"}, {"g.txt", "feat"}} {
if err := os.WriteFile(filepath.Join(dir, step[0]), []byte(step[1]+"\n"), 0o644); err != nil {
t.Fatal(err)
}
zitIn(t, dir, "add", step[0])
args := []string{"commit", "-m", step[1], "--user", "u"}
if step[1] == "feat" {
args = append(args, "--branch", "feat")
}
zitIn(t, dir, args...)
}
cli := zitcli.New(dir)
tok := lease.Unguarded("test: scratch store, no coordinator")
id, err := cli.LandingOpen(tok, "dan", "feat", "trunk", "land feat", "")
if err != nil {
t.Fatal(err)
}
tip := strings.Fields(zitIn(t, dir, "info", "-R", filepath.Join(dir, ".zit"), "--", "feat"))[0]
if _, err := cli.LandingAccept(tok, id, "dan", "", "", tip); err != nil {
t.Fatal(err)
}
ev, err := events.Start(filepath.Join(t.TempDir(), "nats"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
ix, err = index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
if _, err := ingest.NewPipeline(cli, ev, ix, "test", "n1").Sync(); err != nil {
t.Fatal(err)
}
return dir, ix
}
// The carried verdict is written and only the ingest after it fails.
// The tag exists; the next pass, with the index still behind, must not
// append a second one.
func TestACarriedVerdictWrittenButNotIndexedIsNeverWrittenAgain(t *testing.T) {
dir, ix := acceptedLanding(t)
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
writes := 0
repos := api.NewRepos(map[string]*api.Repo{"test": {
Index: ix, Zit: zitcli.New(dir),
Guard: func(func(lease.Token) error) error { writes++; return nil },
Sync: func() (int, error) { return 0, errors.New("ingest round failed") },
}})
rg := ®istrar{odb: db, ciTagsDefault: true,
deps: &api.Deps{Repos: repos, Checks: premergeRequired{}, CI: greenPairs{}}}
rg.carryForward("test", ix)
if writes != 1 {
t.Fatalf("the first pass wrote %d verdicts, want 1: the fixture carries nothing", writes)
}
rg.carryForward("test", ix)
if writes != 1 {
t.Fatalf("FAIL: %d verdicts written across two passes, want 1", writes)
}
t.Log("ok: two passes, one carried verdict")
}
// A fork and its source share check-in hashes, so the same merge can
// need a verdict in two repositories. Carrying it in one must not make
// the other skip it.
func TestACarriedVerdictInOneRepoDoesNotSkipTheSameMergeInAnother(t *testing.T) {
dir, ix := acceptedLanding(t)
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
writes := map[string]int{}
repo := func(name string) *api.Repo {
return &api.Repo{Index: ix, Zit: zitcli.New(dir),
Guard: func(func(lease.Token) error) error { writes[name]++; return nil },
Sync: func() (int, error) { return 0, errors.New("ingest round failed") }}
}
rg := ®istrar{odb: db, ciTagsDefault: true, deps: &api.Deps{
Repos: api.NewRepos(map[string]*api.Repo{"source": repo("source"), "fork": repo("fork")}),
Checks: premergeRequired{}, CI: greenPairs{}}}
rg.carryForward("source", ix)
rg.carryForward("fork", ix)
if writes["source"] != 1 || writes["fork"] != 1 {
t.Fatalf("FAIL: verdicts written source=%d fork=%d, want one each", writes["source"], writes["fork"])
}
t.Log("ok: the same merge carried once in each repository")
}
// A restarted forge has no memory of what it carried, and the index is
// still behind the store. The verdict is in the store, so nothing is
// written again.
func TestACarriedVerdictTheStoreHoldsIsNotWrittenAgainAfterARestart(t *testing.T) {
dir, ix := acceptedLanding(t)
merge := strings.Fields(zitIn(t, dir, "info", "-R", filepath.Join(dir, ".zit"), "--", "trunk"))[0]
zitIn(t, dir, "tag", "add", "--raw", "ci-pass", merge, "premerge", "-R", filepath.Join(dir, ".zit"))
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
writes := 0
rg := ®istrar{odb: db, ciTagsDefault: true, deps: &api.Deps{
Repos: api.NewRepos(map[string]*api.Repo{"test": {Index: ix, Zit: zitcli.New(dir),
Guard: func(func(lease.Token) error) error { writes++; return nil }}}),
Checks: premergeRequired{}, CI: greenPairs{}}}
rg.carryForward("test", ix)
if writes != 0 {
t.Fatalf("FAIL: %d verdicts written onto a merge the store already holds one on", writes)
}
t.Log("ok: the store's verdict stands; nothing written")
}