Artifact 35c24df26fb13f86f541795e2c839aa597b8e0faf57778d4a31b8889a6899797:
- File
internal/sync/gimme_cap_test.go
— part of check-in
[82dae665dd]
at
2026-10-24 00:57:36
on branch main
— fix(sync): ask for phantoms in hash order
buildGimmeCards ranged over the phantoms map, so each round's gimme cards came out in Go's randomised map order. When a round has more phantoms than mxPhantomReq allows, that also picked a random subset to ask for. A seeded simulation then could not replay the same wire, and the docs generator's engine.json (which records a pull's cards) changed between runs, failing the generated-docs check about one run in three.
Sort the phantom names before building the cards. Phantom sets are bounded by what the peer announced, so the sort is cheap next to the round trip. (user: [email protected], size: 2635) [annotate] [blame] [check-ins using] [more...]
package sync import ( "fmt" "testing" "github.com/danmestas/go-libfossil/internal/xfer" ) // addDBPhantom makes a phantom row so converged's "phantoms remain" check // sees one. func addDBPhantom(t *testing.T, s *session) { t.Helper() db := s.repo.DB() res, err := db.Exec("INSERT INTO blob(rcvid,size,uuid,content) VALUES(0,-1,?,NULL)", "dddddddddddddddddddddddddddddddddddddddd") if err != nil { t.Fatalf("phantom blob: %v", err) } rid, _ := res.LastInsertId() if _, err := db.Exec("INSERT INTO phantom VALUES(?)", rid); err != nil { t.Fatalf("phantom row: %v", err) } } func fillPhantoms(s *session, n int) { for i := 0; i < n; i++ { s.phantoms[fmt.Sprintf("%040x", i+1)] = true } } // TestGimmeCapFollowsFossilMxPhantomReq pins fossil's per-round phantom // request limit (src/xfer.c mxPhantomReq): 200 at first, then // max(200, 2*files received) after a round that continues for its phantoms, // and left as it was by a round that does not (#350). func TestGimmeCapFollowsFossilMxPhantomReq(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true}) fillPhantoms(s, 1000) if got := len(s.buildGimmeCards()); got != 200 { t.Fatalf("first round: %d gimmes, want 200", got) } addDBPhantom(t, s) if _, err := s.converged(replyTally{filesRecvd: 150}); err != nil { t.Fatal(err) } if got := len(s.buildGimmeCards()); got != 300 { t.Fatalf("after 150 files: %d gimmes, want 300", got) } // A round that continues for another reason leaves the limit alone, // as fossil only assigns mxPhantomReq in its phantom branch. if _, err := s.converged(replyTally{filesSent: 1}); err != nil { t.Fatal(err) } if got := len(s.buildGimmeCards()); got != 300 { t.Fatalf("after a no-file round: %d gimmes, want 300", got) } // Few files never lower it below the base. if _, err := s.converged(replyTally{filesRecvd: 10}); err != nil { t.Fatal(err) } if got := len(s.buildGimmeCards()); got != 200 { t.Fatalf("after 10 files: %d gimmes, want 200", got) } } // TestGimmeOrderIsDeterministic pins that a round asks for its phantoms in // hash order, so a capped round always picks the same ones and a seeded // simulation replays the same wire. Map iteration order would not. func TestGimmeOrderIsDeterministic(t *testing.T) { s, _ := newTestSession(t, SyncOpts{Pull: true}) fillPhantoms(s, 1000) cards := s.buildGimmeCards() if len(cards) != 200 { t.Fatalf("%d gimmes, want 200", len(cards)) } for i, c := range cards { want := fmt.Sprintf("%040x", i+1) if got := c.(*xfer.GimmeCard).UUID; got != want { t.Fatalf("gimme %d asks for %s, want %s", i, got, want) } } }