Artifact 24cb59cd3bfb65a674cb1c894f56a48c3f1ebd42ac2ea9dff5fa94bb57352d34:
- File
internal/sync/receipt_realfossil_test.go
— part of check-in
[4add332299]
at
2026-10-18 20:30:31
on branch feat/submanifest-reader
— fix: record one rcvfrom row per receipt, as fossil does
Fossil records each arrival of content (a commit, a tag, a sync or clone, a request its server answers, an unversioned add) as one rcvfrom row, made by content_rcvid_init when the first content arrives, and points every blob and unversioned file it stores at that row. go-libfossil seeded a single row at creation and pointed every blob at rcvid 1.
db.Receipt is that row: it is made lazily on first use, with the time as a Julian day, and remade if the transaction that made it rolled back. A transaction records content under the receipt it is given, and repo.WithReceipt gives one to every transaction, bound ones included, of a commit, tag, sync, clone or served request. The client credits its local user and the peer's address, as fossil's client records g.zIpAddr; the server credits nobody until a login card names a user, then that user and the card's nonce, from the request's address. Phantoms keep rcvid 0, and an applied unversioned deletion keeps its row's rcvid, as in fossil.
A new repository no longer starts with a placeholder row; an existing one keeps it. (user: [email protected], size: 6349) [annotate] [blame] [check-ins using] [more...]
package sync_test import ( "context" "fmt" "net" "net/url" "os" "path/filepath" "strings" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/auth" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/testutil" ) // openRepoDB opens a repository fossil wrote, for reading its rcvfrom rows. func openRepoDB(t *testing.T, path string) *db.DB { t.Helper() d, err := db.Open(path) if err != nil { t.Fatalf("open %s: %v", path, err) } t.Cleanup(func() { d.Close() }) return d } // startFossilHTTP serves path with the fossil binary and returns its URL with // user's credentials, on 127.0.0.1 so both clients record the same address. func startFossilHTTP(t *testing.T, bin, path, user, password string) string { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } port := ln.Addr().(*net.TCPAddr).Port ln.Close() testutil.StartServer(t, bin, "server", fmt.Sprintf("--port=%d", port), path) addr := fmt.Sprintf("127.0.0.1:%d", port) deadline := time.Now().Add(10 * time.Second) for { conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond) if err == nil { conn.Close() break } if time.Now().After(deadline) { t.Fatalf("fossil server not ready on %s: %v", addr, err) } time.Sleep(50 * time.Millisecond) } return withUser("http://"+addr, user, password) } func withUser(base, user, password string) string { u, err := url.Parse(base) if err != nil { panic(err) } u.User = url.UserPassword(user, password) return u.String() } // fossilCommitFile commits name with text in a checkout of repoPath at work, // as alice. func fossilCommitFile(t *testing.T, bin, work, repoPath, name, text string) { t.Helper() if _, err := os.Stat(work); os.IsNotExist(err) { if err := os.Mkdir(work, 0o755); err != nil { t.Fatal(err) } fossilOK(t, bin, work, "settings", "autosync", "off", "-R", repoPath) fossilOK(t, bin, work, "open", "--nosync", repoPath) } if err := os.WriteFile(filepath.Join(work, name), []byte(text), 0o644); err != nil { t.Fatal(err) } fossilOK(t, bin, work, "add", name) fossilOK(t, bin, work, "commit", "-m", "add "+name, "--no-warnings", "--user", "alice") } // fossilIntegrity runs fossil's own checks on a repository go-libfossil // holds open, after moving its WAL into the file fossil sizes up. func fossilIntegrity(t *testing.T, bin, dir string, r *repo.Repo) { t.Helper() if err := r.Checkpoint(db.CheckpointTruncate); err != nil { t.Fatalf("checkpoint: %v", err) } path := r.Path() out, err := fossilRun(bin, dir, "test-integrity", "-R", path) if err != nil || !strings.Contains(out, " 0 errors") { t.Fatalf("fossil test-integrity %s: %v\n%s", path, err, out) } fossilOK(t, bin, dir, "rebuild", path) } // sameShape reports how fossil's receipt row f and go-libfossil's g differ in // what both should agree on: the user, the address, the blobs, a Julian-day // mtime, and whether there is a nonce and that it is a SHA-1. func sameShape(f, g rcvRow) bool { nonceShape := func(n string) string { if n == "NULL" { return n } if sha1Hex.MatchString(n) { return "sha1" } return "other:" + n } return f.Login == g.Login && f.IPAddr == g.IPAddr && f.Blobs == g.Blobs && f.Julian && g.Julian && nonceShape(f.Nonce) == nonceShape(g.Nonce) } func compareReceipts(t *testing.T, what string, fossil, glf []rcvRow) { t.Helper() t.Logf("%s: fossil %+v, go-libfossil %+v", what, fossil, glf) if len(fossil) != len(glf) { t.Fatalf("%s: fossil recorded %+v, go-libfossil %+v", what, fossil, glf) } for i := range fossil { if !sameShape(fossil[i], glf[i]) { t.Errorf("%s row %d: fossil %+v, go-libfossil %+v", what, i, fossil[i], glf[i]) } } } // A clone and a pull from a fossil server record the receipts fossil's own // client records for them, and a push into a go-libfossil server records the // receipt a fossil server records for the same push. Nonces and times differ // by nature; their shape must not. func TestReceiptsMatchRealFossil(t *testing.T) { bin := testutil.RequireFossilBin(t) dir := t.TempDir() ctx, cancel := context.WithCancel(context.Background()) defer cancel() origin := filepath.Join(dir, "origin.fossil") fossilOK(t, bin, dir, "new", "--admin-user", "alice", origin) fossilOK(t, bin, dir, "user", "password", "alice", "pw", "-R", origin) fossilCommitFile(t, bin, filepath.Join(dir, "w-origin"), origin, "one.txt", "one\n") originURL := startFossilHTTP(t, bin, origin, "alice", "pw") // Clone: fossil's client against go-libfossil's. fClone := filepath.Join(dir, "f-clone.fossil") fossilOK(t, bin, dir, "clone", originURL, fClone) gClone, _, err := sync.Clone(ctx, filepath.Join(dir, "g-clone.fossil"), &sync.HTTPTransport{URL: originURL}, sync.CloneOpts{User: "alice", Password: "pw"}) if err != nil { t.Fatalf("Clone: %v", err) } defer gClone.Close() compareReceipts(t, "clone", rcvRows(t, openRepoDB(t, fClone)), rcvRows(t, gClone.DB())) // Pull a new check-in. fossilCommitFile(t, bin, filepath.Join(dir, "w-origin"), origin, "two.txt", "two\n") fossilOK(t, bin, dir, "pull", originURL, "-R", fClone) if _, err := sync.Sync(ctx, gClone, &sync.HTTPTransport{URL: originURL}, sync.SyncOpts{Pull: true, User: "alice", Password: "pw"}); err != nil { t.Fatalf("pull: %v", err) } compareReceipts(t, "pull", rcvRows(t, openRepoDB(t, fClone)), rcvRows(t, gClone.DB())) fossilIntegrity(t, bin, dir, gClone) // Push: the same fossil commit into the fossil server and into the // go-libfossil clone, served, which holds what the fossil server holds. pc, err := gClone.Config("project-code") if err != nil { t.Fatal(err) } if err := auth.SetPassword(gClone.DB(), pc, "alice", "pw"); err != nil { t.Fatalf("SetPassword: %v", err) } gURL := withUser(serveRepo(ctx, t, gClone), "alice", "pw") work := filepath.Join(dir, "w-clone") fossilCommitFile(t, bin, work, fClone, "three.txt", "three\n") fossilOK(t, bin, work, "push", originURL) fossilOK(t, bin, work, "push", gURL) fRows := rcvRows(t, openRepoDB(t, origin)) gRows := rcvRows(t, gClone.DB()) compareReceipts(t, "push", fRows[len(fRows)-1:], gRows[len(gRows)-1:]) fossilIntegrity(t, bin, dir, gClone) }