Artifact c79ba515e7fa2baae9a400b809ca8f59ad6f44ce3e5bec7e2f5418fd4d602d21:
- File
internal/content/sparse_horizon_test.go
— part of check-in
[63d67666cd]
at
2026-10-22 23:43:26
on branch feat/horizon-cones
— feat(content): elide out-of-cone nodes under a content horizon
draft-fossil-horizon-00 at zit 3a2df62515 has a path cone elide sub-manifest nodes under either horizon, as the sparse store does (§3.2, §4.1). A content-horizon store held every node of every age, so classification kept a phantom node no in-cone slot reaches as transit and the store kept asking for it. Such a node is now elided, below the horizon too; a node on a path into the cone stays wanted at every age, and a horizon without path members still wants every node.
With nodes elided, a check-in's tree is read by the sparse readiness check before its spine is linked, and that check waited on every covered file. Under a horizon a covered file may lie below it, and which do is placed over the very check-ins the check gates, so a check-in below the horizon was never linked and the derivation lost it. Under a horizon the check no longer waits on files: the spine it publishes reads none, and the classification at rest tells an elided file from a transit one. (user: [email protected], size: 16346) [annotate] [blame] [check-ins using] [more...]
package content_test import ( "context" "errors" "path/filepath" "strings" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/delta" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/simio" ) // hzMode is how the fixture's check-ins name their files: full manifests, // c3 a delta manifest over c2 (naming src/keep.c only through its B // baseline), or sub-manifest trees. type hzMode string const ( hzFull hzMode = "full" hzDelta hzMode = "delta" hzTree hzMode = "tree" ) // hzFixture is c1 -> c2 -> c3. src/keep.c enters at c1 and never changes, // so mlink records it at c1 alone though every check-in names it: the case // a horizon computed from mlink would elide. type hzFixture struct { r *repo.Repo c1, c2, c3 libfossil.FslID // blob is each version's rid, by "keep", "old1", "old2", "gone", "now", // "new"; every one of them is a phantom. blob map[string]libfossil.FslID uuid map[string]string } func newHZFixture(t *testing.T, mode hzMode) *hzFixture { t.Helper() r, err := repo.Create(filepath.Join(t.TempDir(), "hz.fossil"), "u", simio.CryptoRand{}, "") if err != nil { t.Fatal(err) } t.Cleanup(func() { r.Close() }) if mode == hzTree { for k, v := range map[string]string{ manifest.SettingEnableSubmanifests: "on", manifest.SettingSubmanifestK: "1", } { if err := r.SetConfig(k, v); err != nil { t.Fatal(err) } } } f := &hzFixture{r: r, blob: map[string]libfossil.FslID{}, uuid: map[string]string{}} when := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) commit := func(parent libfossil.FslID, delta bool, files ...string) libfossil.FslID { t.Helper() // Held files that never change make a delta manifest smaller than // its full form, so the writer emits one. mf := []manifest.File{ {Name: "lib/x.c", Content: []byte("x\n")}, {Name: "lib/y.c", Content: []byte("y\n")}, {Name: "lib/z.c", Content: []byte("z\n")}, } for i := 0; i < len(files); i += 2 { mf = append(mf, manifest.File{Name: files[i], Content: []byte(files[i+1])}) } when = when.Add(time.Hour) rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: mf, Comment: "c", User: "u", Parent: parent, Time: when, Delta: delta, }) if err != nil { t.Fatal(err) } return rid } f.c1 = commit(0, false, "doc/gone.txt", "gone\n", "src/keep.c", "keep\n", "src/old.c", "old 1\n") f.c2 = commit(f.c1, false, "doc/now.txt", "now\n", "src/keep.c", "keep\n", "src/old.c", "old 2\n") f.c3 = commit(f.c2, mode == hzDelta, "doc/now.txt", "now\n", "src/keep.c", "keep\n", "src/new.c", "new\n", "src/old.c", "old 2\n") if mode == hzDelta { if d := hzDeck(t, r, f.c3); d.B != f.uuidOf(t, f.c2) || hzNames(d.F)["src/keep.c"] { t.Fatalf("c3 is not a delta manifest over c2: %+v", d) } } for key, at := range map[string]struct { rid libfossil.FslID name string }{ "keep": {f.c1, "src/keep.c"}, "old1": {f.c1, "src/old.c"}, "gone": {f.c1, "doc/gone.txt"}, "old2": {f.c2, "src/old.c"}, "now": {f.c2, "doc/now.txt"}, "new": {f.c3, "src/new.c"}, } { e, ok, err := manifest.LookupFile(r, at.rid, at.name) if err != nil || !ok { t.Fatalf("LookupFile %s: %v, %v", at.name, ok, err) } f.uuid[key] = e.UUID } for key, uuid := range f.uuid { f.blob[key] = hzPhantomize(t, r, uuid) } return f } func hzDeck(t *testing.T, r *repo.Repo, rid libfossil.FslID) *deck.Deck { t.Helper() data, err := content.Expand(r.DB(), rid) if err != nil { t.Fatal(err) } d, err := deck.Parse(data) if err != nil { t.Fatal(err) } return d } func hzNames(cards []deck.FileCard) map[string]bool { out := map[string]bool{} for _, f := range cards { out[f.Name] = true } return out } // hzPhantomize turns a held artifact into a phantom, first expanding every // delta over it in place. func hzPhantomize(t *testing.T, r *repo.Repo, uuid string) libfossil.FslID { t.Helper() rid, ok := content.AvailableByUUID(r.DB(), uuid) if !ok { t.Fatalf("hzPhantomize: %s is not held", uuid) } err := r.WithTx(func(tx *db.Tx) error { rows, err := tx.Query("SELECT rid FROM delta WHERE srcid=?", rid) if err != nil { return err } var children []libfossil.FslID for rows.Next() { var child libfossil.FslID if err := rows.Scan(&child); err != nil { return errors.Join(err, rows.Close()) } children = append(children, child) } if err := errors.Join(rows.Err(), rows.Close()); err != nil { return err } for _, child := range children { if err := content.Undelta(tx, child); err != nil { return err } } for _, stmt := range []string{ "DELETE FROM delta WHERE rid=?", "UPDATE blob SET size=-1, content=NULL WHERE rid=?", "INSERT OR IGNORE INTO phantom(rid) VALUES(?)", } { if _, err := tx.Exec(stmt, rid); err != nil { return err } } return nil }) if err != nil { t.Fatal(err) } return rid } func (f *hzFixture) uuidOf(t *testing.T, rid libfossil.FslID) string { t.Helper() var uuid string if err := f.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil { t.Fatal(err) } return uuid } // setHorizon makes the store cone a content horizon at c under rule, with // the path members include (none: horizon-only). func (f *hzFixture) setHorizon(t *testing.T, c string, rule cone.HorizonRule, include ...string) { t.Helper() h, err := cone.NewHorizon(c, rule, cone.DepthContent) if err != nil { t.Fatal(err) } sc, err := cone.NewWithHorizon(include, nil, h) if err != nil { t.Fatal(err) } if err := content.SetStoreCone(f.r.DB(), sc); err != nil { t.Fatal(err) } } // expect requires each named blob in its class and the elided count to be // the number named elided. func (f *hzFixture) expect(t *testing.T, want map[string]content.Absence) { t.Helper() a, err := content.LoadAbsencesContext(context.Background(), f.r.DB()) if err != nil { t.Fatal(err) } elided := 0 for key, k := range want { if k == content.Elided { elided++ } if got := a.Class(f.blob[key]); got != k { t.Errorf("%s = %v, want %v", key, got, k) } } if got := a.Count(content.Elided); got != elided { t.Errorf("elided count %d, want %d", got, elided) } } func hzClasses(transit content.Absence, elided ...string) map[string]content.Absence { out := map[string]content.Absence{} for _, key := range []string{"keep", "old1", "old2", "gone", "now", "new"} { out[key] = transit } for _, key := range elided { out[key] = content.Elided } return out } // Under descendants of c2, K is {c2, c3}. Every blob a K check-in names is // above, including src/keep.c, which mlink places at c1 only and which c2 // names directly or through a sub-manifest node. With C at the delta // manifest c3, K is {c3}, which names it and every other c2 file only // through its B baseline, c2. Blobs only c1 names are elided. func TestHorizonAboveFollowsHeldTrees(t *testing.T) { for _, mode := range []hzMode{hzFull, hzDelta, hzTree} { t.Run(string(mode), func(t *testing.T) { f := newHZFixture(t, mode) c := f.c2 if mode == hzDelta { c = f.c3 } f.setHorizon(t, f.uuidOf(t, c), cone.RuleDescendants) f.expect(t, hzClasses(content.Transit, "old1", "gone")) }) } } // Moving C to an ancestor (a deepen) and losing the cone are // recomputation: the classes follow, and no blob or phantom row changes. func TestHorizonReclassifiesWithoutMigration(t *testing.T) { f := newHZFixture(t, hzTree) dump := func() string { var out string if err := f.r.DB().QueryRow(`SELECT (SELECT group_concat(rid || ':' || size || ':' || uuid, ',') FROM (SELECT * FROM blob ORDER BY rid)) || '|' || (SELECT group_concat(rid, ',') FROM (SELECT * FROM phantom ORDER BY rid))`).Scan(&out); err != nil { t.Fatal(err) } return out } before := dump() f.setHorizon(t, f.uuidOf(t, f.c3), cone.RuleDescendants) f.expect(t, hzClasses(content.Transit, "old1", "gone")) f.setHorizon(t, f.uuidOf(t, f.c1), cone.RuleDescendants) f.expect(t, hzClasses(content.Transit)) f.setHorizon(t, f.uuidOf(t, f.c2), cone.RuleDescendants) f.expect(t, hzClasses(content.Transit, "old1", "gone")) if err := content.SetStoreCone(f.r.DB(), nil); err != nil { t.Fatal(err) } f.expect(t, hzClasses(content.Transit)) if after := dump(); after != before { t.Fatalf("reclassification changed stored rows:\n%s\nwas\n%s", after, before) } } // With path members as well, a blob is kept only when it is above the // horizon and in the path cone. func TestHorizonIntersectsPathCone(t *testing.T) { for _, mode := range []hzMode{hzFull, hzTree} { t.Run(string(mode), func(t *testing.T) { f := newHZFixture(t, mode) f.setHorizon(t, f.uuidOf(t, f.c2), cone.RuleDescendants, "src") f.expect(t, hzClasses(content.Transit, "old1", "gone", "now")) }) } } // A path cone elides sub-manifest nodes under a content horizon as the // sparse store does (draft-fossil-horizon-00 §3.2, §4.1): of every // check-in's tree, below the horizon too, a phantom node on a path into the // cone is wanted and one no in-cone slot reaches is elided. Without path // members every node is wanted. func TestHorizonPathConeElidesNodes(t *testing.T) { f := newHZFixture(t, hzTree) inC2 := hzTreeNodes(t, f.r, f.c2) own := map[string]string{} for uuid, name := range hzTreeNodes(t, f.r, f.c1) { if inC2[uuid] == "" { own[name[:strings.IndexByte(name, '/')]] = uuid } } if own["doc"] == "" || own["src"] == "" { t.Fatalf("c1 has no doc/ and src/ leaves of its own: %v", own) } outside, inside := hzPhantomize(t, f.r, own["doc"]), hzPhantomize(t, f.r, own["src"]) classes := func() (content.Absence, content.Absence) { t.Helper() a, err := content.LoadAbsences(f.r.DB()) if err != nil { t.Fatal(err) } return a.Class(outside), a.Class(inside) } f.setHorizon(t, f.uuidOf(t, f.c2), cone.RuleDescendants, "src") if out, in := classes(); out != content.Elided || in != content.Transit { t.Fatalf("below the horizon with paths: outside node %v, inside node %v; want elided, transit", out, in) } f.setHorizon(t, f.uuidOf(t, f.c2), cone.RuleDescendants) if out, in := classes(); out != content.Transit || in != content.Transit { t.Fatalf("below a horizon alone: outside node %v, inside node %v; want transit, transit", out, in) } } // hzTreeNodes is every leaf node of rid's tree with the name its slot // starts at; interior nodes are left out, so a leaf can be made a phantom // while the slot that names it stays readable. func hzTreeNodes(t *testing.T, r *repo.Repo, rid libfossil.FslID) map[string]string { t.Helper() out := map[string]string{} var walk func(cards []deck.SubmanifestCard) walk = func(cards []deck.SubmanifestCard) { for _, s := range cards { node, ok := content.AvailableByUUID(r.DB(), s.UUID) if !ok { continue } d := hzDeck(t, r, node) if len(d.S) == 0 { out[s.UUID] = s.Name } walk(d.S) } } walk(hzDeck(t, r, rid).S) return out } // A phantom a held artifact is a delta against is transit whatever the // horizon says (draft-fossil-horizon-00 §4.2), and a shunned one is shunned. func TestHorizonDeltaBaseAndShun(t *testing.T) { f := newHZFixture(t, hzFull) target := []byte("derived from old 1\n") d, err := delta.Create([]byte("old 1\n"), target) if err != nil { t.Fatal(err) } name := hash.SHA3(target) if len(f.uuid["old1"]) == 40 { name = hash.SHA1(target) } if _, err := blob.StoreDeltaRaw(f.r.DB(), name, d, f.blob["old1"], nil); err != nil { t.Fatal(err) } if _, err := f.r.DB().Exec("INSERT INTO shun(uuid, mtime) VALUES(?, 0)", f.uuid["gone"]); err != nil { t.Fatal(err) } f.setHorizon(t, f.uuidOf(t, f.c2), cone.RuleDescendants) want := hzClasses(content.Transit) want["gone"] = content.Shunned f.expect(t, want) } // A horizon whose C the store does not hold as a check-in elides nothing: // no blob can be proven below a horizon the store cannot place, and eliding // everything would leave the pull that brings C nothing to ask for. Path // members still elide. func TestHorizonUnplacedElidesNothing(t *testing.T) { f := newHZFixture(t, hzFull) for _, c := range []string{strings.Repeat("ab", 20), f.uuid["old2"]} { f.setHorizon(t, c, cone.RuleDescendants) f.expect(t, hzClasses(content.Transit)) f.setHorizon(t, c, cone.RuleDescendants, "src") f.expect(t, hzClasses(content.Transit, "gone", "now")) } } // The predicate a sync applies to what it received is the classification's: // it places exactly the blobs LoadAbsences elides below the horizon, and it // is nil for a cone without one, which elides nothing by age. func TestHorizonBelowMatchesClassification(t *testing.T) { for _, mode := range []hzMode{hzFull, hzDelta, hzTree} { t.Run(string(mode), func(t *testing.T) { f := newHZFixture(t, mode) c := f.c2 if mode == hzDelta { c = f.c3 } f.setHorizon(t, f.uuidOf(t, c), cone.RuleDescendants) sc, err := content.StoreCone(f.r.DB()) if err != nil { t.Fatal(err) } ctx := context.Background() refs, err := content.LoadConeReferences(ctx, f.r.DB(), sc) if err != nil { t.Fatal(err) } below, err := refs.HorizonBelow(ctx, f.r.DB(), sc) if err != nil || below == nil { t.Fatalf("HorizonBelow = %v, %v", below != nil, err) } for key, rid := range f.blob { want := key == "old1" || key == "gone" if got := below(rid); got != want { t.Errorf("%s below = %v, want %v", key, got, want) } } for _, rid := range []libfossil.FslID{f.c1, f.c2, f.c3} { if below(rid) { t.Errorf("check-in rid=%d placed below the horizon", rid) } } paths, err := cone.New([]string{"src"}, nil) if err != nil { t.Fatal(err) } if none, err := refs.HorizonBelow(ctx, f.r.DB(), paths); err != nil || none != nil { t.Fatalf("a path cone's HorizonBelow = %v, %v; want nil", none != nil, err) } }) } } // A cluster a horizon store was sent is not held: its phantom, which the // cluster tag marks, is elided under a horizon and transit without one, // and a held delta's base stays transit whatever it is. func TestHorizonElidesClusterPhantom(t *testing.T) { f := newHZFixture(t, hzFull) d := &deck.Deck{Type: deck.Cluster, M: []string{f.uuid["old1"]}} data, err := d.Marshal() if err != nil { t.Fatal(err) } cluster := hzPhantomCluster(t, f.r, data) classOf := func() content.Absence { t.Helper() a, err := content.LoadAbsences(f.r.DB()) if err != nil { t.Fatal(err) } return a.Class(cluster) } if got := classOf(); got != content.Transit { t.Fatalf("dense store: cluster phantom %v, want transit", got) } paths, err := cone.New([]string{"src"}, nil) if err != nil { t.Fatal(err) } if err := content.SetStoreCone(f.r.DB(), paths); err != nil { t.Fatal(err) } if got := classOf(); got != content.Transit { t.Fatalf("path cone: cluster phantom %v, want transit", got) } f.setHorizon(t, strings.Repeat("ab", 20), cone.RuleDescendants) if got := classOf(); got != content.Elided { t.Fatalf("horizon: cluster phantom %v, want elided", got) } target := append(data, "derived\n"...) dd, err := delta.Create(data, target) if err != nil { t.Fatal(err) } if _, err := blob.StoreDeltaRaw(f.r.DB(), hash.SHA1(target), dd, cluster, nil); err != nil { t.Fatal(err) } if got := classOf(); got != content.Transit { t.Fatalf("horizon: a held delta's cluster base %v, want transit", got) } } // hzPhantomCluster records data as a cluster this store was sent and did // not keep: a phantom carrying the cluster tag. func hzPhantomCluster(t *testing.T, r *repo.Repo, data []byte) libfossil.FslID { t.Helper() rid, err := blob.StorePhantom(r.DB(), hash.SHA1(data)) if err != nil { t.Fatal(err) } if _, err := r.DB().Exec(`INSERT INTO tagxref(tagid, tagtype, srcid, origid, value, mtime, rid) VALUES(7, 1, ?, ?, NULL, 0, ?)`, rid, rid, rid); err != nil { t.Fatal(err) } return rid }