Skip to content
go-libfossil Artifact [ebef00b549]
Login

Artifact [ebef00b549]

Artifact ebef00b5494684c2a6d74880f29d97d16b51bb8046c185fe176dfaa644240031:


package content

import (
	"encoding/binary"
	"fmt"
	"math"
	"math/rand/v2"
	"slices"
	"testing"

	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
)

func TestTreeIDsRoundTrip(t *testing.T) {
	rng := rand.New(rand.NewPCG(1, 2))
	for _, n := range []int{0, 1, 2, 127, 128, 1000, 5000} {
		set := map[uint32]bool{}
		for len(set) < n {
			set[1+rng.Uint32N(1<<31)] = true
		}
		ids := slices.Sorted(func(yield func(uint32) bool) {
			for id := range set {
				if !yield(id) {
					return
				}
			}
		})
		enc := appendTreeIDs(nil, ids)
		if got := decodeTreeIDs(nil, enc); !slices.Equal(got, ids) {
			t.Fatalf("%d ids: round trip differs", n)
		}
	}
	for _, enc := range [][]byte{
		binary.AppendUvarint(binary.AppendUvarint(nil, math.MaxUint32), 1),
		binary.AppendUvarint(binary.AppendUvarint(nil, 1), math.MaxUint64),
		binary.AppendUvarint(nil, 0),
		{0x80},
	} {
		func() {
			defer func() {
				if recover() == nil {
					t.Errorf("% x decoded", enc)
				}
			}()
			decodeTreeIDs(nil, enc)
		}()
	}
	top := []uint32{1, math.MaxUint32}
	if got := decodeTreeIDs(nil, appendTreeIDs(nil, top)); !slices.Equal(got, top) {
		t.Fatalf("decoded %v, want %v", got, top)
	}
	dense := []uint32{1, 2, 3, 4, 5, 100, 227}
	if enc := appendTreeIDs(nil, dense); len(enc) != len(dense) {
		t.Fatalf("dense ids take %d bytes, want one each", len(enc))
	}
}

// treeIndexFixture is a store's trees as a loader reads them, in the order
// it reads them: full check-ins that drift a few files at a time, a few
// unrelated ones (which a set difference would not shrink), delta
// manifests on those baselines, and sub-manifest trees with their nodes.
func treeIndexFixture(rng *rand.Rand) (names []string, trees []*plainTree) {
	file := func(i int) string { return fmt.Sprintf("%040x", i) }
	current := map[int]bool{}
	for i := 0; i < 300; i++ {
		current[i] = true
	}
	next := 300
	add := func(name string, tr *plainTree) {
		names = append(names, name)
		trees = append(trees, tr)
	}
	for i := 0; i < 120; i++ {
		for j := 0; j < 1+rng.IntN(6); j++ {
			for k := range current {
				delete(current, k)
				break
			}
			current[next] = true
			next++
		}
		files := make([]string, 0, len(current)+1)
		for k := range current {
			files = append(files, file(k))
		}
		if i%37 == 5 {
			files = []string{file(next), file(next + 1)}
			next += 2
		}
		if len(files) > 0 {
			files = append(files, files[0]) // two paths, one blob
		}
		rid := libfossil.FslID(1000 - i)
		add(fmt.Sprintf("c%039d", i), &plainTree{rid: rid, size: int64(10 * i), checkin: true, files: files})
		if i%10 == 3 {
			add(fmt.Sprintf("d%039d", i), &plainTree{rid: rid + 2000, size: 7, checkin: true,
				b: fmt.Sprintf("c%039d", i), files: []string{file(next), file(1)}})
			next++
		}
		if i%25 == 9 {
			node := fmt.Sprintf("n%039d", i)
			add(node, &plainTree{rid: rid + 4000, size: 3, files: []string{file(2), file(next)}})
			add(fmt.Sprintf("t%039d", i), &plainTree{rid: rid + 6000, size: 5, checkin: true,
				s: []string{node, "missing" + node}})
			next++
		}
	}
	add(fmt.Sprintf("e%039d", 0), &plainTree{rid: 9000, size: 11})
	return names, trees
}

// draft-fossil-horizon-00 ยง6.2: a tree's file set read back from the memo's
// encoding is exactly the set its F cards name, whichever order a walk
// asks for the trees in and however long the chain of set differences
// behind it.
func TestTreeIndexRoundTrip(t *testing.T) {
	rng := rand.New(rand.NewPCG(3, 4))
	names, trees := treeIndexFixture(rng)
	x := newTreeIndex()
	for i, tr := range trees {
		x.add(names[i], tr)
	}
	diffs := 0
	for _, name := range names {
		if p := x.piece(name); p.base != nil {
			diffs++
			if p.depth > treeChain {
				t.Fatalf("%s: chain depth %d past %d", name, p.depth, treeChain)
			}
		}
	}
	if diffs < 50 {
		t.Fatalf("%d of %d trees kept as a set difference", diffs, len(names))
	}
	ascending := slices.Collect(func(yield func(int) bool) {
		for i := range names {
			if !yield(i) {
				return
			}
		}
	})
	descending := slices.Clone(ascending)
	slices.Reverse(descending)
	shuffled := slices.Clone(ascending)
	rng.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] })
	for _, order := range [][]int{ascending, descending, shuffled} {
		var d treeDecoder
		for _, i := range order {
			sameTree(t, x, &d, names[i], trees[i])
		}
	}
	for _, i := range shuffled {
		var d treeDecoder
		sameTree(t, x, &d, names[i], trees[i])
	}
}

// An extended index adds trees without changing what the one it extends
// answers.
func TestTreeIndexExtendLeavesBase(t *testing.T) {
	rng := rand.New(rand.NewPCG(5, 6))
	names, trees := treeIndexFixture(rng)
	half := len(names) / 2
	x := newTreeIndex()
	for i := range names[:half] {
		x.add(names[i], trees[i])
	}
	before := x.bytes
	y := x.clone()
	for i := half; i < len(names); i++ {
		y.add(names[i], trees[i])
	}
	if x.bytes != before || y.bytes <= before {
		t.Fatalf("bytes: base %d (was %d), extension %d", x.bytes, before, y.bytes)
	}
	var d treeDecoder
	for i, name := range names {
		if i >= half {
			if x.piece(name) != nil {
				t.Fatalf("%s: the base index holds a tree added to its extension", name)
			}
		} else {
			sameTree(t, x, &d, name, trees[i])
		}
		sameTree(t, y, &d, name, trees[i])
	}
}

// The memo's trees hold no more names than their bound: a publish that
// would pass it starts again from the view's own pieces, and keeps none
// when even those pass it.
func TestPublishTreesNamesBound(t *testing.T) {
	rng := rand.New(rand.NewPCG(7, 8))
	names, trees := treeIndexFixture(rng)
	pieces := func(lo, hi int) map[string]*plainTree {
		out := map[string]*plainTree{}
		for i := lo; i < hi; i++ {
			out[names[i]] = trees[i]
		}
		return out
	}
	half := len(names) / 2
	count := func(lo, hi int) int {
		x := newTreeIndex()
		x.addPieces(names[lo:hi], pieces(lo, hi), math.MaxInt)
		return len(x.names.names)
	}
	first, whole := count(0, half), count(0, len(names))
	limit := first + (whole-first)/2
	if count(half, len(names)) > limit {
		t.Fatalf("fixture: the second half's names pass the bound %d", limit)
	}
	m := &Memo{}
	m.publishTrees(pieces(0, half), limit)
	if m.trees == nil || m.trees.piece(names[0]) == nil {
		t.Fatal("the first publish, under the bound, was not kept")
	}
	m.publishTrees(pieces(half, len(names)), limit)
	if m.trees == nil || m.trees.piece(names[0]) != nil || m.trees.piece(names[len(names)-1]) == nil {
		t.Fatal("past the bound the trees did not start again from the view's pieces")
	}
	if n := len(m.trees.names.names); n > limit {
		t.Fatalf("%d names past the bound %d", n, limit)
	}
	var d treeDecoder
	for i := half; i < len(names); i++ {
		sameTree(t, m.trees, &d, names[i], trees[i])
	}
	m.publishTrees(pieces(0, len(names)), 10)
	if m.trees != nil {
		t.Fatalf("a view whose own pieces pass the bound left %d names", len(m.trees.names.names))
	}
}

func sameTree(t *testing.T, x *treeIndex, d *treeDecoder, name string, want *plainTree) {
	t.Helper()
	p := x.piece(name)
	if p == nil {
		t.Fatalf("%s: not indexed", name)
	}
	if p.rid != want.rid || p.size != want.size || x.names.name(p.b) != want.b {
		t.Fatalf("%s: rid %d size %d b %q, want %d %d %q", name, p.rid, p.size, x.names.name(p.b),
			want.rid, want.size, want.b)
	}
	s := make([]string, len(p.s))
	for i, id := range p.s {
		s[i] = x.names.name(id)
	}
	if !slices.Equal(s, want.s) {
		t.Fatalf("%s: S %v, want %v", name, s, want.s)
	}
	var got []string
	for _, id := range d.files(p) {
		got = append(got, x.names.name(id))
	}
	slices.Sort(got)
	files := slices.Compact(slices.Sorted(slices.Values(want.files)))
	if !slices.Equal(got, files) {
		t.Fatalf("%s: %d files decoded, want %d", name, len(got), len(files))
	}
}

// BenchmarkTreeIndexClone clones an index the size of fossil-scm.org's
// whole tree history, as a publish does.
func BenchmarkTreeIndexClone(b *testing.B) {
	x := newTreeIndex()
	for i := 0; i < 45000; i++ {
		x.intern(fmt.Sprintf("%040x", i))
	}
	for i := 0; i < 21000; i++ {
		x.add(fmt.Sprintf("m%039x", i), &plainTree{rid: libfossil.FslID(i + 1), checkin: true,
			files: []string{fmt.Sprintf("%040x", i), fmt.Sprintf("%040x", i+1)}})
	}
	b.ResetTimer()
	for b.Loop() {
		_ = x.clone()
	}
}