Skip to content
quarry Artifact [2deaf30c15]
Login

Artifact [2deaf30c15]

Artifact 2deaf30c1562f43e2792cea0b09dc77451b3458df19bfd4b5dd800fd6f16783f:


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