Skip to content
quarry Artifact [61dc493c12]
Login

Artifact [61dc493c12]

Artifact 61dc493c121a8136b3d058aa82ce5c0f6da4a7e305dadf9ab495aa1dc8424700:


package main

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// fakeZit puts a `zit` on PATH in place of the real one. `body` is the
// shell it runs; the tally counts invocations for the cases where the
// process is allowed to finish (a killed one may not flush it, which is
// why the deadline case asserts on behaviour rather than on this count).
func fakeZit(t *testing.T, body string) (calls func() int) {
	t.Helper()
	dir := t.TempDir()
	tally := filepath.Join(dir, "calls")
	script := "#!/bin/sh\necho x >> " + tally + "\n" + body + "\n"
	if err := os.WriteFile(filepath.Join(dir, "zit"), []byte(script), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
	return func() int {
		b, err := os.ReadFile(tally)
		if err != nil {
			return 0
		}
		return strings.Count(string(b), "x")
	}
}

// A background warm must end rather than hold a core for the life of
// the process. Since zit 0.3.18 the walk it interrupts is checkpointed,
// so ending it costs the progress of one chunk rather than all of it —
// which is why the store is retried afterwards rather than abandoned.
func TestFileHistoryWarmGivesUpAtTheDeadline(t *testing.T) {
	fakeZit(t, "exec sleep 30")
	defer func(d time.Duration) { warmFinfoTimeout = d }(warmFinfoTimeout)
	warmFinfoTimeout = 300 * time.Millisecond

	done := make(chan time.Duration, 1)
	go func() {
		start := time.Now()
		warmFileHistory(zitcli.New(t.TempDir()), "tip", "slowstore")
		done <- time.Since(start)
	}()
	select {
	case d := <-done:
		// Generously above the deadline, tightly below the fake's sleep:
		// passing here means the deadline ended it, not the sleep.
		if d > 10*time.Second {
			t.Fatalf("the warm ran %s against a %s deadline", d, warmFinfoTimeout)
		}
	case <-time.After(25 * time.Second):
		t.Fatal("the warm never returned: the deadline does not reach the subprocess")
	}
	// And it DOES come back. Each attempt resumes the checkpointed
	// walk, so a store needing several deadlines converges across them;
	// refusing to retry would strand it one chunk short forever. This
	// is the opposite of what this test asserted while the build was
	// all-or-nothing.
	//
	// Measured by how long the second call takes, not by counting
	// invocations: the fake is KILLED at the deadline and a killed
	// process need not have flushed its tally, so the count is not a
	// reliable witness here. A retry spends the deadline again; an
	// abandoned store would return at once.
	start := time.Now()
	warmFileHistory(zitcli.New(t.TempDir()), "tip", "slowstore")
	if d := time.Since(start); d < warmFinfoTimeout/2 {
		t.Errorf("a timed-out store was not retried: second call took %s", d)
	}
}

// Each store gets its own attempt.
func TestFileHistoryWarmStillTriesOtherStores(t *testing.T) {
	calls := fakeZit(t, "echo done")
	warmFileHistory(zitcli.New(t.TempDir()), "tip", "one")
	warmFileHistory(zitcli.New(t.TempDir()), "tip", "two")
	if n := calls(); n != 2 {
		t.Errorf("each store gets its own attempt: %d invocations", n)
	}
}