package api
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/zitcli"
)
func zrun(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)
}
func zwrite(t *testing.T, dir, name, body string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func ztip(t *testing.T, dir, branch string) string {
t.Helper()
out := zrun(t, dir, "branch", "info", branch)
for _, line := range strings.Split(out, "\n") {
for _, f := range strings.Fields(line) {
if len(f) == 64 && strings.Trim(f, "0123456789abcdef") == "" {
return f
}
}
}
t.Fatalf("no tip hash in `zit branch info %s`:\n%s", branch, out)
return ""
}
// landedStore builds a store with two branches: one whose work was
// merged into trunk and LEFT OPEN, and one with work genuinely
// outstanding. The first is the case four cheap questions cannot tell
// from the second.
func landedStore(t *testing.T) (dir, trunkTip, landedTip, strandedTip string) {
t.Helper()
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir = t.TempDir()
zrun(t, dir, "init", ".")
zwrite(t, dir, "f.txt", "root\n")
zrun(t, dir, "add", "f.txt")
zrun(t, dir, "commit", "-m", "root", "--user", "u")
// The branch that lands and stays open.
zwrite(t, dir, "f.txt", "root\nlanded\n")
zrun(t, dir, "commit", "-m", "landed work", "--branch", "went-in", "--user", "u")
landedTip = ztip(t, dir, "went-in")
zrun(t, dir, "update", "trunk")
zrun(t, dir, "merge", "went-in")
zrun(t, dir, "commit", "-m", "merge went-in", "--user", "u")
trunkTip = ztip(t, dir, "trunk")
// The branch with work nobody has taken.
zwrite(t, dir, "g.txt", "outstanding\n")
zrun(t, dir, "add", "g.txt")
zrun(t, dir, "commit", "-m", "real work", "--branch", "stranded", "--user", "u")
strandedTip = ztip(t, dir, "stranded")
return dir, trunkTip, landedTip, strandedTip
}
// The four cheap questions are not the question. Landing a branch
// normally closes it, so the open test covers the usual case — but a
// branch landed and left OPEN still got the offer, and accepting it
// opened a record that said "merges up-to-date" on arrival.
func TestABranchWhoseWorkLandedIsNotOfferedAgain(t *testing.T) {
dir, trunkTip, landedTip, strandedTip := landedStore(t)
repo := &Repo{Zit: zitcli.New(dir)}
if !alreadyLanded(repo, landedTip, trunkTip) {
t.Error("FAIL: a branch merged into trunk still reads as having work to propose")
}
if alreadyLanded(repo, strandedTip, trunkTip) {
t.Error("FAIL: real stranded work reads as already landed — the offer would be withheld")
}
}
// The unproposed list drops the landed rows and keeps the stranded one.
func TestTheUnproposedListDropsWorkThatLanded(t *testing.T) {
dir, trunkTip, landedTip, strandedTip := landedStore(t)
repo := &Repo{Zit: zitcli.New(dir)}
got := dropLandedRows(repo, []unproposedRow{
{Branch: "went-in", Tip: landedTip},
{Branch: "stranded", Tip: strandedTip},
}, trunkTip)
if len(got) != 1 || got[0].Branch != "stranded" {
t.Errorf("FAIL: the list kept %+v, want only stranded", got)
}
}
// A probe that cannot run answers NOT LANDED. This check exists to
// withhold an offer, and withholding one because a subprocess failed
// would hide real stranded work behind an infrastructure fault.
func TestAProbeThatCannotRunKeepsTheOldBehaviour(t *testing.T) {
for _, tc := range []struct {
name string
repo *Repo
}{
{"no repository", nil},
{"no store handle", &Repo{}},
} {
if alreadyLanded(tc.repo, "aaaa", "bbbb") {
t.Errorf("FAIL: %s answered landed; the safe default is not landed", tc.name)
}
}
// And an unanswerable target leaves every row in place rather than
// dropping rows nobody checked.
rows := []unproposedRow{{Branch: "a", Tip: "t1"}, {Branch: "b", Tip: "t2"}}
if got := dropLandedRows(&Repo{}, rows, ""); len(got) != 2 {
t.Errorf("FAIL: an unknown target dropped rows: %+v", got)
}
}
// Past the cap the rows are reported UNPROBED — the behaviour that
// existed before this check. Degrading to the old answer beats
// degrading to a slow page, and beats silently dropping rows.
func TestPastTheCapRowsSurviveUnprobed(t *testing.T) {
dir, trunkTip, landedTip, _ := landedStore(t)
repo := &Repo{Zit: zitcli.New(dir)}
var rows []unproposedRow
for i := 0; i < landedProbeCap+3; i++ {
rows = append(rows, unproposedRow{Branch: "went-in", Tip: landedTip})
}
got := dropLandedRows(repo, rows, trunkTip)
// Every row is the same landed branch, so the probed ones all go and
// exactly the overflow survives.
if len(got) != 3 {
t.Errorf("FAIL: %d rows survived, want the 3 past the cap of %d", len(got), landedProbeCap)
}
}