Artifact b90d71ae5aee452ce5f54d3d591aa17fb0928f2e43f4c0cf0fb62de9afd6d81f:
- File
internal/sync/ckin_lock_test.go
— part of check-in
[161d6ab68f]
at
2026-10-13 05:26:26
on branch main
— fix(sync): cover the sync-protocol-01 coverage rows (#394)
Check the remaining partial and uncovered sync rows, and fix the client private-gimme answer, the unversioned-file NAME encoding, the check-in lock table, the uv-hash name form and the uvgimme round limit they exposed. Tracked in #392. (user: [email protected], size: 8503) [annotate] [blame] [check-ins using]
package sync import ( "context" "path/filepath" "testing" "time" "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 newCkinLockTestRepo(t *testing.T) *repo.Repo { t.Helper() path := filepath.Join(t.TempDir(), "test.fossil") r, err := repo.Create(path, "test", simio.CryptoRand{}, testPC) if err != nil { t.Fatalf("repo.Create: %v", err) } t.Cleanup(func() { r.Close() }) return r } // insertLeafBlob inserts a blob and marks it as a leaf, returning the UUID. func insertLeafBlob(t *testing.T, r *repo.Repo, uuid string) { t.Helper() d := r.DB() d.Exec("INSERT INTO blob(uuid, size, content) VALUES(?, 0, X'')", uuid) var rid int64 if err := d.QueryRow("SELECT rid FROM blob WHERE uuid=?", uuid).Scan(&rid); err != nil { t.Fatalf("insertLeafBlob: %v", err) } d.Exec("INSERT INTO leaf(rid) VALUES(?)", rid) } func TestCkinLock_Acquire(t *testing.T) { r := newCkinLockTestRepo(t) uuid := "acq00000000000000000000000000000000000000" insertLeafBlob(t, r, uuid) fail := processCkinLock(r.DB(), uuid, "client-A", "alice", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("expected nil, got CkinLockFail{HeldBy: %q}", fail.HeldBy) } } func TestCkinLock_SameClient(t *testing.T) { r := newCkinLockTestRepo(t) d := r.DB() uuid := "same0000000000000000000000000000000000000" insertLeafBlob(t, r, uuid) fail := processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("first acquire failed: %+v", fail) } fail = processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("same-client re-acquire should succeed, got: %+v", fail) } } func TestCkinLock_Conflict(t *testing.T) { r := newCkinLockTestRepo(t) d := r.DB() uuid := "conflict0000000000000000000000000000000000" insertLeafBlob(t, r, uuid) fail := processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("first acquire failed: %+v", fail) } fail = processCkinLock(d, uuid, "client-B", "bob", DefaultCkinLockTimeout) if fail == nil { t.Fatal("expected CkinLockFail for different client, got nil") } if fail.HeldBy != "alice" { t.Errorf("HeldBy = %q, want %q", fail.HeldBy, "alice") } } func TestCkinLock_Expiry(t *testing.T) { r := newCkinLockTestRepo(t) d := r.DB() uuid := "exp00000000000000000000000000000000000000" insertLeafBlob(t, r, uuid) // Acquire lock for client-A. fail := processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("first acquire failed: %+v", fail) } // Backdate the lock's mtime by 120 seconds so it appears stale. key := configKey(uuid) stale := time.Now().Unix() - 120 d.Exec("UPDATE config SET mtime=? WHERE name=?", stale, key) // Different client should now succeed because the lock is expired. fail = processCkinLock(d, uuid, "client-B", "bob", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("expected expired lock to be reaped, got: %+v", fail) } } func TestCkinLock_ParentNotLeaf(t *testing.T) { r := newCkinLockTestRepo(t) d := r.DB() // Insert a blob that is NOT in the leaf table. uuid := "deadbeef00000000000000000000000000000000" d.Exec("INSERT INTO blob(uuid, size, content) VALUES(?, 0, X'')", uuid) // Acquire lock for client-A on this non-leaf blob. fail := processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("first acquire failed: %+v", fail) } // Different client should succeed because the parent is not a leaf. fail = processCkinLock(d, uuid, "client-B", "bob", DefaultCkinLockTimeout) if fail != nil { t.Fatalf("expected non-leaf parent lock to be cleaned, got: %+v", fail) } } func TestCkinLock_SyncRoundTrip(t *testing.T) { serverRepo := newCkinLockTestRepo(t) // Insert a leaf blob on the server so the lock doesn't get cleaned // up by expireStaleLocks (which removes locks for non-leaf parents). parentUUID := "abcd0000000000000000000000000000000000ab" insertLeafBlob(t, serverRepo, parentUUID) // Only a requester who may commit takes a lock. grantNobodyPush(t, serverRepo) transport := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { resp, _ := HandleSync(context.Background(), serverRepo, req) return resp }, } // The client holds the parent it locks, as a client committing on top // of it does. The server's copy has no content to send, and the error a // gimme for it would draw ends the reply before the ci-lock-fail that // follows it in card order. clientRepo := newCkinLockTestRepo(t) insertLeafBlob(t, clientRepo, parentUUID) result, err := Sync(context.Background(), clientRepo, transport, SyncOpts{ Pull: true, CkinLock: &CkinLockReq{ ParentUUID: parentUUID, ClientID: "client-1", }, }) if err != nil { t.Fatalf("Sync: %v", err) } if result.CkinLockFail != nil { t.Fatalf("unexpected lock fail: %+v", result.CkinLockFail) } // Second client should get lock-fail. result2, err := Sync(context.Background(), clientRepo, transport, SyncOpts{ Pull: true, CkinLock: &CkinLockReq{ ParentUUID: parentUUID, ClientID: "client-2", }, }) if err != nil { t.Fatalf("Sync 2: %v", err) } if result2.CkinLockFail == nil { t.Fatal("expected lock fail for second client") } // Default user with no login card is "nobody" if result2.CkinLockFail.HeldBy != "nobody" { t.Fatalf("HeldBy = %q, want %q", result2.CkinLockFail.HeldBy, "nobody") } } // TestCkinLock_SharesFossilTable pins the lock table to fossil's: a row // fossil's ci-lock wrote (config ci-lock-HASH, JSON login and clientid, the // time in the mtime column) is the lock this server sees, and a live row is // kept as it is, with no refresh, even for the client holding it (ยง9.4). func TestCkinLock_SharesFossilTable(t *testing.T) { r := newCkinLockTestRepo(t) d := r.DB() uuid := "f0551100000000000000000000000000000000ab" insertLeafBlob(t, r, uuid) since := time.Now().Unix() - 30 if _, err := d.Exec("INSERT INTO config(name, value, mtime) VALUES(?,"+ " json_object('login', 'bob', 'clientid', 'client-A'), ?)", "ci-lock-"+uuid, since); err != nil { t.Fatal(err) } fail := processCkinLock(d, uuid, "client-B", "alice", DefaultCkinLockTimeout) if fail == nil || fail.HeldBy != "bob" || fail.Since.Unix() != since { t.Fatalf("fossil's lock: got %+v, want held by bob since %d", fail, since) } if fail := processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout); fail != nil { t.Fatalf("same client: got %+v, want no failure", fail) } var login string var mtime int64 if err := d.QueryRow("SELECT json_extract(value, '$.login'), CAST(mtime AS INTEGER)"+ " FROM config WHERE name=?", "ci-lock-"+uuid).Scan(&login, &mtime); err != nil { t.Fatal(err) } if login != "bob" || mtime != since { t.Fatalf("lock now held by %s since %d, want bob since %d", login, mtime, since) } } // TestCkinLock_DropsLegacyRows checks that lock rows under the old // libfossil-ci-lock- key, which no sweep matches any longer, are cleared. func TestCkinLock_DropsLegacyRows(t *testing.T) { r := newCkinLockTestRepo(t) d := r.DB() d.Exec("INSERT INTO config(name, value, mtime) VALUES(?, ?, ?)", "libfossil-ci-lock-aaaa", "bob|client-B", time.Now().Unix()) uuid := "leg00000000000000000000000000000000000000" insertLeafBlob(t, r, uuid) if fail := processCkinLock(d, uuid, "client-A", "alice", DefaultCkinLockTimeout); fail != nil { t.Fatalf("unexpected conflict: %+v", fail) } var n int d.QueryRow("SELECT count(*) FROM config WHERE name GLOB 'libfossil-ci-lock-*'").Scan(&n) if n != 0 { t.Fatalf("%d legacy lock rows remain", n) } } // TestLockTimeout_ReadsConfig checks that the lock-timeout setting, which // fossil honors, sets how long a lock lives, and that a bad value does not. func TestLockTimeout_ReadsConfig(t *testing.T) { d := newCkinLockTestRepo(t).DB() if got := lockTimeout(d); got != DefaultCkinLockTimeout { t.Fatalf("unset: %v", got) } for v, want := range map[string]time.Duration{ "300": 300 * time.Second, "0": DefaultCkinLockTimeout, "-5": DefaultCkinLockTimeout, "soon": DefaultCkinLockTimeout, } { d.Exec("REPLACE INTO config(name, value, mtime) VALUES('lock-timeout', ?, 1)", v) if got := lockTimeout(d); got != want { t.Errorf("lock-timeout %q: %v, want %v", v, got, want) } } }