Skip to content
quarry Artifact [0ae989a05a]
Login

Artifact [0ae989a05a]

Artifact 0ae989a05a65f515e01d05d4e89d17bf9384ee2a7ddd9b90c016fc1512725359:


package index

import (
	"testing"

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

// EventKinds is a full scan of the event table — covering index or not,
// that is every artifact in the store — and the dashboard asks EVERY
// repository for it on the landing page. Measured cold across this
// fleet it was 610ms of one page's time, for numbers that only move
// when a fold does.
func TestEventKindsIsMemoisedAndRefreshes(t *testing.T) {
	ix := openIndex(t)
	if got, err := ix.EventKinds(); err != nil || len(got) != 0 {
		t.Fatalf("empty index: %v err=%v", got, err)
	}
	if err := ix.FoldNow([]Parsed{
		{Hash: "c1", Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-01-01T00:00:00", User: "dan", Comment: "one"}},
	}); err != nil {
		t.Fatal(err)
	}
	// The census taken of the empty index is a real answer, so the fold
	// keeps serving it while one refresh runs in the background. Wait
	// for that refresh, as a later dashboard load would.
	ix.warmWG.Wait()
	got, err := ix.EventKinds()
	if err != nil {
		t.Fatal(err)
	}
	if got["ci"] != 1 {
		t.Fatalf("after fold: %v, want one check-in — the memo went stale", got)
	}
	// It IS memoised: a write behind the fold's back is not seen...
	if _, err := ix.db.Exec(`DELETE FROM event`); err != nil {
		t.Fatal(err)
	}
	if got, _ := ix.EventKinds(); got["ci"] != 1 {
		t.Error("not memoised; every dashboard render pays the scan")
	}
	// ...and a fold refreshes it, which is the only thing that moves
	// these numbers in practice. The refresh runs in the background, so
	// the census catches up rather than being current on return.
	if err := ix.FoldNow([]Parsed{
		{Hash: "c2", Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-01-02T00:00:00", User: "dan", Comment: "two"}},
	}); err != nil {
		t.Fatal(err)
	}
	// A census that was warm and held something is never served empty
	// while the refresh runs: the reader gets the old one or the new.
	if got, _ := ix.EventKinds(); len(got) == 0 {
		t.Errorf("served an empty census after a fold on a warm one: %v", got)
	}
	// Compared against the table itself rather than a number I guessed:
	// the property is that the memo matches reality after a fold, not
	// that reality holds any particular count.
	var live int
	if err := ix.db.QueryRow(`SELECT count(*) FROM event WHERE kind = 'ci'`).Scan(&live); err != nil {
		t.Fatal(err)
	}
	ix.warmWG.Wait()
	got, _ = ix.EventKinds()
	if got["ci"] != live {
		t.Errorf("after refold the memo says %d and the table holds %d", got["ci"], live)
	}
}

// The memo outlives the call, so handing a caller the map itself makes
// every reader a potential writer of everyone else's counts.
func TestEventKindsHandsBackACopy(t *testing.T) {
	ix := openIndex(t)
	if err := ix.FoldNow([]Parsed{
		{Hash: "c1", Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-01-01T00:00:00", User: "dan", Comment: "one"}},
	}); err != nil {
		t.Fatal(err)
	}
	first, _ := ix.EventKinds()
	first["ci"] = 9999
	delete(first, "ci")
	second, _ := ix.EventKinds()
	if second["ci"] != 1 {
		t.Fatalf("a caller's edit reached the memo: %v", second)
	}
}