Artifact ede6ff1a5a118bc9329c98566b2535e2281c2aefdf3c3cad14ed513e72d72ad1:
- File
conformance/sync_server_cases_test.go
— part of check-in
[479df656d3]
at
2026-10-13 04:56:41
on branch fix/sync-server-order
— fix(sync): handle server cards in request order, phantom gimme after push, clone bad cards
Tracked in #385.
The server verified the login card first and handled every control card before any data card. A file card ahead of its push card was stored, and cards ahead of a successful login got the login's capabilities. Cards are now handled in one in-order loop, as fossil's page_xfer reads them. A request holding an invalid clone sequence is first walked without side effects, which stands in for fossil's whole-request rollback.
A refused push without an earlier pull now resets and answers "not authorized to write"; a refused pull resets, answers "not authorized to read" and ends card processing (sync-protocol-01 Section 6.6).
An accepted push now asks for the server's phantoms after the card loop, at most 500, skipping what the client asked for, shunned names and private ones outside a private sync, as request_phantoms does. A pushed igot makes a phantom instead of drawing a gimme on the spot.
A clone reply card the client cannot read ends the reply-card loop instead of aborting: the session goes on, the second clone round is still made, and the clone then fails naming the card.
The conformance matchers that accepted "push denied" and "pull denied" are narrowed to the draft's text, and new cases put output ahead of a refusal and cards ahead of the push or login they depend on. (user: [email protected], size: 4246) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "os" "strings" "testing" ) // syncServerCase is a request and what the draft says the server does with // it. The expectation is checked against fossil(1)'s reply on every run. type syncServerCase struct { Case // setup is SQL run on the copy of F before the request. setup []string method string path string ctype string // body is the request's card lines; see syncFixture.body. body []string // cookie, when set, is "USER PASSWORD [CHANGE]": the request carries a // login cookie for that user over its body (Section 6.5). CHANGE is a // loginLine change, such as "stale" for a nonce over another body. cookie string // rawCookie, when set, is the request's Cookie header as it stands. rawCookie string want replyCheck // deviates, for a case marked Deviation, is what go-libfossil does // instead; a reply that is neither is fatal, as the mark excuses only // the deviation it names. deviates replyCheck // notSent says why the request is not sent to fossil(1) at all. notSent string // fossil, when set, says why fossil(1) does not do what the case // expects; the case fails once fossil(1) does. fossil string } func (c syncServerCase) request(fx *syncFixture) syncRequest { req := syncRequest{method: c.method, path: c.path, ctype: c.ctype, text: fx.body(c.body...)} if c.cookie != "" { f := append(strings.Fields(c.cookie), "") login := loginLine(fx.vars["PC"], f[0], f[1], f[2], string(req.text)) req.cookie = "x-f-l-c=" + strings.ReplaceAll(login, " ", "+") } if c.rawCookie != "" { req.cookie = c.rawCookie } if req.method == "" { req.method = "POST" } if req.path == "" { req.path = "/" } if req.ctype == "" { req.ctype = typeCompressed } return req } // run sends the request to fossil(1), when present, then to go-libfossil, // each on its own copy of F. fatal is a fossil(1) disagreement or a // harness failure; problem is go-libfossil's departure from the case. func (c syncServerCase) run(fx *syncFixture, ref *fossilReference, dir string) (fatal, problem error) { req := c.request(fx) if ref != nil && c.notSent == "" { path, err := prepare(fx, dir, "fossil.fossil", c.setup) if err != nil { return err, nil } reply, err := ref.serve(fx, path, req) if err != nil { return err, nil } if err := fossilAgrees(c.want(reply), c.fossil); err != nil { return err, nil } } path, err := prepare(fx, dir, "go.fossil", c.setup) if err != nil { return err, nil } reply, err := serveGo(fx, path, req) if err != nil { return nil, err } problem = c.want(reply) if problem != nil && c.deviates != nil { if err := c.deviates(reply); err != nil { return expectf("not the deviation the case names: %v", err), nil } } return nil, problem } // fossilAgrees turns fossil(1)'s verdict on a case into a fatal problem: // none when fossil does what the case expects, or when it does not and the // case says why. func fossilAgrees(problem error, differs string) error { switch { case differs == "" && problem != nil: return expectf("fossil(1) disagrees with the case: %v", problem) case differs != "" && problem == nil: return expectf("fossil(1) now does what the case expects; remove fossil: %q", differs) } return nil } func TestSyncServer(t *testing.T) { ref := newFossilReference(t) quietLogs(t) top := t.TempDir() fx := newSyncFixture(t, top) for _, c := range syncServerCases() { c.Case = markSync(c.Case) checkSync(t, c.Case, func() (error, error) { dir, err := os.MkdirTemp(top, "case") if err != nil { return err, nil } return c.run(fx, ref, dir) }) } } func syncServerCases() []syncServerCase { var all []syncServerCase for _, group := range [][]syncServerCase{ serverRoundCases(), serverTransportCases(), serverCardCases(), serverErrorCases(), serverAuthCases(), serverLoginCases(), serverContentCases(), serverUVCases(), serverUVGimmeCases(), serverCloneCases(), serverDirectionCases(), serverPragmaCases(), serverConfigCases(), serverVersionCases(), serverGateCases(), serverAdmissionCases(), serverCookieRefusalCases(), serverPragmaMoreCases(), serverRefusalCases(), serverOrderCases(), } { all = append(all, group...) } return all }