Skip to content
quarry Artifact [8efc9d7754]
Login

Artifact [8efc9d7754]

Artifact 8efc9d7754abf806aef860d67f072edb20d1b4aa5a06b2a9906ef48b1480f83c:


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

// The index build is not resumable: zit writes it only when the whole
// walk completes, so a warm that is never cut off can hold a core for
// the life of the process and still leave nothing behind. It must end,
// and it must not come back for the same store.
func TestFileHistoryWarmGivesUpAtTheDeadline(t *testing.T) {
	fakeZit(t, "exec sleep 30")
	defer func(d time.Duration) { warmFinfoTimeout = d }(warmFinfoTimeout)
	warmFinfoTimeout = 300 * time.Millisecond
	t.Cleanup(func() { finfoGaveUp.Delete("slowstore") })

	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")
	}
	if _, gave := finfoGaveUp.Load("slowstore"); !gave {
		t.Fatal("the warm timed out without recording that it had")
	}

	// And it does not come back. A retry per tip move is the full
	// deadline again, for a result the last attempt already ruled out.
	start := time.Now()
	warmFileHistory(zitcli.New(t.TempDir()), "tip", "slowstore")
	if d := time.Since(start); d > 50*time.Millisecond {
		t.Errorf("the warm retried a store it had given up on: took %s", d)
	}
}

// Giving up on one store says nothing about the others.
func TestFileHistoryWarmStillTriesOtherStores(t *testing.T) {
	calls := fakeZit(t, "echo done")
	t.Cleanup(func() { finfoGaveUp.Delete("one"); finfoGaveUp.Delete("two") })
	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)
	}
	if _, gave := finfoGaveUp.Load("one"); gave {
		t.Error("a store that answered was recorded as having given up")
	}
}