Skip to content
quarry Artifact [cada05b68f]
Login

Artifact [cada05b68f]

Artifact cada05b68f77b2f2734434ab66b8b4a7b2e2aebbf8442a3b757fc06a7977028c:


package api

import (
	"bytes"
	"errors"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/zitcli"
)

// syncedMove is a required-checks policy that, the first time it is
// asked, pushes to feat AND folds that push into the index, so the
// index tip has moved between the check and the merge.
type syncedMove struct {
	once     sync.Once
	move     func()
	required []string
}

func (m *syncedMove) RequiredChecks(string, string) []string {
	m.once.Do(m.move)
	return m.required
}

// runsAt answers the runs recorded for exactly one check-in each, and
// for a pair, the runs of the branch tip it merged: a landing's checks
// are its merge (quarry#290), green here when its tip's are.
type runsAt map[string][]CIRun

func (r runsAt) CIRuns(string, int) ([]CIRun, error)          { return nil, nil }
func (r runsAt) CIRunsPage(string, int, int) ([]CIRun, error) { return nil, nil }
func (r runsAt) CIRunsFor(_ string, c string) ([]CIRun, error) {
	return r[c], nil
}
func (r runsAt) CIRunsForPair(_ string, pair string) ([]CIRun, error) {
	head, _, _ := parsePair(pair)
	return r[head], nil
}

// acceptAfterAMove accepts the landing with checks that move feat, and
// the index, once they are consulted. It answers the tip checked, the
// tip moved to, the accept's error and the --expect-tip zit was given.
func acceptAfterAMove(t *testing.T, required []string, runs func(checked, moved string) runsAt) (checked, moved, expectTip string, err error) {
	t.Helper()
	dir, id, ix, pipeSync := acceptStore(t)
	real, lerr := exec.LookPath("zit")
	if lerr != nil {
		t.Skip("zit not on PATH")
	}
	argv := filepath.Join(t.TempDir(), "argv")
	bin := t.TempDir()
	script := "#!/bin/sh\nif [ \"$1\" = landing ] && [ \"$2\" = accept ]; then printf '%s\\n' \"$@\" > '" +
		argv + "'; fi\nexec '" + real + "' \"$@\"\n"
	if werr := os.WriteFile(filepath.Join(bin, "zit"), []byte(script), 0o755); werr != nil {
		t.Fatal(werr)
	}
	t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
	checked = ztip(t, dir, "feat")
	checks := &syncedMove{required: required, move: func() {
		zwrite(t, dir, "h.txt", "unchecked\n")
		zrun(t, dir, "add", "h.txt")
		zrun(t, dir, "commit", "-m", "late push", "--user", "u")
		moved = ztip(t, dir, "feat")
		if _, serr := pipeSync(); serr != nil {
			t.Fatal(serr)
		}
	}}
	deps := Deps{Version: "v", Checks: checks, Repos: NewRepos(map[string]*Repo{
		"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
	if runs != nil {
		// The runs depend on the tips, and moved is known only once the
		// checks are consulted: resolve lazily through the map.
		ra := runsAt{}
		deps.CI = ra
		orig := checks.move
		checks.move = func() {
			orig()
			for k, v := range runs(checked, moved) {
				ra[k] = v
			}
		}
	}
	r := ops.NewRegistry()
	if rerr := RegisterAll(r, deps); rerr != nil {
		t.Fatal(rerr)
	}
	_, err = r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: id})
	if moved == "" || moved == checked {
		t.Fatalf("the test never moved the branch: checked %s, now %s", checked, moved)
	}
	if b, rerr := os.ReadFile(argv); rerr == nil {
		args := strings.Split(strings.TrimSpace(string(b)), "\n")
		for i := 0; i+1 < len(args); i++ {
			if args[i] == "--expect-tip" {
				expectTip = args[i+1]
			}
		}
	}
	return checked, moved, expectTip, err
}

func assertBranchMoved(t *testing.T, err error, checked, moved string) {
	t.Helper()
	var ref *ops.Refusal
	if !errors.As(err, &ref) || ref.Code != RefusalBranchMoved {
		t.Fatalf("FAIL: accept = %v; want a %s refusal", err, RefusalBranchMoved)
	}
	if ref.Fields["expected"] != checked || ref.Fields["current"] != moved {
		t.Fatalf("FAIL: refusal fields %v; want expected %s, current %s", ref.Fields, checked, moved)
	}
}

// The index can catch up with a push between the checks and the merge.
// zit is still handed the tip the checks judged, never a fresh read.
func TestAcceptHandsZitTheCheckedTipEvenWhenTheIndexMoved(t *testing.T) {
	checked, moved, expectTip, err := acceptAfterAMove(t, nil, nil)
	if expectTip != checked {
		t.Fatalf("FAIL: zit was given --expect-tip %q; want the checked tip %s (moved is %s)", expectTip, checked, moved)
	}
	assertBranchMoved(t, err, checked, moved)
	t.Logf("ok: %v", err)
}

// Green at the checked tip, then moved: the checks pass because they
// judged the checked tip, and zit refuses the moved branch.
func TestChecksGreenAtTheCheckedTipStillRefuseAMovedBranch(t *testing.T) {
	checked, moved, _, err := acceptAfterAMove(t, []string{"ci"}, func(checked, _ string) runsAt {
		return runsAt{checked: {{Checkin: checked, Workflow: "ci", Pipeline: "ci", Status: "success"}}}
	})
	assertBranchMoved(t, err, checked, moved)
	t.Logf("ok: %v", err)
}

// Green only at the new tip: the tip being accepted was never checked,
// so the accept is refused as unmet before zit is asked.
func TestChecksGreenOnlyAtTheNewTipAreUnmetAtTheCheckedOne(t *testing.T) {
	_, _, expectTip, err := acceptAfterAMove(t, []string{"ci"}, func(_, moved string) runsAt {
		return runsAt{moved: {{Checkin: moved, Workflow: "ci", Pipeline: "ci", Status: "success"}}}
	})
	if !errors.Is(err, ops.ErrForbidden) || !strings.Contains(err.Error(), "required checks not green") {
		t.Fatalf("FAIL: accept = %v; want refused as unmet required checks", err)
	}
	if expectTip != "" {
		t.Fatalf("FAIL: zit was asked to accept (--expect-tip %s) over unmet checks", expectTip)
	}
	t.Logf("ok: %v", err)
}

type sweepStub struct {
	rows    []oltp.AutoAccept
	waiting []string
}

func (s *sweepStub) AutoAccepts() ([]oltp.AutoAccept, error) { return s.rows, nil }
func (s *sweepStub) ClearAutoAccept(string, string) error    { return nil }
func (s *sweepStub) SetAutoWaiting(_, _, reason string)      { s.waiting = append(s.waiting, reason) }

// On a zit too old to guard an accept, the sweep tries nothing and says
// so at most once, not once per armed landing per pass.
func TestTheSweepOnAnOldZitIsQuiet(t *testing.T) {
	var buf bytes.Buffer
	log.SetOutput(&buf)
	t.Cleanup(func() { log.SetOutput(os.Stderr) })
	d := Deps{Version: "v", ZitVersion: "0.3.30", Repos: NewRepos(map[string]*Repo{})}
	store := &sweepStub{rows: []oltp.AutoAccept{{Repo: "a", Landing: "L1", Account: "dan"}, {Repo: "b", Landing: "L2", Account: "dan"}}}
	for range 3 {
		if landed := d.AutoAcceptSweep(store); len(landed) != 0 {
			t.Fatalf("FAIL: the sweep landed %v on an old zit", landed)
		}
	}
	out := buf.String()
	if strings.Contains(out, "auto-land") {
		t.Fatalf("FAIL: the sweep logged per landing per pass:\n%s", out)
	}
	if n := strings.Count(out, zitcli.MinVersion); n > 1 {
		t.Fatalf("FAIL: the too-old line was logged %d times:\n%s", n, out)
	}
	if len(store.waiting) == 0 || !strings.Contains(store.waiting[0], zitcli.MinVersion) {
		t.Fatalf("FAIL: an armed landing's page is not told why it waits: %v", store.waiting)
	}
	t.Logf("ok: %d waiting notes, log %q", len(store.waiting), out)
}