Artifact c51837bd93743690341c093ab90d8cf2943178ab4300ebb915d1a08daa50091e:
- File
internal/horizon/name_test.go
— part of check-in
[017c7f9393]
at
2026-10-24 01:34:52
on branch main
— fix: send a clone's horizon by name even as a full hash
A horizon given through CloneOpts.Horizon or --horizon now always goes out as pragma horizon-name, a full hash at depth 0 included. The origin tries a full hash as a check-in and then as a symbolic name (§6.2.7, C.3), so a branch or tag spelled as 40 hex digits is found; a peer that resolves no name is covered by the local fallback. A horizon held on the store cone still goes out as pragma horizon on every pull, sync and deepen.
nameAuthority now says why crediting the login card to the whole request is harmless, and TestResolveNameBounded fails within 2s with a message naming the unbounded walk. (user: [email protected], size: 5039) [annotate] [blame] [check-ins using] [more...]
package horizon_test import ( "errors" "fmt" "math" "slices" "strings" "testing" "time" "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" ) func (d *dag) uuid(rid libfossil.FslID) string { d.t.Helper() var uuid string if err := d.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil { d.t.Fatal(err) } return uuid } // TestResolveName resolves names as draft-fossil-horizon-00 §6.2.7 does: tip // is the most recent check-in, a full hash names its check-in, and any other // name is the most recent check-in carrying sym-NAME; depth then steps back // along primary parents, stopping at a root. func TestResolveName(t *testing.T) { d := newDAG(t) trunk := d.chain("c1", "c2", "c3", "c4") feat := d.commit("feat", p(trunk[1]), branchTags("feat")...) m := d.commit("m", p(trunk[3], feat)) rel := d.commit("rel", p(m), deck.TagCard{Type: deck.TagSingleton, Name: "sym-v1", UUID: "*"}) side := d.commit("side", p(trunk[2]), branchTags("side")...) for _, tc := range []struct { name string depth uint32 want libfossil.FslID steps uint32 }{ {"tip", 0, side, 0}, {"trunk", 0, rel, 0}, {"trunk", 3, trunk[2], 3}, // rel -> m -> c4 -> c3: m's primary parent is c4 {"feat", 0, feat, 0}, {"v1", 1, m, 1}, {"side", 0, side, 0}, {d.uuid(trunk[3]), 2, trunk[1], 2}, {strings.ToUpper(d.uuid(trunk[3])), 0, trunk[3], 0}, {"side", 10, trunk[0], 3}, // side -> c3 -> c2 -> c1, the root } { got, err := horizon.ResolveName(d.r.DB(), tc.name, tc.depth) if err != nil { t.Fatalf("ResolveName(%.12s, %d): %v", tc.name, tc.depth, err) } if got.Hash != d.uuid(tc.want) || got.Steps != tc.steps { t.Errorf("ResolveName(%.12s, %d) = %.12s after %d steps, want %.12s after %d", tc.name, tc.depth, got.Hash, got.Steps, d.uuid(tc.want), tc.steps) } } // A file's hash names no check-in, and nothing carries sym-nope. var file string if err := d.r.DB().QueryRow( "SELECT uuid FROM blob WHERE rid NOT IN (SELECT objid FROM event) LIMIT 1").Scan(&file); err != nil { t.Fatal(err) } for _, name := range []string{"nope", file, ""} { if _, err := horizon.ResolveName(d.r.DB(), name, 0); !errors.Is(err, horizon.ErrNameNotFound) { t.Errorf("ResolveName(%q) = %v, want ErrNameNotFound", name, err) } } } // TestResolveNameCancelled: a cancelled branch tag no longer names the // check-ins below the cancellation. func TestResolveNameCancelled(t *testing.T) { d := newDAG(t) c := d.chain("c1", "c2") d.commit("x", p(c[1]), branchTags("x")...) got, err := horizon.ResolveName(d.r.DB(), "trunk", 0) if err != nil || got.Hash != d.uuid(c[1]) { t.Fatalf("trunk = %.12s, %v; want c2 (x cancels sym-trunk)", got.Hash, err) } } // TestResolveNameTie: check-ins tied on mtime resolve to the lowest full // hash, never the lowest rid, so two origins resolve a name alike. The // siblings are committed until one sorts below the first, which has the // lowest rid. func TestResolveNameTie(t *testing.T) { d := newDAG(t) root := d.commit("root", nil) when := time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC) var hashes []string for i := 0; i < 64 && (len(hashes) < 2 || slices.Min(hashes) == hashes[0]); i++ { rid, _, err := manifest.Checkin(d.r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "f.txt", Content: []byte(fmt.Sprintf("sibling %d\n", i))}}, Comment: "sibling", User: "test", Time: when, Parent: root, Tags: branchTags("x"), }) if err != nil { t.Fatal(err) } hashes = append(hashes, d.uuid(rid)) } want := slices.Min(hashes) if want == hashes[0] { t.Fatal("fixture: no sibling sorts below the lowest rid's") } for _, name := range []string{"tip", "x"} { got, err := horizon.ResolveName(d.r.DB(), name, 0) if err != nil || got.Hash != want { t.Errorf("ResolveName(%s) = %.12s, %v; want the lowest hash %.12s, not the lowest rid's %.12s", name, got.Hash, err, want, hashes[0]) } } } // TestResolveNameBounded: the walk back costs no more than the history it // walks. On a corrupt graph whose primary parents form a cycle, the deepest // depth ends within the repository's check-in count instead of circling. func TestResolveNameBounded(t *testing.T) { d := newDAG(t) c := d.chain("c1", "c2", "c3") if _, err := d.r.DB().Exec("INSERT INTO plink(pid,cid,isprim,mtime) VALUES(?,?,1,0)", c[2], c[0]); err != nil { t.Fatal(err) } done := make(chan error, 1) var got horizon.Resolved go func() { var err error got, err = horizon.ResolveName(d.r.DB(), "trunk", math.MaxUint32) done <- err }() select { case err := <-done: if err != nil { t.Fatal(err) } if got.Steps > 3 { t.Fatalf("walked %d steps over 3 check-ins", got.Steps) } case <-time.After(2 * time.Second): t.Fatal("the walk back over a 3-check-in cycle at depth 2^32-1 did not end within 2s: it is not bounded by the history") } }