Skip to content
quarry Artifact [ca0d97ecbd]
Login

Artifact [ca0d97ecbd]

Artifact ca0d97ecbd199c9ac9b9553d17ac72cdc4eedae85d3dd10ce35c333bfacb1a7d:


package index

import (
	"path/filepath"
	"strings"
	"testing"

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

// forumIndex folds n threads, each one post, a day apart so the order
// is unambiguous.
func forumIndex(t *testing.T, n int) *Index {
	t.Helper()
	ix, err := Open(filepath.Join(t.TempDir(), "i.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	var batch []Parsed
	for i := 0; i < n; i++ {
		h := strings.Repeat("0", 60) + string("0123456789abcdef"[i%16]) + string("0123456789abcdef"[(i/16)%16]) + "ab"
		batch = append(batch, Parsed{Hash: h, Art: &artifact.Artifact{
			Type: artifact.Forum, ThreadTitle: "thread", User: "dan",
			Date: "2026-09-" + string(rune('0'+(i/10)%10)) + string(rune('0'+i%10)) + "T10:00:00",
		}})
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	return ix
}

// Walking the cursor visits every thread exactly once, in the same
// order an unpaged read gives. A keyset cursor that is off by one
// either repeats a row at every boundary or drops one, and both look
// fine on a single page.
func TestWalkingTheForumCursorSeesEachThreadOnce(t *testing.T) {
	ix := forumIndex(t, 25)
	all, err := ix.ForumThreads(0, "")
	if err != nil {
		t.Fatal(err)
	}
	if len(all) != 25 {
		t.Fatalf("FAIL: unpaged read gave %d threads, want 25", len(all))
	}

	var walked []string
	before := ""
	for pages := 0; pages < 20; pages++ {
		page, err := ix.ForumThreads(7, before)
		if err != nil {
			t.Fatal(err)
		}
		if len(page) == 0 {
			break
		}
		for _, th := range page {
			walked = append(walked, th.Root)
		}
		before = ForumCursor(page[len(page)-1])
	}
	if len(walked) != len(all) {
		t.Fatalf("FAIL: walked %d threads, the forum holds %d", len(walked), len(all))
	}
	for i := range all {
		if walked[i] != all[i].Root {
			t.Fatalf("FAIL: paging diverged from the listing's order at row %d", i)
		}
	}
	seen := map[string]bool{}
	for _, r := range walked {
		if seen[r] {
			t.Fatalf("FAIL: %s was returned twice", r[:12])
		}
		seen[r] = true
	}
}

// A limit returns at most that many, and zero means everything — which
// is what the hot sort needs, because it ranks across the whole forum
// before it can say what the first page is.
func TestALimitBoundsThePageAndZeroMeansAll(t *testing.T) {
	ix := forumIndex(t, 25)
	if got, _ := ix.ForumThreads(10, ""); len(got) != 10 {
		t.Errorf("FAIL: limit 10 gave %d", len(got))
	}
	if got, _ := ix.ForumThreads(0, ""); len(got) != 25 {
		t.Errorf("FAIL: limit 0 gave %d, want every thread", len(got))
	}
	if got, _ := ix.ForumThreads(500, ""); len(got) != 25 {
		t.Errorf("FAIL: a limit past the end gave %d", len(got))
	}
}

// The count does not build the threads it counts.
func TestTheCountMatchesTheListing(t *testing.T) {
	ix := forumIndex(t, 25)
	n, err := ix.ForumThreadCount()
	if err != nil {
		t.Fatal(err)
	}
	all, _ := ix.ForumThreads(0, "")
	if n != len(all) {
		t.Errorf("FAIL: count says %d, the listing has %d", n, len(all))
	}
}

// A cursor that is empty or malformed starts at the beginning rather
// than erroring: a stale link is a page, not a 500.
func TestAnUnreadableCursorStartsAtTheTop(t *testing.T) {
	ix := forumIndex(t, 5)
	for _, c := range []string{"", "nonsense", "no-separator-here"} {
		got, err := ix.ForumThreads(0, c)
		if err != nil {
			t.Fatalf("FAIL: cursor %q errored: %v", c, err)
		}
		if len(got) != 5 {
			t.Errorf("FAIL: cursor %q gave %d threads, want the whole listing", c, len(got))
		}
	}
}