Artifact bf0b7594e3dc0922b36b5ce6b06dc4c9319820024cd1906031d0d0a34f37d4c4:
- File
internal/manifest/crosslink_leaf_test.go
— part of check-in
[08820292b8]
at
2026-10-19 20:20:36
on branch perf/tag-repair-batch
— perf(manifest): bound the repair gate's tag propagation by the batch
repairTagPropagation re-propagated tags from every primary parent in the repository at every gate, so the gate at the end of a sweep, clone or sync cost the repository, not the batch. The batch now records the primary parents it writes plink rows for, and the new parents of a reparent, in a set beside its leaf schedule; the set is marked, rolled back and reset with the schedule, and the gate propagates from it alone. A rebuild links every check-in, so its set is every primary parent and nothing changes there.
A phantom-fill cascade's parents are known only to the cascade, so each cascade batch runs the gate itself before it commits rather than leaving propagation to the end of the sync.
Tracked in #432. (user: [email protected], size: 10640) [annotate] [blame] [check-ins using]
package manifest import ( "context" "errors" "fmt" "sort" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/repo" ) // storeCheckin builds a check-in manifest with the given parents and branch // declaration and stores it as a blob without crosslinking it, mirroring the // receiver state after a sync round delivers manifests but before the sweep // runs. branch == "" declares no branch tag, so the check-in inherits its // primary parent's branch through tag propagation the way a plain commit does. func storeCheckin(t *testing.T, r *repo.Repo, comment, branch string, when time.Time, parents ...string) string { t.Helper() content := []byte("content for " + comment) fileUUID := repoHash(r, content) if _, _, err := blob.Store(r.DB(), content); err != nil { t.Fatalf("blob.Store(file for %s): %v", comment, err) } d := &deck.Deck{ Type: deck.Checkin, C: comment, D: when, F: []deck.FileCard{{Name: "f.txt", UUID: fileUUID}}, P: parents, U: deck.User("tester"), } if branch != "" { d.T = []deck.TagCard{ {Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: branch}, {Type: deck.TagSingleton, Name: "sym-" + branch, UUID: "*"}, } } rHash, err := d.ComputeR(func(uuid string) ([]byte, error) { if uuid == fileUUID { return content, nil } return nil, fmt.Errorf("unknown uuid: %s", uuid) }) if err != nil { t.Fatalf("ComputeR(%s): %v", comment, err) } d.R = rHash manifestBytes, err := d.Marshal() if err != nil { t.Fatalf("Marshal(%s): %v", comment, err) } _, uuid, err := blob.Store(r.DB(), manifestBytes) if err != nil { t.Fatalf("blob.Store(manifest %s): %v", comment, err) } return uuid } // leafComments returns the comment of every check-in in the leaf table, so an // assertion failure names the check-ins rather than opaque rids. func leafComments(t *testing.T, r *repo.Repo) []string { t.Helper() rows, err := r.DB().Query(` SELECT e.comment FROM leaf l JOIN event e ON e.objid = l.rid WHERE e.type = 'ci'`) if err != nil { t.Fatalf("query leaf: %v", err) } defer rows.Close() var got []string for rows.Next() { var c string if err := rows.Scan(&c); err != nil { t.Fatalf("scan leaf: %v", err) } got = append(got, c) } if err := rows.Err(); err != nil { t.Fatalf("rows: %v", err) } sort.Strings(got) return got } func assertLeaves(t *testing.T, r *repo.Repo, where string, want []string) { t.Helper() got := leafComments(t, r) if len(got) != len(want) { t.Fatalf("%s: leaf = %v, want %v", where, got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("%s: leaf = %v, want %v", where, got, want) } } } // TestCrosslinkLeafMatchesCanonicalBranchRule pins the leaf rule canonical // fossil implements in src/leaf.c: a check-in is a leaf when no plink child of // it carries the *same* branch name, where a check-in with no branch tagxref // value counts as "trunk". // // leaf_check(): SELECT 1 FROM plink WHERE pid=:rid // AND coalesce((SELECT value FROM tagxref WHERE tagid=TAG_BRANCH // AND rid=:rid),'trunk') // == coalesce((SELECT value FROM tagxref WHERE tagid=TAG_BRANCH // AND rid=plink.cid),'trunk') // // Neither "has any child" nor "has a primary child" is the rule, and each // disagrees with fossil on one arm of the fixture below (issue #189): // // c1 trunk ─┬─> c3 trunk ─┐ // │ ├─> c4 trunk(merge) ──> c5 bar ──> c6 (inherits bar) // └─> c2 foo ───┘ // // - c2 is the tip of branch foo, merged into trunk at c4. It has a child, so // an any-child rule drops it; fossil keeps it because c4 is on trunk. This // is the class that lost 939 of 1,426 leaves on the real corpus. // - c4's only child c5 is its *primary* child, so an isprim rule drops it; // fossil keeps it because c5 is on bar. // - c5 is not a leaf even though c6 declares no branch of its own: c6 // inherits bar by tag propagation, which is why the leaf recompute has to // run after propagation rather than before it. // // The fixture is built from stored manifests and linked by Crosslink, so it // runs in the default suite with no fossil binary. func TestCrosslinkLeafMatchesCanonicalBranchRule(t *testing.T) { r := setupTestRepo(t) base := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC) c1 := storeCheckin(t, r, "c1-trunk-root", "trunk", base) c2 := storeCheckin(t, r, "c2-foo-tip", "foo", base.Add(time.Minute), c1) c3 := storeCheckin(t, r, "c3-trunk", "", base.Add(2*time.Minute), c1) c4 := storeCheckin(t, r, "c4-trunk-merge", "", base.Add(3*time.Minute), c3, c2) c5 := storeCheckin(t, r, "c5-bar-root", "bar", base.Add(4*time.Minute), c4) storeCheckin(t, r, "c6-bar-tip", "", base.Add(5*time.Minute), c5) linked, err := Crosslink(r) if err != nil { t.Fatalf("Crosslink: %v", err) } if linked != 6 { t.Fatalf("Crosslink linked %d checkins, want 6", linked) } assertLeaves(t, r, "after Crosslink", []string{ "c2-foo-tip", // merged branch tip: child c4 is on trunk "c4-trunk-merge", // branch point: primary child c5 is on bar "c6-bar-tip", // ordinary tip }) } // TestCheckinLeafMatchesCanonicalBranchRule is the same rule on the local // commit path. manifest.Checkin maintained leaf by deleting every parent of the // new check-in, which is the any-child rule, so committing onto a new branch // silently retired the branch point (issue #189). func TestCheckinLeafMatchesCanonicalBranchRule(t *testing.T) { r := setupTestRepo(t) base := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC) trunkTip, _, err := Checkin(r, CheckinOpts{ Comment: "c1-trunk-root", User: "tester", Time: base, Files: []File{{Name: "f.txt", Content: []byte("one")}}, }) if err != nil { t.Fatalf("Checkin c1: %v", err) } assertLeaves(t, r, "after root checkin", []string{"c1-trunk-root"}) if _, _, err := Checkin(r, CheckinOpts{ Comment: "c2-foo-root", User: "tester", Time: base.Add(time.Minute), Parent: trunkTip, Files: []File{{Name: "f.txt", Content: []byte("two")}}, Tags: []deck.TagCard{ {Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: "foo"}, {Type: deck.TagSingleton, Name: "sym-foo", UUID: "*"}, }, }); err != nil { t.Fatalf("Checkin c2: %v", err) } // The branch point keeps its leaf row: its only child is on branch foo. assertLeaves(t, r, "after branch checkin", []string{"c1-trunk-root", "c2-foo-root"}) if _, _, err := Checkin(r, CheckinOpts{ Comment: "c3-trunk", User: "tester", Time: base.Add(2 * time.Minute), Parent: trunkTip, Files: []File{{Name: "f.txt", Content: []byte("three")}}, }); err != nil { t.Fatalf("Checkin c3: %v", err) } // c3 is on trunk, so now the branch point does lose its leaf row. assertLeaves(t, r, "after trunk checkin", []string{"c2-foo-root", "c3-trunk"}) } // TestRepairLeafTableKeepsParentlessCheckin pins the one place our recompute // deliberately differs from fossil's leaf_rebuild(), whose candidate set is // `SELECT cid FROM plink` and so cannot return a check-in that has no parent. // Fossil reaches that check-in through leaf_check() instead -- a fresh // repository holding only its initial empty check-in has one leaf row -- and // selecting candidates from event reproduces that without a second pass. func TestRepairLeafTableKeepsParentlessCheckin(t *testing.T) { r := setupTestRepo(t) storeCheckin(t, r, "c1-only", "trunk", time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC)) if _, err := Crosslink(r); err != nil { t.Fatalf("Crosslink: %v", err) } assertLeaves(t, r, "single parentless checkin", []string{"c1-only"}) var plinks int if err := r.DB().QueryRow("SELECT count(*) FROM plink").Scan(&plinks); err != nil { t.Fatalf("count plink: %v", err) } if plinks != 0 { t.Fatalf("plink rows = %d, want 0 (fixture must have no parent edges)", plinks) } } // TestCheckinOntoSingletonBranchKeepsVerdicts pins draft-fossil-repo-state-01 // Section 7.3 on the local commit path: a commit is rechecked, with its // parents, only when a branch row lands on it. c1's branch row is a // singleton, so c2 inherits none and nothing is scheduled: c1 keeps its leaf // row although c2 is on the default branch with it, and c2 is no leaf, as in // fossil's commit (issue #426). func TestCheckinOntoSingletonBranchKeepsVerdicts(t *testing.T) { r := setupTestRepo(t) base := time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC) c1, _, err := Checkin(r, CheckinOpts{ Comment: "c1-singleton-root", User: "tester", Time: base, Files: []File{{Name: "f.txt", Content: []byte("one")}}, Tags: []deck.TagCard{ {Type: deck.TagSingleton, Name: "branch", UUID: "*", Value: "trunk"}, }, }) if err != nil { t.Fatalf("Checkin c1: %v", err) } assertLeaves(t, r, "after c1", []string{"c1-singleton-root"}) if _, _, err := Checkin(r, CheckinOpts{ Comment: "c2-untagged", User: "tester", Time: base.Add(time.Minute), Parent: c1, Files: []File{{Name: "f.txt", Content: []byte("two")}}, }); err != nil { t.Fatalf("Checkin c2: %v", err) } assertLeaves(t, r, "after c2", []string{"c1-singleton-root"}) } // A recheck LinkStored schedules inside a transaction that then fails is // dropped with it: the rolled-back rid may be reused by an unrelated blob. func TestReceiveLinkerWithTxDropsRechecksOnFailure(t *testing.T) { r := setupTestRepo(t) d := &deck.Deck{ Type: deck.Checkin, C: "branch root", U: deck.User("testuser"), D: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), T: []deck.TagCard{{ Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: "trunk", }}, } body, err := d.Marshal() if err != nil { t.Fatalf("Marshal: %v", err) } rid, uuid, err := blob.Store(r.DB(), body) if err != nil { t.Fatalf("Store: %v", err) } linker, err := NewReceiveLinker(r) if err != nil { t.Fatalf("NewReceiveLinker: %v", err) } boom := errors.New("later step failed") err = linker.WithTx(r, func(tx *db.Tx) error { if err := linker.LinkStored(context.Background(), tx, rid, uuid, body); err != nil { return err } if linker.state.sched.leaves.Len() == 0 { t.Fatal("LinkStored scheduled no recheck for a branch tag") } return boom }) if !errors.Is(err, boom) { t.Fatalf("WithTx error = %v, want %v", err, boom) } if got := linker.state.sched.leaves.Len(); got != 0 { t.Fatalf("schedule after a rolled-back transaction has %d entries, want 0", got) } }