package api
import (
"errors"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/events"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ingest"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// noChecks requires nothing anywhere: an ungated target.
type noChecks struct{}
func (noChecks) RequiredChecks(string, string) []string { return nil }
// strataStore is a real store with a two-rung strata, low onto trunk
// and high onto low, and an unrelated landing, other onto trunk; every
// branch touches its own file. Landing ids are keyed by branch.
func strataStore(t *testing.T) (dir string, ids map[string]string, ix *index.Index, pipeSync func() (int, error)) {
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")
ids = map[string]string{}
for _, b := range []struct{ branch, from string }{{"low", "trunk"}, {"high", "low"}, {"other", "trunk"}} {
zrun(t, dir, "update", b.from)
zwrite(t, dir, b.branch+".txt", b.branch+"\n")
zrun(t, dir, "add", b.branch+".txt")
zrun(t, dir, "commit", "-m", b.branch, "--branch", b.branch, "--user", "u")
id, err := zitcli.New(dir).LandingOpen(testToken, "dan", b.branch, b.from, "land "+b.branch, "")
if err != nil {
t.Fatal(err)
}
ids[b.branch] = id
}
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() })
pipe := ingest.NewPipeline(zitcli.New(dir), ev, ix, "test", "n1")
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
return dir, ids, ix, pipe.Sync
}
// ageOf reads a landing's merge verdict the way every caller does.
func ageOf(t *testing.T, d Deps, ix *index.Index, id string) mergeAge {
t.Helper()
l, found, err := ix.LandingByPrefix(id)
if err != nil || !found {
t.Fatalf("no landing %s: %v", id, err)
}
return d.mergeAgeOf("test", l)
}
func isStaleRefusal(err error) bool {
var ref *ops.Refusal
return errors.As(err, &ref) && ref.Code == RefusalStaleVerdict && errors.Is(err, ops.ErrForbidden)
}
func TestAMergeTestedOnTheTargetAsItIsIsFresh(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "one"), root)
d := Deps{Version: "v", Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
if a := ageOf(t, d, ix, ids[0]); a.Stale || a.Tested != root || a.Behind != 0 {
t.Fatalf("FAIL: a merge tested on trunk's tip reads %+v, want fresh against %.12s", a, root)
}
if !t.Failed() {
t.Log("ok: tested on the tip trunk has now is fresh")
}
}
// dmestas's rule: a target that moved under a landing voids its merge
// verdict. Four landings accepted within a minute were each tested
// against the same old trunk; the second one here is that case.
func TestAnUnrelatedLandingMakesTheMergeStaleAndTheAcceptWaitsForARerun(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "one"), root)
ci.green(ztip(t, dir, "two"), root)
d := Deps{Version: "v", Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[0]}, "accept"); err != nil {
t.Fatalf("FAIL: the first accept of a fresh green merge failed: %v", err)
}
a := ageOf(t, d, ix, ids[1])
if !a.Stale || a.Tested != root || a.Behind < 1 {
t.Fatalf("FAIL: after one landed, two reads %+v, want stale against %.12s and behind", a, root)
}
_, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[1]}, "accept")
if !isStaleRefusal(err) {
t.Fatalf("FAIL: an accept over a stale verdict answered %v, want the %s refusal", err, RefusalStaleVerdict)
}
// Overriding is the existing gate's: an owner or an admin, with a
// reason. A plain committer's reason is not enough.
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[1], Override: "hotfix"}, "accept"); !errors.Is(err, ops.ErrForbidden) {
t.Fatalf("FAIL: a committer overrode a stale verdict: %v", err)
}
// The re-run: the merge tested again with trunk as it is now.
now := ztip(t, dir, "trunk")
ci.green(ztip(t, dir, "two"), now)
if a := ageOf(t, d, ix, ids[1]); a.Stale || a.Tested != now {
t.Fatalf("FAIL: after the re-run two reads %+v, want fresh against %.12s", a, now)
}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[1]}, "accept"); err != nil {
t.Fatalf("FAIL: the accept after a re-run was refused: %v", err)
}
if !t.Failed() {
t.Log("ok: stale is refused, the re-run makes it fresh, and it lands")
}
}
func TestAnAdminMayAcceptOverAStaleVerdictSayingWhy(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "one"), root)
ci.green(ztip(t, dir, "two"), root)
d := Deps{Version: "v", Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[0]}, "accept"); err != nil {
t.Fatal(err)
}
if _, err := d.landingSay(as("dan", "ia"), landingSayIn{Repo: "test", ID: ids[1], Override: "release cut"}, "accept"); err != nil {
t.Fatalf("FAIL: an admin's stated override of a stale verdict was refused: %v", err)
}
if !t.Failed() {
t.Log("ok: the override the checks gate has applies to a stale verdict")
}
}
// A target with no required checks has no gate to close: the accept
// lands and its merge message records which trunk was tested.
func TestAStaleVerdictOnAnUngatedTargetLandsAndSaysSo(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "one"), root)
ci.green(ztip(t, dir, "two"), root)
d := Deps{Version: "v", Checks: noChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[0]}, "accept"); err != nil {
t.Fatal(err)
}
if !ageOf(t, d, ix, ids[1]).Stale {
t.Fatal("FAIL: two should read stale once one landed")
}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[1]}, "accept"); err != nil {
t.Fatalf("FAIL: an ungated target refused a stale verdict: %v", err)
}
if out := zrun(t, dir, "artifact", ztip(t, dir, "trunk")); !strings.Contains(out, root[:12]) {
t.Errorf("FAIL: the merge's message does not name the trunk its checks tested:\n%s", out)
}
if !t.Failed() {
t.Log("ok: ungated, the stale accept lands and its record names the tested trunk")
}
}
// A strata's upper rung was tested against the rung below. When that
// rung lands, trunk moved only by the strata's own work, which the
// upper rung's test already held: the verdict stands.
func TestAStrataRungStaysFreshWhenOnlyItsLowerRungLanded(t *testing.T) {
dir, ids, ix, pipeSync := strataStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "low"), root)
ci.green(ztip(t, dir, "high"), ztip(t, dir, "low"))
d := Deps{Version: "v", Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
if a := ageOf(t, d, ix, ids["high"]); a.Stale {
t.Fatalf("FAIL: high reads %+v before anything landed, want fresh", a)
}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids["low"]}, "accept"); err != nil {
t.Fatalf("FAIL: the lower rung's accept failed: %v", err)
}
if a := ageOf(t, d, ix, ids["high"]); a.Stale {
t.Fatalf("FAIL: high reads %+v after only its lower rung landed, want fresh", a)
}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids["high"]}, "accept"); err != nil {
t.Fatalf("FAIL: the upper rung's accept was refused: %v", err)
}
if !t.Failed() {
t.Log("ok: the strata lands rung by rung on the verdicts it already had")
}
}
// The same strata, but other landed on trunk too: the upper rung's
// test never met other's work, so its verdict is stale.
func TestAStrataRungGoesStaleWhenAnythingElseLandedToo(t *testing.T) {
dir, ids, ix, pipeSync := strataStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "low"), root)
ci.green(ztip(t, dir, "other"), root)
ci.green(ztip(t, dir, "high"), ztip(t, dir, "low"))
d := Deps{Version: "v", Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids["low"]}, "accept"); err != nil {
t.Fatal(err)
}
ci.green(ztip(t, dir, "other"), ztip(t, dir, "trunk"))
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids["other"]}, "accept"); err != nil {
t.Fatal(err)
}
if a := ageOf(t, d, ix, ids["high"]); !a.Stale {
t.Fatalf("FAIL: high reads %+v after other also landed, want stale", a)
}
// The strata walk accepts each rung through the same gate.
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(as("dan", "i"), "landing.land", landingLandIn{Repo: "test", ID: ids["high"]})
if err != nil {
t.Fatal(err)
}
if out := got.(landingLandOut); out.Stopped != ids["high"] || len(out.Landed) != 0 {
t.Fatalf("FAIL: the walk over a stale rung answered %+v, want it stopped at high", out)
}
if !t.Failed() {
t.Log("ok: anything beside the strata's own rungs voids the upper rung's verdict")
}
}
// landing.get and landing.list carry the state, so the page and an
// API client read the one answer the gate acts on.
func TestTheLandingReadsCarryTheStaleState(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "one"), root)
ci.green(ztip(t, dir, "two"), root)
d := Deps{Version: "v", Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[0]}, "accept"); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(as("dan", "i8"), "landing.get", landingGetIn{Repo: "test", ID: ids[1]})
if err != nil {
t.Fatal(err)
}
if g := got.(landingGetOut); !g.Stale || g.TestedAgainst != root {
t.Errorf("FAIL: landing.get reads stale=%v tested=%.12s, want stale against the root", g.Stale, g.TestedAgainst)
}
list, err := r.Invoke(as("dan", "i8"), "landing.list", landingListIn{Repo: "test", State: "open"})
if err != nil {
t.Fatal(err)
}
for _, l := range list.(landingListOut).Landings {
if l.ID == ids[1] && !l.Stale {
t.Errorf("FAIL: landing.list does not mark two stale")
}
}
if !t.Failed() {
t.Log("ok: both reads say stale")
}
}
// An armed landing whose verdict went stale tests its merge again by
// itself, once, and lands when that is green: nobody is at the page to
// press Re-run, and waiting on a refusal that only a person can lift
// left it armed forever. One re-run per target tip, not one per sweep.
func TestAnArmedLandingReRunsItsStaleMergeOnce(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
root := ztip(t, dir, "trunk")
ci := &mergeRuns{pairRuns: &pairRuns{runs: map[string][]CIRun{}}}
ci.green(ztip(t, dir, "one"), root)
ci.green(ztip(t, dir, "two"), root)
var reruns []string
d := Deps{Version: "v", ZitVersion: zitcli.MinVersion, Checks: mergeChecks{}, CI: ci, Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
d.CIRunPair = func(repo, branch, tip, target string) error {
reruns = append(reruns, tip+":"+target)
ci.green(tip, target) // the re-run, finished at once
return nil
}
if _, err := d.landingSay(as("dan", "i"), landingSayIn{Repo: "test", ID: ids[0]}, "accept"); err != nil {
t.Fatal(err)
}
now := ztip(t, dir, "trunk")
store := &sweepStub{rows: []oltp.AutoAccept{{Repo: "test", Landing: ids[1], Account: "dan"}}}
if landed := d.AutoAcceptSweep(store); len(landed) != 0 {
t.Fatalf("FAIL: the first sweep landed over a stale verdict: %v", landed)
}
if len(reruns) != 1 || reruns[0] != ztip(t, dir, "two")+":"+now {
t.Fatalf("FAIL: the first sweep re-ran %v, want one merge against %.12s", reruns, now)
}
if landed := d.AutoAcceptSweep(store); len(landed) != 1 {
t.Fatalf("FAIL: after the re-run went green the sweep landed %v, want it", landed)
}
if len(reruns) != 1 {
t.Fatalf("FAIL: the merge was re-run %d times, want once", len(reruns))
}
}