Skip to content
quarry Artifact [b6a1d8eebd]
Login

Artifact [b6a1d8eebd]

Artifact b6a1d8eebd59de12047f97279b19c06a53811bd47cd40f5304dd3e40ed297559:


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("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
}
func (m *movingChecks) PremergeMode(string, string) bool { return false }

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