Skip to content
quarry Artifact [ee2a67262c]
Login

Artifact [ee2a67262c]

Artifact ee2a67262c6fb91dec3d0b99d12d18647b8900cb7d75bbc19e8c653d695ea095:


package zitcli

import (
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

// acceptFixture is a real store with feat one check-in ahead of trunk
// and a landing of feat open onto trunk. It answers the store, the
// landing id and feat's tip.
func acceptFixture(t *testing.T) (dir, id, tip string) {
	t.Helper()
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	dir = t.TempDir()
	zitIn(t, dir, "init")
	writeIn(t, dir, "f.txt", "root\n")
	zitIn(t, dir, "add", "f.txt")
	zitIn(t, dir, "commit", "-m", "root", "--user", "u")
	writeIn(t, dir, "g.txt", "work\n")
	zitIn(t, dir, "add", "g.txt")
	zitIn(t, dir, "commit", "-m", "work", "--branch", "feat", "--user", "u")
	c := New(dir)
	id, err := c.LandingOpen(testToken, "dan", "feat", "trunk", "land feat", "")
	if err != nil {
		t.Fatal(err)
	}
	tip, _, err = c.Info(t.Context(), "feat")
	if err != nil {
		t.Fatal(err)
	}
	return dir, id, tip
}

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)
}

func writeIn(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)
	}
}

// The accept names the tip the caller checked, so zit merges that
// check-in or nothing.
func TestLandingAcceptPassesTheCheckedTip(t *testing.T) {
	argv := filepath.Join(t.TempDir(), "argv")
	fakeZit(t, `printf '%s\n' "$@" > '`+argv+`'; echo "accepted abc feat -> trunk"`)
	c := &Client{repo: "store"}
	const tip = "4e57933c87b08073006071b51eea5d89d873c467fd401f8fe132d1e424e66051"
	if _, err := c.LandingAccept(testToken, "L1", "dan", "", "", tip); err != nil {
		t.Fatal(err)
	}
	b, err := os.ReadFile(argv)
	if err != nil {
		t.Fatal(err)
	}
	args := strings.Split(strings.TrimSpace(string(b)), "\n")
	found := false
	for i := range args[:len(args)-1] {
		if args[i] == "--expect-tip" && args[i+1] == tip {
			found = true
		}
	}
	if args[0] != "landing" || args[1] != "accept" || !found {
		t.Fatalf("FAIL: argv %q; want landing accept ... --expect-tip %s", args, tip)
	}
	t.Log("ok")
}

// An accept without a checked tip would merge whatever zit finds. The
// client refuses it before spawning anything.
func TestLandingAcceptRefusesWithoutATip(t *testing.T) {
	fakeZit(t, `echo "spawned" >&2; exit 99`)
	c := &Client{repo: "store"}
	_, err := c.LandingAccept(testToken, "L1", "dan", "", "", "")
	if err == nil || strings.Contains(err.Error(), "spawned") {
		t.Fatalf("FAIL: an accept with no tip = %v; want a refusal before zit runs", err)
	}
	t.Logf("ok: %v", err)
}

// accept goes through LandingAccept only, so no caller can merge by
// branch name without naming what it checked.
func TestLandingSayWillNotAccept(t *testing.T) {
	fakeZit(t, `echo "accepted abc feat -> trunk"`)
	c := &Client{repo: "store"}
	if _, err := c.LandingSay(testToken, "accept", "L1", "dan", "", "", ""); err == nil {
		t.Fatal("FAIL: LandingSay accepted without an expected tip")
	}
	t.Log("ok")
}

// A push after the check moved the branch: zit refuses with exit 6 and
// writes nothing. The client says so as its own error, naming both
// tips, distinct from a conflict and from a fault.
func TestLandingAcceptOnAMovedBranchIsBranchMoved(t *testing.T) {
	dir, id, checked := acceptFixture(t)
	writeIn(t, dir, "h.txt", "unchecked\n")
	zitIn(t, dir, "add", "h.txt")
	zitIn(t, dir, "commit", "-m", "late push", "--user", "u")
	c := New(dir)
	now, _, err := c.Info(t.Context(), "feat")
	if err != nil {
		t.Fatal(err)
	}
	_, err = c.LandingAccept(testToken, id, "dan", "", "", checked)
	var moved *BranchMoved
	if !errors.As(err, &moved) {
		t.Fatalf("FAIL: accept on a moved branch = %v; want *BranchMoved", err)
	}
	var conflict *LandingConflict
	if errors.As(err, &conflict) || errors.Is(err, ErrNoEntry) {
		t.Fatalf("FAIL: a moved branch also reads as a conflict or a refusal: %v", err)
	}
	if moved.Branch != "feat" || moved.Current != now || moved.Expected != checked {
		t.Fatalf("FAIL: BranchMoved = %+v; want feat, current %s, expected %s", moved, now, checked)
	}
	if out := zitIn(t, dir, "landing", "ls"); !strings.Contains(out, id[:10]) {
		t.Fatalf("FAIL: the landing is not open after a refused accept:\n%s", out)
	}
	t.Logf("ok: %v", err)
}

// The tip it checked is the tip it merges.
func TestLandingAcceptAtTheCheckedTipMerges(t *testing.T) {
	dir, id, checked := acceptFixture(t)
	if _, err := New(dir).LandingAccept(testToken, id, "dan", "", "", checked); err != nil {
		t.Fatalf("FAIL: accept at the checked tip: %v", err)
	}
	if out := zitIn(t, dir, "landing", "ls"); strings.Contains(out, id[:10]) {
		t.Fatalf("FAIL: the landing is still open after its accept:\n%s", out)
	}
	t.Log("ok")
}

// A zit older than the flag rejects it as a usage error. That must
// never degrade into an unguarded accept, and the error names the zit
// the forge needs.
func TestLandingAcceptOnAnOldZitNamesTheVersionNeeded(t *testing.T) {
	fakeZit(t, `echo "zit landing: unknown option '--expect-tip'" >&2; echo "try 'zit landing --help'" >&2; exit 2`)
	c := &Client{repo: "store"}
	_, err := c.LandingAccept(testToken, "L1", "dan", "", "",
		"4e57933c87b08073006071b51eea5d89d873c467fd401f8fe132d1e424e66051")
	if !errors.Is(err, ErrZitTooOld) || !strings.Contains(err.Error(), MinVersion) {
		t.Fatalf("FAIL: old zit = %v; want ErrZitTooOld naming %s", err, MinVersion)
	}
	t.Logf("ok: %v", err)
}

func TestVersionAtLeast(t *testing.T) {
	for _, c := range []struct {
		have, want string
		ok         bool
	}{
		{"0.3.31", "0.3.31", true},
		{"0.3.32", "0.3.31", true},
		{"0.4.0", "0.3.31", true},
		{"1.0.0", "0.3.31", true},
		{"0.3.30", "0.3.31", false},
		{"0.3.4", "0.3.31", false},
		{"0.2.99", "0.3.31", false},
		{"", "0.3.31", false},
		{"garbage", "0.3.31", false},
	} {
		if got := VersionAtLeast(c.have, c.want); got != c.ok {
			t.Errorf("FAIL: VersionAtLeast(%q, %q) = %v, want %v", c.have, c.want, got, c.ok)
		}
	}
}