Artifact 1fa4e244888991890584ed446481495dee8d39a62e8528fdfe12f8e8afb3300c:
- File
conformance/sync_interop_test.go
— part of check-in
[80c72b27c7]
at
2026-10-11 13:57:34
on branch main
— test(conformance): sync-protocol conformance suite (#262) (#311)
* test(conformance): sync-protocol conformance suite (#262)
Checks go-libfossil against draft-fossil-sync-protocol-00, requirement by requirement: 149 requirements, 199 cases. Server cases send raw requests to Repo.XferHandler and, through `fossil http --in`, to fossil(1) on a copy of the same repository; client cases drive the public Sync and Clone against a scripted peer, with fossil(1)'s client run against the same script for most of them; HTTP cases cover redirects, status codes and media types; interop cases run whole exchanges in both directions.
Known failures are marked with the placeholder #0 until their issues are filed, so TestCoverage fails until then.
* test(conformance): sync suite review fixes (#262)
Cases for the requirements that had none where fossil(1) can show the behavior: server-version, the SHA3 gate, shun admission, cookie and cleartext logins, pushed configuration, ci-unlock, cluster brackets, the clone's unversioned catalog and the pull-only message. Two rows added: link ARG handling and the short uvigot.
A fossil(1) disagreement, a harness failure or a deviation other than the one named now fails a case whatever its mark. The server's log output is discarded in the tests.
* test(conformance): mark sync gaps with their issues; drop gaps v0.11.1 closed (#262)
v0.11.1's read gate and redirect policy make seven sync cases conform, so their gap marks are removed. The rest are marked with the issues that track each fix: #296 and #299-#310.
* test(conformance): accept either fossil refusal of a negative clone SEQNO
fossil 2.28's macOS build answers clone 3 -1 with clone_seqno 0 and its Linux build, which CI pins, with the invalid-sequence error alone. The draft leaves the reply open; the case now holds go-libfossil only to sending no content. (user: [email protected], size: 6173) [annotate] [blame] [check-ins using]
package conformance import ( "bytes" "context" "net/http/httptest" "os" "path/filepath" "testing" "time" libfossil "github.com/danmestas/go-libfossil" "github.com/danmestas/go-libfossil/internal/content" ) // The interop cases run whole sessions between go-libfossil and fossil(1), // each side serving or cloning the other. They need fossil(1); CI has it. // fossilTransport is a libfossil.FramedTransport whose far end is fossil(1) // serving the repository at path, one `fossil http` per round. type fossilTransport struct { ref *fossilReference path string } func (t fossilTransport) RoundTrip(ctx context.Context, payload []byte) ([]byte, error) { reply, _, err := t.RoundTripFramed(ctx, payload) return reply, err } func (t fossilTransport) RoundTripFramed(_ context.Context, payload []byte) ([]byte, string, error) { status, ctype, raw, err := t.ref.serveEntity(t.path, syncRequest{method: "POST", path: "/", ctype: typeCompressed, raw: payload}) if err == nil && status != 200 { err = expectf("fossil http answered %d: %.200q", status, raw) } return raw, ctype, err } type syncInteropCase struct { Case run func(fx *syncFixture, ref *fossilReference, dir string) error } func syncInteropCases() []syncInteropCase { return []syncInteropCase{ {Case{ID: "sync.7.2.interop-clone-from-fossil", Req: "SP-7.2-1"}, cloneFromFossil}, {Case{ID: "sync.8.2.interop-fossil-clones-us", Req: "SP-8.2-1"}, fossilClonesUs}, {Case{ID: "sync.3.7.interop-roundtrip", Req: "SP-3.7-1"}, syncBothWays}, } } func TestSyncInterop(t *testing.T) { ref := newFossilReference(t) quietLogs(t) if ref == nil { t.Skip("fossil not found") } top := t.TempDir() fx := newSyncFixture(t, top) for _, c := range syncInteropCases() { 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 nil, c.run(fx, ref, dir) }) } } // cloneFromFossil clones F, served by fossil(1), with go-libfossil's Clone. func cloneFromFossil(fx *syncFixture, ref *fossilReference, dir string) error { src, err := prepare(fx, dir, "served.fossil", nil) if err != nil { return err } dst := filepath.Join(dir, "clone.fossil") ctx, cancel := context.WithTimeout(context.Background(), clientTimeout) defer cancel() peer := fossilTransport{ref: ref, path: src} r, _, err := libfossil.Clone(ctx, dst, peer, libfossil.CloneOpts{}) if err != nil { return expectf("clone from fossil(1): %v", err) } if err := r.Close(); err != nil { return err } return holdsAll(dst, src) } // fossilClonesUs clones F, served by go-libfossil's handler, with // fossil(1)'s clone. func fossilClonesUs(fx *syncFixture, ref *fossilReference, dir string) error { src, err := prepare(fx, dir, "served.fossil", nil) if err != nil { return err } r, err := libfossil.Open(src) if err != nil { return err } srv := httptest.NewServer(r.XferHandler()) dst := filepath.Join(dir, "fossil-clone.fossil") _, cloneErr := ref.run(dir, "clone", srv.URL+"/", dst) srv.Close() if err := r.Close(); err != nil { return err } if cloneErr != nil { return expectf("fossil clone: %v", cloneErr) } return holdsAll(dst, src) } // syncBothWays adds a check-in to each of two copies of F, then syncs // go-libfossil's copy with fossil(1) serving the other: each must end // with the other's check-in. func syncBothWays(fx *syncFixture, ref *fossilReference, dir string) error { ours, err := prepareCheckin(fx, dir, "ours.fossil", "b.txt") if err != nil { return err } theirs, err := prepareCheckin(fx, dir, "theirs.fossil", "c.txt") if err != nil { return err } r, err := libfossil.Open(ours) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), clientTimeout) defer cancel() _, syncErr := r.Sync(ctx, fossilTransport{ref: ref, path: theirs}, libfossil.SyncOpts{Push: true, Pull: true}) if err := r.Close(); err != nil { return err } if syncErr != nil { return expectf("sync with fossil(1): %v", syncErr) } if err := holdsAll(ours, theirs); err != nil { return err } return holdsAll(theirs, ours) } // prepareCheckin copies F and commits one new file to the copy. func prepareCheckin(fx *syncFixture, dir, name, file string) (string, error) { path, err := prepare(fx, dir, name, nil) if err != nil { return "", err } r, err := libfossil.Open(path) if err != nil { return "", err } _, _, err = r.Commit(libfossil.CommitOpts{ Files: []libfossil.FileToCommit{{Name: file, Content: []byte(file + "\n")}}, Comment: "add " + file, User: "setup", Time: time.Unix(syncEpoch+3600, 0), }) if closeErr := r.Close(); err == nil { err = closeErr } return path, err } // holdsAll checks the repository at path can reconstruct every public // artifact of the one at from, to the same bytes. func holdsAll(path, from string) error { src, err := libfossil.Open(from) if err != nil { return err } defer src.Close() dst, err := libfossil.Open(path) if err != nil { return err } defer dst.Close() rows, err := src.DB().Query("SELECT uuid FROM blob WHERE size>=0" + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)") if err != nil { return err } var names []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { rows.Close() return err } names = append(names, name) } if err := rows.Close(); err != nil { return err } for _, name := range names { if err := sameArtifact(src, dst, name); err != nil { return err } } return nil } func sameArtifact(src, dst *libfossil.Repo, name string) error { want, err := expandName(src, name) if err != nil { return err } got, err := expandName(dst, name) if err != nil { return expectf("%s: %v", filepath.Base(dst.Path()), err) } if !bytes.Equal(got, want) { return expectf("%s: %s differs", filepath.Base(dst.Path()), name) } return nil } func expandName(r *libfossil.Repo, name string) ([]byte, error) { rid, ok := content.AvailableByUUID(r.DB(), name) if !ok { return nil, expectf("%s is not available", name) } return content.Expand(r.DB(), rid) }