Skip to content
quarry Artifact [28c9255153]
Login

Artifact [28c9255153]

Artifact 28c92551530a259354ac77a099c2824a9f55da1bb6ae6b03965252ed32fe1471:


package api

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

	"craftdesign.group/quarry/internal/events"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ingest"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/zitcli"
)

// acceptStore is a real store with one open landing of feat onto trunk,
// folded into a real index by a real ingest pipeline — the accept path
// reads state from the index and merges through zit, and both races
// live in the gap between those two.
func acceptStore(t *testing.T) (dir, id string, ix *index.Index, sync 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")
	zwrite(t, dir, "g.txt", "work\n")
	zrun(t, dir, "add", "g.txt")
	zrun(t, dir, "commit", "-m", "work", "--branch", "feat", "--user", "u")
	id, err := zitcli.New(dir).LandingOpen(testToken, "dan", "feat", "trunk", "land feat", "")
	if 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() })
	pipe := ingest.NewPipeline(zitcli.New(dir), ev, ix, "test", "n1")
	if _, err := pipe.Sync(); err != nil {
		t.Fatal(err)
	}
	return dir, id, ix, pipe.Sync
}

// Two accepts pressed at once both read "open" — the settle is a fold
// and then a merge, and nothing held the record between the two. The
// second must be told the record is already being accepted, not merge
// alongside the first.
func TestTwoSimultaneousAcceptsLetExactlyOneThrough(t *testing.T) {
	dir, id, ix, pipeSync := acceptStore(t)
	entered := make(chan struct{})
	release := make(chan struct{})
	var once sync.Once
	syncFn := func() (int, error) {
		first := false
		once.Do(func() { first = true })
		if first {
			// Hold the first accept inside its settle, the window a
			// second press used to walk straight through.
			close(entered)
			<-release
		}
		return pipeSync()
	}
	r := ops.NewRegistry()
	deps := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{
		"test": {Index: ix, Zit: zitcli.New(dir), Sync: syncFn}})}
	if err := RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	accept := func() error {
		_, err := r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: id})
		return err
	}
	firstErr := make(chan error, 1)
	go func() { firstErr <- accept() }()
	select {
	case <-entered:
	case <-time.After(30 * time.Second):
		t.Fatal("the first accept never reached its settle")
	}
	secondErr := make(chan error, 1)
	go func() { secondErr <- accept() }()
	var second error
	select {
	case second = <-secondErr:
	case <-time.After(30 * time.Second):
		close(release)
		t.Fatal("FAIL: the second accept waited on the first instead of being refused")
	}
	close(release)
	first := <-firstErr
	if first != nil {
		t.Fatalf("FAIL: the first accept failed: %v", first)
	}
	if second == nil {
		t.Fatal("FAIL: both accepts proceeded")
	}
	if !errors.Is(second, ops.ErrInvalid) || !strings.Contains(second.Error(), "already being accepted") {
		t.Fatalf("FAIL: the second accept was refused unclearly: %v", second)
	}
	t.Logf("ok: first accepted, second refused: %v", second)
}

// movingChecks is the required-checks policy read at the moment a push
// can still land: it moves the branch once, then asks for nothing.
type movingChecks struct {
	once sync.Once
	move func()
}

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

// A push between the check and the merge moved the branch, and zit
// merges the branch by NAME — so the merge took a tip nothing checked.
// The accept must refuse and name the tip the branch moved to.
func TestAnAcceptRefusesWhenTheBranchMovedAfterItWasChecked(t *testing.T) {
	dir, id, ix, pipeSync := acceptStore(t)
	checked := ztip(t, dir, "feat")
	var moved string
	checks := &movingChecks{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")
	}}
	r := ops.NewRegistry()
	deps := Deps{Version: "v", Checks: checks, Repos: NewRepos(map[string]*Repo{
		"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}
	if err := RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	_, 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 err == nil {
		t.Fatal("FAIL: the accept merged a tip that was never checked")
	}
	if !errors.Is(err, ops.ErrInvalid) || !strings.Contains(err.Error(), short(moved)) {
		t.Fatalf("FAIL: the refusal does not name the moved tip %s: %v", short(moved), err)
	}
	// A refusal a client can act on: a stable code, and the two tips.
	var ref *ops.Refusal
	if !errors.As(err, &ref) || ref.Code != RefusalBranchMoved {
		t.Fatalf("FAIL: the refusal carries no %q code: %#v", RefusalBranchMoved, err)
	}
	if ref.Fields["expected"] != checked || ref.Fields["current"] != moved {
		t.Fatalf("FAIL: refusal fields %v; want expected %s, current %s", ref.Fields, checked, moved)
	}
	if out := zrun(t, dir, "landing", "ls"); !strings.Contains(out, id[:10]) {
		t.Fatalf("FAIL: the landing is no longer open after a refused accept:\n%s", out)
	}
	t.Logf("ok: %v", err)
}

// A tip that cannot be read is not a tip that did not move. Skipping
// the check there would merge whatever zit finds, checked or not, so
// the accept is refused as unavailable and nothing reaches the store.
func TestAnAcceptRefusesWhenTheCheckedTipCannotBeRead(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	// The record is in the index; the branch it names has no check-in
	// there, so the tip lookup comes back empty.
	ix := landingIndex(t, "dan", "feat", "trunk", "")
	dir := t.TempDir()
	if err := zitcli.Init(t.Context(), dir); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{
		"test": {Index: ix, Zit: zitcli.New(dir)}})}); err != nil {
		t.Fatal(err)
	}
	_, err := r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: landingHash})
	if !errors.Is(err, ops.ErrUnavailable) || !strings.Contains(err.Error(), "feat") {
		t.Fatalf("FAIL: accept = %v; want ErrUnavailable naming the branch whose tip is unreadable", err)
	}
	got, gerr := r.Invoke(as("dan", "i"), "landing.get", landingGetIn{Repo: "test", ID: landingHash})
	if gerr != nil {
		t.Fatal(gerr)
	}
	if st := got.(landingGetOut).Landing.State; st != "open" {
		t.Fatalf("FAIL: the landing is %q after a refused accept, want open", st)
	}
	t.Logf("ok: %v", err)
}

func TestARefusalNamesEverySettleVerbInThePastTense(t *testing.T) {
	for verb, want := range map[string]string{"accept": "accepted", "reject": "rejected", "update": "updated"} {
		if got := pastTense(verb); got != want {
			t.Errorf("FAIL: pastTense(%q) = %q, want %q", verb, got, want)
		}
	}
}