Artifact 2cfb0e304ff965ef56258933bf7f0eac1b04e424e0e68d93a54668a9f609e186:
- File
conformance/horizon_test.go
— part of check-in
[75ded1b0e0]
at
2026-10-21 22:19:03
on branch feat/horizon-cones
— feat(horizon): compute the held set K under both rules
draft-fossil-horizon-00 Section 3.1 defines K as a function of C, the rule and the check-in graph, recomputed by whoever needs it and stored by no one. internal/horizon.HeldSet computes it from the plink and leaf tables: one bounded plink load, then iterative breadth-first walks.
Merge parents are followed both ways, as zit's walk does (src/sparse.zig, ClassifyState.extend and walkParents). Under the leaves rule each open leaf's ancestry is held up to where it meets C's ancestry, inclusive, so every equally near common ancestor the walk reaches is held. "Open leaf" is fossil's leaf table minus active closed tags rather than zit's "no held child", which keeps a below-C branch merged into a descendant held back to its fork, so any two open leaves keep their merge base in K. C that is not a held check-in is ErrNotHeld; phantom parents are never entered.
Section 8 requires the walk to carry visited-node bounds: the load and every walk share the 1<<20 bound of the other graph walks and fail with ErrBound, and cancellation is honoured. The conformance suite gains cases for HZ-3.1-1 and HZ-3.1-3, both PARTIAL until the horizon member and the content horizon exist. (user: [email protected], size: 6679) [annotate] [blame] [check-ins using]
package conformance import ( "context" "fmt" "slices" "testing" "time" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/horizon" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/sync" ) // The horizon suite tracks draft-fossil-horizon-00. Its cases are the half // of §3.3 that binds an implementation predating the document -- a cone // serialized with a version go-libfossil does not know, as a cone carrying // a horizon member will be, is refused rather than read as path members -- // and the §3.1 held set, computed by internal/horizon. func horizonSuiteCases() []Case { return []Case{horizonUnknownVersion, horizonHeldSetRules, horizonHeldSetRecomputed} } var ( horizonUnknownVersion = Case{ID: "horizon.3.3.unknown-version", Req: "HZ-3.3-2"} horizonHeldSetRules = Case{ID: "horizon.3.1.held-set-rules", Req: "HZ-3.1-1"} horizonHeldSetRecomputed = Case{ID: "horizon.3.1.held-set-recomputed", Req: "HZ-3.1-3"} ) func TestHorizon(t *testing.T) { check(t, horizonUnknownVersion, func() error { // A version-2 cone with one inclusion, relabelled as each later // version: a reader that skipped the version would see "src". for v := byte(3); v < 8; v++ { forged := []byte{0, v, 1, 0, 3, 0, 's', 'r', 'c'} if c, err := cone.Parse(forged); err == nil { return expectf("serialized version %d read as %v", v, c) } } return nil }) check(t, horizonHeldSetRules, func() error { return hzRules(t) }) check(t, horizonHeldSetRecomputed, func() error { return hzRecomputed(t) }) } // hzGraph is trunk c1..c4 with C = c3, a branch feat forked at c1 (below C) // and merged into c4's child m, and a branch side forked at C itself. type hzGraph struct { f *scRepo c1, c2, c3, c4, m libfossil.FslID feat, side1, side2, tip libfossil.FslID } func hzBranch(name string) []deck.TagCard { return []deck.TagCard{ {Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: name}, {Type: deck.TagPropagating, Name: "sym-" + name, UUID: "*"}, {Type: deck.TagCancel, Name: "sym-trunk", UUID: "*"}, } } func hzCommit(t *testing.T, f *scRepo, name string, parents []libfossil.FslID, tags []deck.TagCard) libfossil.FslID { t.Helper() f.when = f.when.Add(time.Hour) rid, _, err := manifest.Checkin(f.r, manifest.CheckinOpts{ Files: []manifest.File{{Name: name, Content: []byte(name + "\n")}}, Comment: name, User: "u", Parent: parents[0], MergeParents: parents[1:], Time: f.when, Tags: tags, }) if err != nil { t.Fatal(err) } return rid } func newHZGraph(t *testing.T) *hzGraph { t.Helper() g := &hzGraph{f: newSCSyncRepo(t, scSyncFiles())} g.c1 = g.f.c1 g.c2 = hzCommit(t, g.f, "c2", []libfossil.FslID{g.c1}, nil) g.c3 = hzCommit(t, g.f, "c3", []libfossil.FslID{g.c2}, nil) g.c4 = hzCommit(t, g.f, "c4", []libfossil.FslID{g.c3}, nil) g.feat = hzCommit(t, g.f, "feat", []libfossil.FslID{g.c1}, hzBranch("feat")) g.m = hzCommit(t, g.f, "m", []libfossil.FslID{g.c4, g.feat}, nil) g.side1 = hzCommit(t, g.f, "side1", []libfossil.FslID{g.c3}, hzBranch("side")) g.side2 = hzCommit(t, g.f, "side2", []libfossil.FslID{g.side1}, nil) return g } func hzWant(r *repo.Repo, c libfossil.FslID, rule horizon.Rule, want ...libfossil.FslID) error { got, err := horizon.HeldSet(context.Background(), r.DB(), c, rule) if err != nil { return err } slices.Sort(want) if !slices.Equal(got, want) { return expectf("%v held set = %v, want %v", rule, got, want) } return nil } // hzRules: descendants holds C, its trunk descendants (the merge m among // them) and the branch forked at C; leaves adds the below-C branch feat, // still an open leaf after its merge, back to c1, its nearest common // ancestor with C. c2 is on no leaf's path and stays out under both. func hzRules(t *testing.T) error { g := newHZGraph(t) if err := hzWant(g.f.r, g.c3, horizon.Descendants, g.c3, g.c4, g.m, g.side1, g.side2); err != nil { return err } return hzWant(g.f.r, g.c3, horizon.Leaves, g.c3, g.c4, g.m, g.side1, g.side2, g.feat, g.c1) } // hzUUIDs names a held set by artifact hash, so sets computed in two // repositories compare. func hzUUIDs(r *repo.Repo, c string, rule horizon.Rule) ([]string, error) { var rid libfossil.FslID if err := r.DB().QueryRow("SELECT rid FROM blob WHERE uuid=?", c).Scan(&rid); err != nil { return nil, err } k, err := horizon.HeldSet(context.Background(), r.DB(), rid, rule) if err != nil { return nil, err } out := make([]string, 0, len(k)) for _, rid := range k { var uuid string if err := r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil { return nil, err } out = append(out, uuid) } slices.Sort(out) return out, nil } // hzRecomputed: a clone holding the origin's manifests computes the // origin's held set from its own tables, and after a new check-in arrives // both recompute a set that includes it. Nothing is stored between the // computations; each reads the check-in graph as it then stands. func hzRecomputed(t *testing.T) error { g := newHZGraph(t) peer := &scSyncPeer{r: g.f.r} clone, _ := scSyncClone(t, peer, sync.CloneOpts{}) var c string if err := g.f.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", g.c3).Scan(&c); err != nil { return err } compare := func(stage string) ([]string, error) { var last []string for _, rule := range []horizon.Rule{horizon.Descendants, horizon.Leaves} { atOrigin, err := hzUUIDs(g.f.r, c, rule) if err != nil { return nil, err } atClone, err := hzUUIDs(clone, c, rule) if err != nil { return nil, err } if !slices.Equal(atOrigin, atClone) { return nil, fmt.Errorf("%s %v: origin %v, clone %v", stage, rule, atOrigin, atClone) } last = atOrigin } return last, nil } before, err := compare("cloned") if err != nil { return err } g.tip = hzCommit(t, g.f, "tip", []libfossil.FslID{g.side2}, nil) if _, err := sync.Sync(context.Background(), clone, peer, sync.SyncOpts{Pull: true}); err != nil { return err } after, err := compare("pulled") if err != nil { return err } var tip string if err := g.f.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", g.tip).Scan(&tip); err != nil { return err } if slices.Contains(before, tip) || !slices.Contains(after, tip) || len(after) != len(before)+1 { return expectf("held set %v after the new check-in %s arrived, %v before", after, tip, before) } return nil }