package index
import (
"sync"
"testing"
"time"
"craftdesign.group/quarry/internal/artifact"
)
func foldCheckin(t *testing.T, ix *Index, hash, date string) {
t.Helper()
if err := ix.FoldNow([]Parsed{
{Hash: hash, Art: &artifact.Artifact{Type: artifact.Manifest,
Date: date, User: "dan", Comment: hash}},
}); err != nil {
t.Fatal(err)
}
}
// holdKindsScan makes every kinds scan stop on its first row until the
// test ends, and reports when one has started.
func holdKindsScan(t *testing.T, ix *Index) (started <-chan struct{}) {
t.Helper()
gate := make(chan struct{})
in := make(chan struct{})
var once sync.Once
ix.kindsScanRow = func() {
once.Do(func() { close(in) })
<-gate
}
// Registered after openIndex's Close, so it runs first: the held
// scans finish before Close waits for them.
t.Cleanup(func() { close(gate) })
return in
}
func kindsWithin(t *testing.T, ix *Index, d time.Duration) (map[string]int, bool) {
t.Helper()
done := make(chan map[string]int, 1)
go func() {
got, err := ix.EventKinds()
if err != nil {
t.Error(err)
}
done <- got
}()
select {
case got := <-done:
return got, true
case <-time.After(d):
return nil, false
}
}
// The dashboard asks every repository for its kinds, and a mirror folds
// on every pull. A fold must not hand the next reader the scan: the old
// census is served while one refresh runs in the background.
func TestAFoldDoesNotMakeTheNextKindsReaderScan(t *testing.T) {
ix := openIndex(t)
foldCheckin(t, ix, "c1", "2026-01-01T00:00:00")
if _, err := ix.EventKinds(); err != nil {
t.Fatal(err)
}
started := holdKindsScan(t, ix)
foldCheckin(t, ix, "c2", "2026-01-02T00:00:00")
got, ok := kindsWithin(t, ix, 2*time.Second)
if !ok {
t.Fatal("the first reader after a fold waited on the scan")
}
if got["ci"] != 1 {
t.Errorf("reader got %v, want the census from before the fold", got)
}
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("a fold on a warm census started no background refresh")
}
}
// The refresh the fold started lands, and what it stores is the census
// after the fold.
func TestTheKindsRefreshReflectsTheFold(t *testing.T) {
ix := openIndex(t)
foldCheckin(t, ix, "c1", "2026-01-01T00:00:00")
if _, err := ix.EventKinds(); err != nil {
t.Fatal(err)
}
foldCheckin(t, ix, "c2", "2026-01-02T00:00:00")
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
ix.countMu.Lock()
n, fresh := ix.kinds["ci"], ix.kindsGen == ix.foldGen
ix.countMu.Unlock()
if n == 2 && fresh {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Error("the census was not refreshed after the fold")
}
// A repository nobody has asked costs nothing on a fold: a bulk import
// folds in a tight loop before anyone has looked.
func TestAFoldDoesNotWarmAColdKindsCensus(t *testing.T) {
ix := openIndex(t)
foldCheckin(t, ix, "c1", "2026-01-01T00:00:00")
time.Sleep(50 * time.Millisecond)
ix.countMu.Lock()
defer ix.countMu.Unlock()
if ix.kinds != nil {
t.Errorf("a fold warmed a census nobody asked for: %v", ix.kinds)
}
}
// Counts is read on every page of a repository. It must not queue
// behind a kinds scan, which is 0.46s cold on the largest store.
func TestCountsIsNotBlockedByAKindsScan(t *testing.T) {
ix := openIndex(t)
foldCheckin(t, ix, "c1", "2026-01-01T00:00:00")
started := holdKindsScan(t, ix)
go func() { _, _ = ix.EventKinds() }()
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("the kinds scan never started")
}
done := make(chan error, 1)
go func() {
_, err := ix.Counts(false)
done <- err
}()
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(2 * time.Second):
t.Fatal("Counts waited on a kinds scan")
}
}