Skip to content
go-libfossil Artifact [c8e18149d1]
Login

Artifact [c8e18149d1]

Artifact c8e18149d1631e9f5763914ca2366d8646b9e78f8bb479ea15a7a57d9d3aed91:


package manifest

import (
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/blob"
	"github.com/danmestas/go-libfossil/internal/deck"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
	"github.com/danmestas/go-libfossil/internal/repo"
)

// These tests drive the writer of draft-fossil-submanifests-00 through
// Checkin, the one place check-in manifests are made: the emission gate
// (§8.1), the tree stored beside its manifest (§6.2), pinning and selecting
// k (§6.3, §8.1) and re-tuning it (§6.4).

// sha3Repo is a test repository that names artifacts as the vectors do.
func sha3Repo(t *testing.T) *repo.Repo {
	t.Helper()
	r := setupTestRepo(t)
	if err := r.SetHashPolicy("sha3"); err != nil {
		t.Fatal(err)
	}
	return r
}

func a1CheckinFiles() []File {
	return []File{
		{Name: "README.md", Content: []byte("# Example\n")},
		{Name: "src/main.c", Content: []byte("int main(void){return 0;}\n")},
		{Name: "src/node11.c", Content: []byte("/* node11 */\n")},
		{Name: "src/util.c", Content: []byte("/* util */\n")},
	}
}

func setSettings(t *testing.T, r *repo.Repo, kv ...string) {
	t.Helper()
	for i := 0; i+1 < len(kv); i += 2 {
		if err := r.SetConfig(kv[i], kv[i+1]); err != nil {
			t.Fatal(err)
		}
	}
}

var writeTime = time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC)

func commit(t *testing.T, r *repo.Repo, opts CheckinOpts) (libfossil.FslID, *deck.Deck) {
	t.Helper()
	if opts.User == "" {
		opts.User = "drh"
	}
	if opts.Time.IsZero() {
		opts.Time = writeTime
	}
	rid, _, err := Checkin(r, opts)
	if err != nil {
		t.Fatalf("Checkin: %v", err)
	}
	d, err := GetManifest(r, rid)
	if err != nil {
		t.Fatal(err)
	}
	return rid, d
}

func seen(t *testing.T, r *repo.Repo) bool {
	t.Helper()
	ok, err := SeenSubmanifest(r.DB())
	if err != nil {
		t.Fatal(err)
	}
	return ok
}

// §8.1: emission is off by default, and the manifest stays flat.
func TestCheckinFlatByDefault(t *testing.T) {
	r := sha3Repo(t)
	_, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "flat"})
	if len(d.S) != 0 || len(d.F) != 4 || seen(t, r) {
		t.Fatalf("default commit: %d S cards, %d F cards, seen %v", len(d.S), len(d.F), seen(t, r))
	}
}

// §6.2: with emission on at k = 3 the A.1 files go to the two leaves of
// Appendix A.2, the manifest carries their S cards and no F card, the tree
// walks back to the four files, and the indicator of §8.1 is set.
func TestCheckinEmitsTree(t *testing.T) {
	r := sha3Repo(t)
	setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3")
	rid, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "tree"})
	if len(d.F) != 0 || len(d.S) != 2 ||
		d.S[0].UUID != "aa9d404c14fe4dadd2ab398961fb0dfe650a877c061f650d8233a5888e920acf" ||
		d.S[1].UUID != "a50a722b465ed67b42ee0fbe61e4a19d6b1d22940ba5740e99e450921e3f52ea" {
		t.Fatalf("manifest S %+v F %d", d.S, len(d.F))
	}
	files, err := ListFiles(r, rid)
	if err != nil || len(files) != 4 || files[3].Name != "src/util.c" {
		t.Fatalf("ListFiles = %+v, %v", files, err)
	}
	if !seen(t, r) {
		t.Fatal("seen-submanifest not recorded")
	}
	for _, s := range d.S {
		leaf, _ := blob.Exists(r.DB(), s.UUID)
		var n int
		r.DB().QueryRow("SELECT count(*) FROM unsent WHERE rid=?", leaf).Scan(&n)
		if n != 1 {
			t.Fatalf("leaf %s is not marked unsent", s.UUID[:10])
		}
	}
}

// §4.4: a tree check-in carries no B card even when a delta is asked for;
// a flat construction under emission still may be a delta.
func TestCheckinTreeIsNeverDelta(t *testing.T) {
	r := sha3Repo(t)
	parent, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "base"})
	setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3")
	files := a1CheckinFiles()
	files[1].Content = []byte("int main(void){return 1;}\n")
	_, d := commit(t, r, CheckinOpts{Files: files, Comment: "edit", Parent: parent, Delta: true,
		Time: writeTime.Add(time.Hour)})
	if d.B != "" || len(d.S) != 2 {
		t.Fatalf("B %q with %d S cards", d.B, len(d.S))
	}
}

// §6.3: a stored k outside 1..12 falls back to flat; §7.2: a commit made in
// a session whose server sent avoid-submanifests is flat.
func TestCheckinFlatFallbacks(t *testing.T) {
	for _, tc := range []struct {
		name, k string
		avoid   bool
	}{{"k 13", "13", false}, {"k 0", "0", false}, {"k unset", "", false}, {"avoid", "3", true}} {
		t.Run(tc.name, func(t *testing.T) {
			r := sha3Repo(t)
			setSettings(t, r, SettingEnableSubmanifests, "on")
			if tc.k != "" {
				setSettings(t, r, SettingSubmanifestK, tc.k)
			}
			_, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "c", AvoidSubmanifests: tc.avoid})
			if len(d.S) != 0 || len(d.F) != 4 || seen(t, r) {
				t.Fatalf("%d S cards, %d F cards", len(d.S), len(d.F))
			}
		})
	}
}

// subCount counts the sub-manifests r holds.
func subCount(t *testing.T, r *repo.Repo) int {
	t.Helper()
	subs := 0
	rows, err := r.DB().Query("SELECT uuid FROM blob WHERE size>=0")
	if err != nil {
		t.Fatal(err)
	}
	var uuids []string
	for rows.Next() {
		var u string
		rows.Scan(&u)
		uuids = append(uuids, u)
	}
	rows.Close()
	for _, u := range uuids {
		rid, _ := blob.Exists(r.DB(), u)
		d, err := GetManifest(r, rid)
		if err != nil {
			continue
		}
		if d.Type == deck.Submanifest {
			subs++
		}
	}
	return subs
}

// §6.2: content addressing shares every unchanged sub-manifest, so editing
// one file adds exactly one leaf.
func TestCheckinReusesUnchangedLeaves(t *testing.T) {
	r := sha3Repo(t)
	setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3")
	parent, first := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "one"})
	files := a1CheckinFiles()
	files[3].Content = []byte("/* util 2 */\n")
	_, second := commit(t, r, CheckinOpts{Files: files, Comment: "two", Parent: parent,
		Time: writeTime.Add(time.Hour)})
	if second.S[0] != first.S[0] || second.S[1] == first.S[1] {
		t.Fatalf("S runs %+v then %+v", first.S, second.S)
	}
	if n := subCount(t, r); n != 3 {
		t.Fatalf("%d sub-manifests stored, want 3", n)
	}
}

func settingValue(t *testing.T, r *repo.Repo, name string) string {
	t.Helper()
	v, err := r.Config(name)
	if err != nil {
		return ""
	}
	return v
}

// §8.1: a local enable request pins k by the rules of §6.3, and one that
// cannot be met changes nothing.
func TestEnableSubmanifests(t *testing.T) {
	r := sha3Repo(t)
	one, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles()[:1], Comment: "one file"})
	four, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "four", Parent: one,
		Time: writeTime.Add(time.Hour)})

	unchanged := func(what string) {
		t.Helper()
		if settingValue(t, r, SettingEnableSubmanifests) != "" || settingValue(t, r, SettingSubmanifestK) != "" {
			t.Fatalf("%s left settings behind", what)
		}
	}
	// Appendix A.5: over one name no k is a candidate, so the request fails.
	if _, err := EnableSubmanifests(r, one, 0); !errors.Is(err, errNoSelection) {
		t.Fatalf("enable over one file: %v", err)
	}
	unchanged("selection without a result")
	if _, err := EnableSubmanifests(r, 0, 0); !errors.Is(err, errNoSelection) {
		t.Fatalf("enable with no current check-in: %v", err)
	}
	unchanged("no current check-in")
	for _, k := range []int{-1, 13} {
		if _, err := EnableSubmanifests(r, four, k); err == nil {
			t.Fatalf("enable --k %d accepted", k)
		}
		unchanged(fmt.Sprintf("k %d", k))
	}
	// Selection over A.1: k = 1, 2 and 3 tie, and the lowest wins.
	if k, err := EnableSubmanifests(r, four, 0); err != nil || k != 1 {
		t.Fatalf("enable = %d, %v", k, err)
	}
	if settingValue(t, r, SettingEnableSubmanifests) != "on" || settingValue(t, r, SettingSubmanifestK) != "1" {
		t.Fatal("settings not written")
	}
	// The pin takes precedence; a different explicit k is a re-tune.
	if k, err := EnableSubmanifests(r, four, 1); err != nil || k != 1 {
		t.Fatalf("re-enable at the pin = %d, %v", k, err)
	}
	if _, err := EnableSubmanifests(r, four, 3); err == nil || !strings.Contains(err.Error(), "re-tune") {
		t.Fatalf("enable --k 3 over pin 1: %v", err)
	}
	// An out-of-range pin is no valid pin: an explicit value replaces it.
	setSettings(t, r, SettingEnableSubmanifests, "off", SettingSubmanifestK, "13")
	if k, err := EnableSubmanifests(r, four, 2); err != nil || k != 2 {
		t.Fatalf("enable --k 2 over pin 13 = %d, %v", k, err)
	}
}

// §8.1: a persistence failure partway through enabling leaves neither
// setting applied.
func TestEnableSubmanifestsAtomic(t *testing.T) {
	r := sha3Repo(t)
	four, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "four"})
	if _, err := r.DB().Exec(`CREATE TRIGGER fail_enable BEFORE INSERT ON config
		WHEN NEW.name='enable-submanifests' BEGIN SELECT RAISE(ABORT, 'injected'); END`); err != nil {
		t.Fatal(err)
	}
	if _, err := EnableSubmanifests(r, four, 3); err == nil {
		t.Fatal("enable succeeded through an injected failure")
	}
	if settingValue(t, r, SettingSubmanifestK) != "" || settingValue(t, r, SettingEnableSubmanifests) != "" {
		t.Fatal("a setting was applied by a failed enable")
	}
}

// §6.4: a manual re-tune governs the next commit; leaves whose bytes are the
// same under both constructions are shared, and history is untouched.
func TestRetuneGovernsNextCommit(t *testing.T) {
	r := sha3Repo(t)
	setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3")
	parent, first := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "one"})
	if err := RetuneSubmanifests(r, 13); err == nil {
		t.Fatal("re-tune to 13 accepted")
	}
	if err := RetuneSubmanifests(r, 4); err != nil {
		t.Fatal(err)
	}
	_, second := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "two", Parent: parent,
		Time: writeTime.Add(time.Hour)})
	if len(second.S) != 0 || len(second.F) != 4 {
		t.Fatalf("k=4 commit has %d S cards; A.1 is flat at k = 4", len(second.S))
	}
	if again, _ := GetManifest(r, parent); len(again.S) != len(first.S) {
		t.Fatal("the parent changed")
	}
	if err := RetuneSubmanifests(r, 2); err != nil {
		t.Fatal(err)
	}
	_, third := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "three", Parent: parent,
		Time: writeTime.Add(2 * time.Hour)})
	if len(third.S) != 2 || third.S[0] != first.S[0] || third.S[1] != first.S[1] {
		t.Fatalf("k=2 shares A.1's partition, so its leaves: %+v vs %+v", third.S, first.S)
	}
}

// retuneFiles is a file set on which k = 1 is far from the optimum.
func retuneFiles() []File {
	var out []File
	for i := 0; i < 300; i++ {
		name := fmt.Sprintf("dir%02d/file%03d.txt", i%20, i)
		out = append(out, File{Name: name, Content: []byte(name + "\n")})
	}
	return out
}

// §6.4: only while submanifest-auto-retune is on does a commit re-pin k, and
// then only past the margin; off, the pin stays.
func TestAutoRetune(t *testing.T) {
	var cards []deck.FileCard
	for _, f := range retuneFiles() {
		cards = append(cards, deck.FileCard{Name: f.Name, UUID: strings.Repeat("0", 64)})
	}
	sortCards(cards)
	target, ok := retuneTarget(partitionCosts(cards), 1)
	if !ok {
		t.Fatal("fixture: k = 1 shows no drift")
	}
	for _, auto := range []string{"off", "on"} {
		r := sha3Repo(t)
		setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "1", SettingAutoRetune, auto)
		commit(t, r, CheckinOpts{Files: retuneFiles(), Comment: "c"})
		want := "1"
		if auto == "on" {
			want = fmt.Sprint(target)
		}
		if got := settingValue(t, r, SettingSubmanifestK); got != want {
			t.Fatalf("auto-retune %s: k %s, want %s", auto, got, want)
		}
	}
}

func sortCards(cards []deck.FileCard) {
	for i := 1; i < len(cards); i++ {
		for j := i; j > 0 && cards[j-1].Name > cards[j].Name; j-- {
			cards[j-1], cards[j] = cards[j], cards[j-1]
		}
	}
}

// A duplicate name is refused rather than built into a tree.
func TestCheckinTreeRefusesDuplicateName(t *testing.T) {
	r := sha3Repo(t)
	setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3")
	files := append(a1CheckinFiles(), File{Name: "README.md", Content: []byte("again\n")})
	if _, _, err := Checkin(r, CheckinOpts{Files: files, Comment: "dup", User: "u"}); err == nil {
		t.Fatal("duplicate name accepted")
	}
	var n int
	if err := r.WithTx(func(tx *db.Tx) error {
		return tx.QueryRow("SELECT count(*) FROM event").Scan(&n)
	}); err != nil || n != 0 {
		t.Fatalf("%d events after a refused commit (%v)", n, err)
	}
}