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