Skip to content
quarry Artifact [9b95e1ef82]
Login

Artifact [9b95e1ef82]

Artifact 9b95e1ef8286d520fc900997f008c9dc9c0a1597dac83e934901f4944d20c6dc:


package index

import (
	"fmt"
	"slices"
	"testing"

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

// centreFixture is twenty check-ins on trunk, one a minute on two days,
// h00 oldest. Every fifth comment mentions "stylesheet"; the ones on
// odd minutes change a.go.
func centreFixture(t *testing.T) *Index {
	t.Helper()
	ix := openIndex(t)
	var batch []Parsed
	var changes []BulkChanges
	for i := range 20 {
		h := fmt.Sprintf("%02d", i)
		hash := "h" + h + "000000000000000000000000000000000000000"
		day := "2026-09-16"
		if i >= 10 {
			day = "2026-09-17"
		}
		comment := "work " + h
		if i%5 == 0 {
			comment = "the stylesheet " + h
		}
		art := &artifact.Artifact{Type: artifact.Manifest,
			Date: fmt.Sprintf("%sT10:%02d:00", day, i), User: "dan", Comment: comment}
		if i > 0 {
			art.Parents = []string{batch[i-1].Hash}
		}
		batch = append(batch, Parsed{Hash: hash, Art: art})
		files := []string{"b.go"}
		if i%2 == 1 {
			files = []string{"a.go"}
		}
		changes = append(changes, BulkChanges{Hash: hash, Art: art, Changes: files})
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	if err := ix.FoldChangesBulk(changes); err != nil {
		t.Fatal(err)
	}
	return ix
}

func shortHashes(es []TimelineEntry) []string {
	var out []string
	for _, e := range es {
		out = append(out, e.Hash[:3])
	}
	return out
}

// A centred page has the anchor in the middle: half a page newer above
// it, the rest older below, newest first throughout like any page.
func TestTimelineAroundPutsTheAnchorInTheMiddle(t *testing.T) {
	ix := centreFixture(t)
	c, ok, err := ix.TimelineAround(TimelineFilter{Limit: 6, Kind: "ci"}, "h10")
	if err != nil || !ok {
		t.Fatalf("ok=%v err=%v", ok, err)
	}
	want := []string{"h13", "h12", "h11", "h10", "h09", "h08"}
	if got := shortHashes(c.Entries); !slices.Equal(got, want) {
		t.Fatalf("got %v, want %v", got, want)
	}
	if !c.Found || c.Anchor[:3] != "h10" || !c.Newer || !c.Older {
		t.Errorf("found=%v anchor=%s newer=%v older=%v", c.Found, c.Anchor[:3], c.Newer, c.Older)
	}
	// At the top of the history there is nothing newer to show.
	c, _, _ = ix.TimelineAround(TimelineFilter{Limit: 6, Kind: "ci"}, "h19")
	if got := shortHashes(c.Entries); got[0] != "h19" || c.Newer {
		t.Errorf("newest: got %v newer=%v", got, c.Newer)
	}
}

// A date stands for the newest check-in at or before the end of it.
func TestTimelineAroundADate(t *testing.T) {
	ix := centreFixture(t)
	c, ok, err := ix.TimelineAround(TimelineFilter{Limit: 4, Kind: "ci"}, "2026-09-16")
	if err != nil || !ok || c.Anchor[:3] != "h09" || !c.Found {
		t.Fatalf("ok=%v err=%v anchor=%v", ok, err, c.Anchor)
	}
	if _, ok, _ := ix.TimelineAround(TimelineFilter{Limit: 4, Kind: "ci"}, "2001-01-01"); ok {
		t.Error("a date before the history names nothing")
	}
	if _, ok, _ := ix.TimelineAround(TimelineFilter{Limit: 4, Kind: "ci"}, "ffff"); ok {
		t.Error("an unknown hash names nothing")
	}
}

// Centring keeps the filters: around h10 in a.go's history, only the
// check-ins that changed a.go, and h10 (which did not) is placed by its
// date without being claimed as found.
func TestTimelineAroundKeepsThePath(t *testing.T) {
	ix := centreFixture(t)
	c, ok, err := ix.TimelineAround(TimelineFilter{Limit: 4, Path: "a.go"}, "h11")
	if err != nil || !ok {
		t.Fatal(err)
	}
	if got := shortHashes(c.Entries); !slices.Equal(got, []string{"h15", "h13", "h11", "h09"}) || !c.Found {
		t.Errorf("a.go around h11: %v found=%v", got, c.Found)
	}
	c, _, _ = ix.TimelineAround(TimelineFilter{Limit: 4, Path: "a.go"}, "h10")
	if got := shortHashes(c.Entries); !slices.Equal(got, []string{"h13", "h11", "h09", "h07"}) || c.Found {
		t.Errorf("a.go around h10: %v found=%v", got, c.Found)
	}
}

// After is Before's mirror: the events just newer than a cursor, nearest
// first in the walk but returned newest first.
func TestTimelineAfterIsTheNewerPage(t *testing.T) {
	ix := centreFixture(t)
	page, _, err := ix.Timeline(TimelineFilter{Limit: 50, Kind: "ci"})
	if err != nil {
		t.Fatal(err)
	}
	var at TimelineEntry
	for _, e := range page {
		if e.Hash[:3] == "h05" {
			at = e
		}
	}
	got, more, err := ix.Timeline(TimelineFilter{Limit: 3, Kind: "ci", After: at.Date + "~" + at.Hash})
	if err != nil {
		t.Fatal(err)
	}
	if s := shortHashes(got); !slices.Equal(s, []string{"h08", "h07", "h06"}) || !more {
		t.Errorf("after h05: %v more=%v", s, more)
	}
}

// The timeline's own search lists matches in timeline order under its
// filters, so stepping through them walks down the page.
func TestTimelineFindIsInTimelineOrder(t *testing.T) {
	ix := centreFixture(t)
	got, err := ix.TimelineFind(TimelineFilter{}, "stylesheet", 50)
	if err != nil {
		t.Fatal(err)
	}
	short := shortHashes(got)
	if !slices.Equal(short, []string{"h15", "h10", "h05", "h00"}) {
		t.Errorf("matches: %v", short)
	}
	got, _ = ix.TimelineFind(TimelineFilter{Path: "a.go"}, "stylesheet", 50)
	if s := shortHashes(got); !slices.Equal(s, []string{"h15", "h05"}) {
		t.Errorf("matches in a.go's history: %v", got)
	}
	// The last word is matched as a prefix, as the reader types it.
	if got, _ := ix.TimelineFind(TimelineFilter{}, "styles", 50); len(got) != 4 {
		t.Errorf("prefix: %d matches, want 4", len(got))
	}
	if got, _ := ix.TimelineFind(TimelineFilter{}, "nothing-like-it", 50); len(got) != 0 {
		t.Errorf("no match: %v", got)
	}
}