Artifact bf2b63243490b0bfeaf48ac36f15fbf4a3d84149274fe67756ae8562f770672a:
- File
internal/manifest/crosslink_leaf_test.go
— part of check-in
[245a0ed306]
at
2026-10-20 02:08:49
on branch feat/submanifest-reader
— conformance: re-pin the three drafts to their -00 names
zit check-in 68a3f76cb7 renames the artifact-format, repo-state and sync-protocol drafts from -01 to -00 in their headers, cross-references and reference entries, and retitles each Changes appendix "Changes in This Revision". No normative text changed and no section number moved.
The pinned copies take zit's file names, the requirement tables pin the new hashes, and the comments, tests and docs that cited the -01 drafts cite the -00 ones. The refresh script copies every draft under its own name. The sparse-cone copy is re-pinned to d497c2432f, whose text is unchanged. Coverage counts are identical before and after. (user: [email protected], size: 10628) [annotate] [blame] [check-ins using] [more...]
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-00 // 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.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.leaves.Len(); got != 0 { t.Fatalf("schedule after a rolled-back transaction has %d entries, want 0", got) } }