Skip to content
quarry Artifact [949a4f0e58]
Login

Artifact [949a4f0e58]

Artifact 949a4f0e5883e7394015c54502e6644ae8e9eecc71f0965a7f6463c3800dd6d6:


package index

import (
	"slices"
	"strings"
	"testing"
)

// pathFixture is a small history with a branch:
//
//	trunk  c1 ── c2 ────────── c3(merge) ── c5
//	         \                /
//	branch    b1 ──────── b2
//
// a.go is added in c1, changed on the branch in b1, and changed again in
// the merge c3. c2 and b2 change other files. dir/ gains a file in c1
// and another in c5.
func pathFixture(t *testing.T) *Index {
	t.Helper()
	ix := openIndex(t)
	type ci struct {
		hash, date, branch string
		parents, files     []string
	}
	for _, c := range []ci{
		{"c1", "2026-01-01T00:00:01", "trunk", nil, []string{"a.go", "dir/x"}},
		{"b1", "2026-01-01T00:00:02", "br", []string{"c1"}, []string{"a.go"}},
		{"c2", "2026-01-01T00:00:03", "trunk", []string{"c1"}, []string{"b.txt"}},
		{"b2", "2026-01-01T00:00:04", "br", []string{"b1"}, []string{"c.txt"}},
		{"c3", "2026-01-01T00:00:05", "trunk", []string{"c2", "b2"}, []string{"a.go", "c.txt"}},
		{"c5", "2026-01-01T00:00:06", "trunk", []string{"c3"}, []string{"dir/y"}},
	} {
		// A trigger writes the check-in's event row.
		if _, err := ix.w.Exec(`INSERT INTO checkin (hash, date, user, comment, branch) VALUES (?, ?, 'dan', ?, ?)`,
			c.hash, c.date, "made "+c.hash, c.branch); err != nil {
			t.Fatal(err)
		}
		for i, p := range c.parents {
			if _, err := ix.w.Exec(`INSERT INTO checkin_parent (child, parent, ord) VALUES (?, ?, ?)`, c.hash, p, i); err != nil {
				t.Fatal(err)
			}
		}
		for _, e := range ancestorPaths(c.files) {
			if _, err := ix.w.Exec(`INSERT INTO filechange (path, checkin, date, dir) VALUES (?, ?, ?, ?)`,
				e.Path, c.hash, c.date, e.Dir); err != nil {
				t.Fatal(err)
			}
		}
	}
	return ix
}

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

// A file's history lists the check-ins that changed it, newest first,
// and nothing else — fossil's file history, read as a timeline.
func TestTimelinePathListsTheCheckinsThatChangedIt(t *testing.T) {
	ix := pathFixture(t)
	for _, c := range []struct {
		path string
		want []string
	}{
		{"a.go", []string{"c3", "b1", "c1"}},
		{"/a.go", []string{"c3", "b1", "c1"}}, // a slash either side is the same path
		{"dir", []string{"c5", "c1"}},         // a folder: anything under it
		{"dir/", []string{"c5", "c1"}},
		{"nope", nil},
	} {
		got, _, err := ix.Timeline(TimelineFilter{Limit: 50, Path: c.path})
		if err != nil {
			t.Fatalf("%s: %v", c.path, err)
		}
		if !slices.Equal(hashesOf(got), c.want) {
			t.Errorf("%s: got %v, want %v", c.path, hashesOf(got), c.want)
		}
	}
}

// The graph joins each version of the path to the one before it along
// each parent line: the merge c3 joins c1 (through c2, which did not
// touch a.go) and b1 (through b2), so the branch forks off and joins
// back instead of the line breaking wherever an unrelated check-in fell.
func TestTimelinePathParentsAreThePathsOwnVersions(t *testing.T) {
	ix := pathFixture(t)
	got, _, err := ix.Timeline(TimelineFilter{Limit: 50, Path: "a.go"})
	if err != nil {
		t.Fatal(err)
	}
	want := map[string][]string{"c3": {"c1", "b1"}, "b1": {"c1"}, "c1": nil}
	for _, e := range got {
		if !slices.Equal(e.Parents, want[e.Hash]) {
			t.Errorf("%s parents: got %v, want %v", e.Hash, e.Parents, want[e.Hash])
		}
	}
	// Children are the path's too: c1's real children include c2, which
	// never touched a.go, and counting it drew a line up off the page.
	children := map[string]int{"c3": 0, "b1": 1, "c1": 2}
	for _, e := range got {
		if e.Children != children[e.Hash] {
			t.Errorf("%s children: got %d, want %d", e.Hash, e.Children, children[e.Hash])
		}
	}
	// The merge is still a merge: the badge reads the check-in's own
	// parents, not the path's.
	if !got[0].IsMerge {
		t.Error("c3 lost its merge flag")
	}
}

// Paging a path's history by cursor visits each check-in once, and a
// branch filter narrows it as on the whole timeline.
func TestTimelinePathPagesAndFilters(t *testing.T) {
	ix := pathFixture(t)
	var seen []string
	f := TimelineFilter{Limit: 1, Path: "a.go"}
	for range 5 {
		page, more, err := ix.Timeline(f)
		if err != nil {
			t.Fatal(err)
		}
		seen = append(seen, hashesOf(page)...)
		if !more {
			break
		}
		f.Before = page[len(page)-1].Date + "~" + page[len(page)-1].Hash
	}
	if !slices.Equal(seen, []string{"c3", "b1", "c1"}) {
		t.Errorf("paged: got %v", seen)
	}
	got, _, err := ix.Timeline(TimelineFilter{Limit: 50, Path: "a.go", Branch: "br"})
	if err != nil {
		t.Fatal(err)
	}
	if !slices.Equal(hashesOf(got), []string{"b1"}) {
		t.Errorf("branch br: got %v", hashesOf(got))
	}
}

func TestPathKind(t *testing.T) {
	ix := pathFixture(t)
	for path, want := range map[string]string{"a.go": "file", "dir": "dir", "dir/": "dir", "nope": ""} {
		got, err := ix.PathKind(path)
		if err != nil {
			t.Fatal(err)
		}
		if got != want {
			t.Errorf("PathKind(%q) = %q, want %q", path, got, want)
		}
	}
}

// A path's history is walked from filechange_by_path, not by reading
// every event in the repository looking for the few that changed it.
func TestTimelinePathWalksFilechange(t *testing.T) {
	ix := openIndex(t)
	q, args, ok, err := ix.timelineQuery(TimelineFilter{Limit: 50, Path: "a.go"})
	if err != nil || !ok {
		t.Fatalf("ok=%v err=%v", ok, err)
	}
	plan := queryPlan(t, ix, q, args...)
	if !strings.Contains(plan, "filechange") || strings.Contains(plan, "SCAN e") {
		t.Errorf("path timeline does not walk filechange by path:\n%s", plan)
	}
}