Artifact 2be756e4dab08e3ad531f564f1a62b411ebbbdb1f1d64f113d29a19194029858:
- File
internal/sync/sync_test.go
— part of check-in
[821cf115ce]
at
2026-10-12 22:48:22
on branch main
— fix(sync): refuse push and pull cards naming another project (#309) (#353)
fossil's page_xfer compares a push or pull card's PROJECTCODE with the repository's project-code before any capability check and, on a mismatch, resets the reply, answers "error wrong project" and breaks out of the card loop (section 8.5). The handler accepted either card whatever its project code.
The refusal goes through a new breakLoop, which the undecodable file card path now shares: the reply is reset to the one error, no later card is processed, and the post-loop work and timestamp still follow. Tests that synced repositories created with different project codes now create them with a shared one, and the DST simulator gives its leaves the upstream's. (user: [email protected], size: 22918) [annotate] [blame] [check-ins using]
package sync import ( "bytes" "context" "crypto/sha1" "encoding/hex" "fmt" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/internal/auth" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/delta" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/repo" _ "github.com/danmestas/go-libfossil/internal/testdriver" "github.com/danmestas/go-libfossil/internal/xfer" "github.com/danmestas/go-libfossil/simio" ) func TestMockTransportExchange(t *testing.T) { mt := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { return &xfer.Message{Cards: []xfer.Card{ &xfer.IGotCard{UUID: "response-uuid"}, }} }, } resp, err := mt.Exchange(context.Background(), &xfer.Message{}) if err != nil { t.Fatalf("Exchange: %v", err) } if len(resp.Cards) != 1 { t.Fatalf("cards = %d, want 1", len(resp.Cards)) } } func TestMockTransportNilHandler(t *testing.T) { mt := &MockTransport{} resp, err := mt.Exchange(context.Background(), &xfer.Message{}) if err != nil { t.Fatalf("Exchange: %v", err) } if len(resp.Cards) != 0 { t.Fatalf("expected empty response") } } func sha1hex(data string) string { h := sha1.Sum([]byte(data)) return hex.EncodeToString(h[:]) } func TestComputeLogin(t *testing.T) { payload := []byte("pull abc def\nigot da39a3ee5e6b4b0d3255bfef95601890afd80709\n") card := computeLogin("testuser", "secret", "projcode", payload) if card.User != "testuser" { t.Fatalf("User = %q", card.User) } expectedNonce := sha1hex(string(payload)) if card.Nonce != expectedNonce { t.Fatalf("Nonce = %q, want %q", card.Nonce, expectedNonce) } secret := sha1hex("projcode/testuser/secret") expectedSig := sha1hex(card.Nonce + secret) if card.Signature != expectedSig { t.Fatalf("Signature = %q, want %q", card.Signature, expectedSig) } } // TestComputeLoginMatchesCapturedFossilCard replays a login card fossil 2.28 // actually put on the wire and requires us to reproduce it byte for byte. // // The payload is the request body that followed that login card, captured with // `fossil clone --httptrace` against a server whose project code and user // password are the constants below. Two things about it are load-bearing and // were both wrong before #203: the trailing "# ..." comment is part of the // hashed payload and so must be transmitted, not merely hashed; and the // signature covers the *hashed* password, salted with the project code. func TestComputeLoginMatchesCapturedFossilCard(t *testing.T) { const ( projectCode = "2d5573080bf03f15ed58105794cc80c6a51f4c52" user = "syncer" password = "pw123" wantNonce = "0a1023892bbe47501cfd1e1a54fd0bc1e403eed7" wantSig = "7e93a6c9f1a38d9a00f08b74248adf6a6b86497a" ) payload := []byte("pragma client-version 22800 20260311 113146\n" + "clone 3 1\n" + "reqconfig /all\n" + "# EAFD8046BFE8B4CF96EC2C919144A9B13DC026BB\n") card := computeLogin(user, password, projectCode, payload) if card.Nonce != wantNonce { t.Errorf("Nonce = %q, want %q (fossil's own card for this payload)", card.Nonce, wantNonce) } if card.Signature != wantSig { t.Errorf("Signature = %q, want %q (fossil's own card for these credentials)", card.Signature, wantSig) } } // TestLoginNonceCoversTheTransmittedBody is the §6.2 conformance test: the // nonce is "SHA-1 over every decompressed body byte after the body login // line's LF through body end". It recomputes the nonce from the message the // client is actually about to send, rather than from whatever the client chose // to hash. // // That distinction is the #203 defect. The random comment supplying the nonce // its uniqueness was appended to the hashed payload but never added to the // message, so the covered range and the transmitted range differed by one // line. The server, which recomputes the nonce from the bytes it received, // rejected every login before comparing a password — making a correct password // indistinguishable from a wrong one. func TestLoginNonceCoversTheTransmittedBody(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, User: "syncer", Password: "pw123"}) msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } login, ok := msg.Cards[0].(*xfer.LoginCard) if !ok { t.Fatalf("first card is %T, want *xfer.LoginCard — the login card leads the body", msg.Cards[0]) } body, err := encodeCards(msg.Cards) if err != nil { t.Fatalf("encodeCards: %v", err) } lf := bytes.IndexByte(body, '\n') if lf < 0 { t.Fatal("encoded body has no LF") } tail := body[lf+1:] if got := sha1hex(string(tail)); got != login.Nonce { t.Errorf("nonce = %q, but SHA-1 of the body after the login line's LF is %q;\n"+ "the client signed bytes it is not sending:\n%s", login.Nonce, got, tail) } // The tail must end with the nonce comment — that is where the randomness // lives, and it only varies the nonce if it is actually transmitted. if !bytes.HasSuffix(tail, []byte("\n")) || !bytes.Contains(tail, []byte("\n# ")) { t.Errorf("body after the login line carries no nonce comment:\n%s", tail) } } func TestComputeLoginAnonymous(t *testing.T) { card := computeLogin("anonymous", "", "projcode", []byte("test\n")) if card.User != "anonymous" { t.Fatalf("User = %q", card.User) } if card.Nonce == "" { t.Fatal("anonymous should still have nonce") } } func TestRandomComment(t *testing.T) { rng := simio.CryptoRand{} if randomComment(rng).Text == randomComment(rng).Text { t.Fatal("random comments should be unique") } // The comment is what makes each nonce unique, so it has to reach the // wire — a login card the server can validate hashes bytes it received. var buf bytes.Buffer if err := xfer.EncodeCard(&buf, randomComment(rng)); err != nil { t.Fatalf("EncodeCard: %v", err) } line := buf.String() if !strings.HasPrefix(line, "# ") || !strings.HasSuffix(line, "\n") { t.Fatalf("comment encoded as %q, want a \"# ...\\n\" line", line) } } // --- Test helpers --- // testPC is the project code test repositories are created with. A server // refuses a push or pull card naming another project (§8.5), so the // repositories a test syncs share it, as a clone shares its origin's. const testPC = "77dc1a9065e8b8ff0d35abe4e26498cdf9576d6a" func setupSyncTestRepo(t *testing.T) *repo.Repo { t.Helper() path := filepath.Join(t.TempDir(), "test.fossil") r, err := repo.Create(path, "testuser", simio.CryptoRand{}, testPC) if err != nil { t.Fatalf("repo.Create: %v", err) } t.Cleanup(func() { r.Close() }) return r } // grantNobodyPush lets unauthenticated clients push artifacts (i) and // unversioned files (y) to r. A new repository grants nobody only fossil's // "gjorz", so a test that pushes without a login card has to grant this. func grantNobodyPush(t *testing.T, r *repo.Repo) { t.Helper() if err := auth.UpdateCaps(r.DB(), "nobody", "gijoryz"); err != nil { t.Fatalf("grant nobody push: %v", err) } } // setupPushableTestRepo is setupSyncTestRepo for a server that accepts // unauthenticated pushes. func setupPushableTestRepo(t *testing.T) *repo.Repo { t.Helper() r := setupSyncTestRepo(t) grantNobodyPush(t, r) return r } func newTestSession(t *testing.T, opts SyncOpts) (*session, *repo.Repo) { t.Helper() r := setupSyncTestRepo(t) return newSession(r, opts), r } // --- buildRequest tests --- func TestBuildRequestPushOnly(t *testing.T) { s, _ := newTestSession(t, SyncOpts{ Push: true, Pull: false, ServerCode: "sc", ProjectCode: testPC, }) msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } pushCards := cardsByType(msg, xfer.CardPush) pullCards := cardsByType(msg, xfer.CardPull) if len(pushCards) != 1 { t.Fatalf("push cards = %d, want 1", len(pushCards)) } if len(pullCards) != 0 { t.Fatalf("pull cards = %d, want 0", len(pullCards)) } } func TestBuildRequestPullOnly(t *testing.T) { s, _ := newTestSession(t, SyncOpts{ Push: false, Pull: true, ServerCode: "sc", ProjectCode: testPC, }) msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } pushCards := cardsByType(msg, xfer.CardPush) pullCards := cardsByType(msg, xfer.CardPull) if len(pushCards) != 0 { t.Fatalf("push cards = %d, want 0", len(pushCards)) } if len(pullCards) != 1 { t.Fatalf("pull cards = %d, want 1", len(pullCards)) } } func TestBuildRequestHasPragmaClientVersion(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } pragmas := cardsByType(msg, xfer.CardPragma) if len(pragmas) == 0 { t.Fatal("no pragma cards found") } p := pragmas[0].(*xfer.PragmaCard) if p.Name != "client-version" { t.Fatalf("pragma name = %q, want client-version", p.Name) } } func TestBuildRequestIGotFromUnclustered(t *testing.T) { s, r := newTestSession(t, SyncOpts{Push: true, ServerCode: "sc", ProjectCode: testPC}) // Store a blob — blob.Store auto-marks unclustered. content := []byte("test artifact for igot") _, uuid, err := blob.Store(r.DB(), content) if err != nil { t.Fatalf("blob.Store: %v", err) } msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } igots := cardsByType(msg, xfer.CardIGot) if len(igots) == 0 { t.Fatal("expected igot cards from unclustered") } found := false for _, c := range igots { if c.(*xfer.IGotCard).UUID == uuid { found = true } } if !found { t.Fatalf("igot card for uuid %s not found", uuid) } } func TestBuildRequestGimmeForPhantoms(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) phantomUUID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" s.phantoms[phantomUUID] = true msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } gimmes := cardsByType(msg, xfer.CardGimme) if len(gimmes) != 1 { t.Fatalf("gimme cards = %d, want 1", len(gimmes)) } if gimmes[0].(*xfer.GimmeCard).UUID != phantomUUID { t.Fatalf("gimme uuid = %q, want %q", gimmes[0].(*xfer.GimmeCard).UUID, phantomUUID) } } func TestBuildRequestFileForPendingSend(t *testing.T) { s, r := newTestSession(t, SyncOpts{Push: true, ServerCode: "sc", ProjectCode: testPC}) // Store a blob and add its UUID to pendingSend content := []byte("file to send over sync") _, uuid, err := blob.Store(r.DB(), content) if err != nil { t.Fatalf("blob.Store: %v", err) } s.pendingSend[uuid] = true msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } files := cardsByType(msg, xfer.CardFile) if len(files) != 1 { t.Fatalf("file cards = %d, want 1", len(files)) } fc := files[0].(*xfer.FileCard) if fc.UUID != uuid { t.Fatalf("file uuid = %q, want %q", fc.UUID, uuid) } if string(fc.Content) != string(content) { t.Fatalf("file content mismatch") } } func TestBuildRequestWithLogin(t *testing.T) { s, _ := newTestSession(t, SyncOpts{ Pull: true, ServerCode: "sc", ProjectCode: testPC, User: "alice", Password: "pass123", }) msg, err := s.buildRequest(0) if err != nil { t.Fatalf("buildRequest: %v", err) } // Login card should be first if len(msg.Cards) == 0 { t.Fatal("no cards") } login, ok := msg.Cards[0].(*xfer.LoginCard) if !ok { t.Fatalf("first card is %T, want *LoginCard", msg.Cards[0]) } if login.User != "alice" { t.Fatalf("login.User = %q, want alice", login.User) } if login.Nonce == "" { t.Fatal("login.Nonce is empty") } if login.Signature == "" { t.Fatal("login.Signature is empty") } } // --- processResponse tests --- func TestProcessResponseStoresFileCard(t *testing.T) { s, r := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) content := []byte("artifact from server") uuid := hash.SHA1(content) resp := &xfer.Message{Cards: []xfer.Card{ &xfer.FileCard{UUID: uuid, Content: content}, }} done, err := s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if done { t.Fatal("should not be done (just received a file)") } // Verify blob was stored rid, ok := blob.Exists(r.DB(), uuid) if !ok { t.Fatalf("blob %s not found after processResponse", uuid) } if rid <= 0 { t.Fatalf("rid = %d, want > 0", rid) } if s.result.FilesRecvd != 1 { t.Fatalf("FilesRecvd = %d, want 1", s.result.FilesRecvd) } } // TestProcessResponseDeltaBeforeBaseDoesNotError is the sync-pull acceptance // test for issue #57 (folded into #53): a delta arriving before its base is // normal transfer steady state, not a fault. processResponse must succeed // and (via storeReceivedFile's loadDBPhantoms call) discover the newly // phantomized base so it gets gimme'd on a later round, rather than // failing the pull outright. func TestProcessResponseDeltaBeforeBaseDoesNotError(t *testing.T) { s, r := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) base := []byte("pull delta-before-base test, content long enough to compress well") target := []byte("pull delta-before-base test, content long enough to compress well, v2") deltaBytes, err := delta.Create(base, target) if err != nil { t.Fatalf("delta.Create: %v", err) } baseUUID := hash.SHA1(base) targetUUID := hash.SHA1(target) resp := &xfer.Message{Cards: []xfer.Card{ &xfer.CFileCard{UUID: targetUUID, DeltaSrc: baseUUID, Content: deltaBytes}, }} if _, err := s.processResponse(context.Background(), resp); err != nil { t.Fatalf("processResponse(delta before base) = %v, want nil", err) } targetRid, ok := blob.Exists(r.DB(), targetUUID) if !ok { t.Fatal("target blob missing after processResponse") } var size int64 r.DB().QueryRow("SELECT size FROM blob WHERE rid=?", targetRid).Scan(&size) if size < 0 { t.Fatalf("target size = %d, want >= 0 (must not be phantomized)", size) } if _, available := content.AvailableByUUID(r.DB(), targetUUID); available { t.Fatal("target reported available before its base ever arrived") } // loadDBPhantoms (already called unconditionally from handleFileCard) // must have discovered the newly phantomized base and queued it for a // gimme card on a later round. if !s.phantoms[baseUUID] { t.Fatal("base UUID not queued for re-request after being phantomized") } } func TestProcessResponseIGotAddsPhantom(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) missingUUID := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" resp := &xfer.Message{Cards: []xfer.Card{ &xfer.IGotCard{UUID: missingUUID}, }} _, err := s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if !s.remoteHas[missingUUID] { t.Fatal("remoteHas should contain the UUID") } if !s.phantoms[missingUUID] { t.Fatal("phantoms should contain the UUID (pull=true, blob missing)") } } func TestProcessResponseIGotNoPhantomWhenExists(t *testing.T) { s, r := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) // Store a blob so it exists locally content := []byte("already have this") _, uuid, err := blob.Store(r.DB(), content) if err != nil { t.Fatalf("blob.Store: %v", err) } resp := &xfer.Message{Cards: []xfer.Card{ &xfer.IGotCard{UUID: uuid}, }} _, err = s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if !s.remoteHas[uuid] { t.Fatal("remoteHas should contain the UUID") } if s.phantoms[uuid] { t.Fatal("phantoms should NOT contain the UUID (we already have it)") } } func TestProcessResponseGimmeAddsPendingSend(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Push: true, ServerCode: "sc", ProjectCode: testPC}) wantUUID := "cccccccccccccccccccccccccccccccccccccccc" resp := &xfer.Message{Cards: []xfer.Card{ &xfer.GimmeCard{UUID: wantUUID}, }} done, err := s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if done { t.Fatal("should not be done (gimme received)") } if !s.pendingSend[wantUUID] { t.Fatal("pendingSend should contain the UUID") } } func TestProcessResponseCookieCached(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) resp := &xfer.Message{Cards: []xfer.Card{ &xfer.CookieCard{Value: "session-abc-123"}, }} _, err := s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if s.cookie != "session-abc-123" { t.Fatalf("cookie = %q, want session-abc-123", s.cookie) } } func TestProcessResponseConvergence(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) // Empty response with no pending work = converged resp := &xfer.Message{} done, err := s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if !done { t.Fatal("should be done (empty response, no pending work)") } } func TestProcessResponseErrorCard(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true, ServerCode: "sc", ProjectCode: testPC}) resp := &xfer.Message{Cards: []xfer.Card{ &xfer.ErrorCard{Message: "access denied"}, }} done, err := s.processResponse(context.Background(), resp) if err != nil { t.Fatalf("processResponse: %v", err) } if !done { t.Fatal("should be done (only error, no work to do)") } if len(s.result.Errors) != 1 { t.Fatalf("Errors = %d, want 1", len(s.result.Errors)) } if s.result.Errors[0] != "error: access denied" { t.Fatalf("error = %q", s.result.Errors[0]) } } // --- Sync convergence loop tests --- func TestSyncSingleRound(t *testing.T) { r := setupSyncTestRepo(t) mt := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { return &xfer.Message{} // empty = converge }, } result, err := Sync(context.Background(), r, mt, SyncOpts{ Pull: true, ServerCode: "sc", ProjectCode: testPC, }) if err != nil { t.Fatalf("Sync: %v", err) } if result.Rounds != 1 { t.Fatalf("Rounds = %d, want 1", result.Rounds) } } func TestSyncMultiRound(t *testing.T) { r := setupSyncTestRepo(t) // Content for the file we'll receive in round 2 fileContent := []byte("artifact from multi-round sync") fileUUID := hash.SHA1(fileContent) round := 0 mt := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { round++ switch round { case 1: // Round 1: server says "igot" for a blob we don't have return &xfer.Message{Cards: []xfer.Card{ &xfer.IGotCard{UUID: fileUUID}, }} case 2: // Round 2: server sends the file we asked for return &xfer.Message{Cards: []xfer.Card{ &xfer.FileCard{UUID: fileUUID, Content: fileContent}, }} default: // Round 3+: empty = converge return &xfer.Message{} } }, } result, err := Sync(context.Background(), r, mt, SyncOpts{ Pull: true, ServerCode: "sc", ProjectCode: testPC, }) if err != nil { t.Fatalf("Sync: %v", err) } if result.Rounds != 3 { t.Fatalf("Rounds = %d, want 3", result.Rounds) } if result.FilesRecvd != 1 { t.Fatalf("FilesRecvd = %d, want 1", result.FilesRecvd) } // Verify the blob ended up in the repo _, ok := blob.Exists(r.DB(), fileUUID) if !ok { t.Fatal("file not stored after multi-round sync") } } func TestSyncContextCancellation(t *testing.T) { r := setupSyncTestRepo(t) ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel immediately mt := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { return &xfer.Message{} }, } _, err := Sync(ctx, r, mt, SyncOpts{ Pull: true, ServerCode: "sc", ProjectCode: testPC, }) if err == nil { t.Fatal("expected error from cancelled context") } if err != context.Canceled { t.Fatalf("error = %v, want context.Canceled", err) } } func TestSyncMaxRoundsExceeded(t *testing.T) { r := setupSyncTestRepo(t) // Mock that never converges: always returns a gimme card mt := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { return &xfer.Message{Cards: []xfer.Card{ &xfer.GimmeCard{UUID: "dddddddddddddddddddddddddddddddddddddddd"}, }} }, } result, err := Sync(context.Background(), r, mt, SyncOpts{ Push: true, ServerCode: "sc", ProjectCode: testPC, }) if err == nil { t.Fatal("expected error for max rounds exceeded") } if result.Rounds != MaxRounds { t.Fatalf("Rounds = %d, want %d", result.Rounds, MaxRounds) } } // --- Benchmarks --- // BenchmarkBuildRequest benchmarks buildRequest with 100 unclustered artifacts. func BenchmarkBuildRequest(b *testing.B) { path := filepath.Join(b.TempDir(), "bench.fossil") r, err := repo.Create(path, "benchuser", simio.CryptoRand{}, testPC) if err != nil { b.Fatalf("repo.Create: %v", err) } defer r.Close() // Populate 100 unclustered artifacts for i := 0; i < 100; i++ { content := []byte(fmt.Sprintf("benchmark artifact %d", i)) _, _, err := blob.Store(r.DB(), content) if err != nil { b.Fatalf("blob.Store: %v", err) } } s := newSession(r, SyncOpts{ Push: true, Pull: true, ServerCode: "sc", ProjectCode: testPC, User: "alice", Password: "secret", }) b.ResetTimer() for i := 0; i < b.N; i++ { _, err := s.buildRequest(0) if err != nil { b.Fatalf("buildRequest: %v", err) } } } // BenchmarkProcessResponse benchmarks processResponse with 50 file cards. func BenchmarkProcessResponse(b *testing.B) { path := filepath.Join(b.TempDir(), "bench.fossil") r, err := repo.Create(path, "benchuser", simio.CryptoRand{}, testPC) if err != nil { b.Fatalf("repo.Create: %v", err) } defer r.Close() // Build a message with 50 file cards var cards []xfer.Card for i := 0; i < 50; i++ { content := []byte(fmt.Sprintf("benchmark file content %d", i)) uuid := hash.SHA1(content) cards = append(cards, &xfer.FileCard{UUID: uuid, Content: content}) } msg := &xfer.Message{Cards: cards} b.ResetTimer() for i := 0; i < b.N; i++ { b.StopTimer() // Each iteration needs a fresh session+repo because processResponse stores blobs bpath := filepath.Join(b.TempDir(), fmt.Sprintf("bench-%d.fossil", i)) br, err := repo.Create(bpath, "benchuser", simio.CryptoRand{}, testPC) if err != nil { b.Fatalf("repo.Create: %v", err) } s := newSession(br, SyncOpts{ Pull: true, ServerCode: "sc", ProjectCode: testPC, }) b.StartTimer() _, err = s.processResponse(context.Background(), msg) if err != nil { b.Fatalf("processResponse: %v", err) } b.StopTimer() br.Close() } }