Artifact c73096a98c6811c2ce696668448bbd96c5590843a47f116664c2c52eb7dea36a:
- File
conformance/sync_client_clone_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: 5472) [annotate] [blame] [check-ins using]
package conformance import "slices" // cloneReply is a clone reply delivering artifacts by placeholder, each as // a cfile padded as Section 5.1 says, then the cards after them. func cloneReply(keys []string, after ...string) []string { var cards []string for _, k := range keys { cards = append(cards, "cfile {"+k+"} {"+k+".size} {"+k+".zsize}", "{"+k+".z}", "{"+k+".pad}") } return reply(append(cards, after...)...) } // noOperation checks request i carries no clone, pull or push card. func noOperation(i int) clientCheck { return every(notSent(i, "clone "), notSent(i, "pull "), notSent(i, "push ")) } // clientCloneCases are Sections 3.7(e), 8 and 8.2 on the client. func clientCloneCases() []syncClientCase { return []syncClientCase{{ Case: Case{ID: "sync.8.first-request", Req: "SP-8-1"}, op: clientOp{clone: true, login: true}, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno 0", pushCard)}, want: func(r *clientRun) error { req, err := r.request(1) if err != nil { return err } got := lines(req.cards) if len(got) != 3 || !slices.Contains(got, "clone 3 1") || index(req.cards, "pragma client-version ") != 0 || !nonceComment.MatchString(got[2]) { return expectf("%s: first request %q, want client-version, clone 3 1, randomness", r.who, got) } return nil }, fc: true, }, { // Neither reqconfig nor uv-hash goes in round 0. fossil(1)'s clone // asks for configuration in the next round; go-libfossil's Clone // asks for neither in any round, having no option for either. Case: Case{ID: "sync.8.4.round-0-defers-config", Req: "SP-8.4-1"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno 0", pushCard)}, want: every(atLeast(2), notSent(1, "reqconfig "), notSent(1, "pragma uv-hash ")), fc: true, }, { Case: Case{ID: "sync.8.2.clone-advancing-cursor", Req: "SP-8.2-2"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno 2", pushCard), cloneReply([]string{"N3"}, "clone_seqno 3", pushCard), cloneReply([]string{"S"}, "clone_seqno 0", pushCard)}, want: every(sent(2, "clone 3 2"), sent(3, "clone 3 3"), noOperation(4)), fc: true, }, { // A NEXT that is not decimal leaves the sequence at 1, and the // push after it is still read. Case: Case{ID: "sync.8.2.nondecimal-next", Req: "SP-8.2-2"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno x7", pushCard)}, want: sent(2, "clone 3 1"), fc: true, }, { Case: Case{ID: "sync.8.2.clone-missing-push", Req: "SP-8.2-4"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno 2", pushCard), cloneReply([]string{"N3"}, "clone_seqno 3")}, want: every(sent(2, "clone 3 2"), noOperation(3), rounds(3)), fc: true, }, { Case: Case{ID: "sync.8.2.clone-zero-cleanup", Req: "SP-8.2-5"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno 0", pushCard)}, want: every(rounds(2), noOperation(2)), fc: true, }, { // T arrives as a delta against S, which does not: the cleanup may // ask for S, but no pull follows it. go-libfossil's Clone pulls S // in a session of its own, which the third reply answers. Case: Case{ID: "sync.8.2.clone-zero-phantom", Req: "SP-8.2-5"}, op: opClone, replies: [][]string{reply("cfile {T} {S} {T.size} {DTS.zsize}", "{DTS.z}", "{DTS.pad}", "clone_seqno 0", pushCard), reply(), reply("file {S} {S.size}", "{S.data}")}, want: every(rounds(2), noOperation(2)), deviates: every(noFailure, noOperation(2), sentPrefix(3, "pull "), sent(3, "gimme {S}"), notSent(3, "clone "), notSent(3, "push "), expands("T")), fc: true, }} } // clientCloneStallCases are Section 3.7(e)'s stop for a clone that does // not advance. func clientCloneStallCases() []syncClientCase { return []syncClientCase{{ // The second reply stores nothing and repeats the sequence: a // positive sequence alone does not continue. Case: Case{ID: "sync.8.2.clone-stalled-cursor", Req: "SP-3.7-5"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "clone_seqno 2", pushCard), reply("clone_seqno 2", pushCard)}, want: rounds(2), fc: true, }} } // clientCloneErrorCases are Sections 8.3 and 10.1: errors in a clone. func clientCloneErrorCases() []syncClientCase { return []syncClientCase{{ // No push came, so the forced second round is the cleanup. Case: Case{ID: "sync.8.3.clone-first-error-only", Req: "SP-8.3-1"}, op: opClone, replies: [][]string{reply(`error not\sauthorized\sto\sclone`)}, want: every(rounds(2), noOperation(2)), fc: true, }, { Case: Case{ID: "sync.8.3.clone-first-error-content", Req: "SP-8.3-1"}, op: opClone, replies: [][]string{cloneReply([]string{"N1"}, "error x", "cfile {N3} {N3.size} {N3.zsize}", "{N3.z}", "{N3.pad}", "clone_seqno 3", pushCard)}, want: sent(2, "clone 3 3"), fc: true, }, { // After the first round an error ends the reply: N3 and the push // after it are not read, but N1 was stored, so a cleanup follows. Case: Case{ID: "sync.8.3.later-error", Req: "SP-10.1-1"}, op: opClone, replies: [][]string{cloneReply([]string{"S"}, "clone_seqno 2", pushCard), cloneReply([]string{"N1"}, "error boom", "cfile {N3} {N3.size} {N3.zsize}", "{N3.z}", "{N3.pad}", "clone_seqno 3", pushCard)}, want: every(rounds(3), noOperation(3)), fc: true, }} }