Artifact b68d5364e2a01a3a0ac06bee1c8c50db8504f3d7e644b14cbfd4a0ca8439deae:
- File
conformance/delta_fossil_test.go
— part of check-in
[7fa0b9b5da]
at
2026-10-11 02:13:19
on branch trunk
— test(conformance): repository-state conformance suite (#262) (#295)
* test(conformance): repository-state conformance suite (#262)
Check go-libfossil against draft-fossil-repo-state-00, requirement by requirement. Each derived-state case is a scripted sequence of artifacts loaded into go-libfossil and into fossil(1); the tables derived from it are compared row by row, joined by artifact hash. Every case runs through manifest.Crosslink, as one sweep and one artifact at a time, and most also run as a ".rebuild" case through verify.Rebuild. fossil's rows must equal each expectation on every run, and each case states what ignoring its requirement would change, so a case cannot pass or fail by accident.
API cases cover name resolution, leaves, the ambiguity test, branch creation and listing, and tagging, each compared with fossil's answer on the same repository. TestRepoStateCorpus compares verify.Rebuild and a Crosslink sweep with fossil rebuild on real repositories named by CONFORMANCE_REPOS.
Known failures are marked with the placeholder Gap #0 until their issues are filed, so TestCoverage refuses them for now.
* test(conformance): repository-state suite review fixes (#262)
- Unsorted T-card cases use a non-hex name, so the ordering rule, not the hex-name rule, is what rejects them. - RS-4.2-7 crosslinks every artifact a second time over the rows the first left (repostate.4.2.relink-unchanged), instead of a sweep that links nothing. - RS-7.3-2, -3 and -4 are checked through manifest.Checkin and branch.Create, which maintain leaves one check-in at a time; a crosslink sweep recomputes the whole table. - retag-branch is built so the retagged check-in's own leaf status changes; branch membership is read through BranchLeaves, and the tagxref case it replaced is reattributed to RS-5.4-5; bad-prefix also compares tagxref. - parentless-untagged follows the draft and marks go-libfossil's crosslink, which follows fossil(1), as a deviation; the unused draft field becomes fossil, checked against fossil's rows. - Every fossil(1) command, in all suites, runs with HOME, FOSSIL_HOME and XDG_CONFIG_HOME in the test's directory, so it no longer records checkouts in the user's global configuration.
* test(conformance): mark repository-state gaps with their issues (#262)
Replace the placeholder gap marks with the issues that track each fix: #278 and #285-#294. (user: [email protected], size: 3506) [annotate] [blame] [check-ins using]
package conformance import ( "context" "encoding/hex" "fmt" "os" "path/filepath" "strconv" "strings" ) // fossil's delta functions live in its SQL layer, which wants a repository // even though these never read one. test-delta-apply cannot serve: it exits 0 // and writes an empty file whether it applied the delta or rejected it. // deltaRepo returns the scratch repository the SQL functions run against, // creating it on first use. func (f *fossilReference) deltaRepo() (string, error) { path := filepath.Join(f.dir, "delta-scratch.fossil") if _, err := os.Stat(path); err == nil { return path, nil } if output, err := f.command(context.Background(), "new", path).CombinedOutput(); err != nil { return "", fmt.Errorf("fossil new: %v: %s", err, output) } return path, nil } // blobLiteral is b as an SQL hex literal. Bytes travel as hex so that NUL, // quotes and the bytes 0x80-0xFF reach fossil unchanged. func blobLiteral(b []byte) string { return "x'" + hex.EncodeToString(b) + "'" } // deltaSQL evaluates one SELECT expression. fossil reports a rejected delta // as "Error: corrupt fossil delta", with exit status 0; any other failure, // such as the crash on a zero-length blob, is an error, not a verdict. func (f *fossilReference) deltaSQL(expr string) (out string, rejected bool, err error) { if f == nil { panic("conformance: deltaSQL without fossil") } repo, err := f.deltaRepo() if err != nil { return "", false, err } output, runErr := f.command(context.Background(), "sql", "-R", repo, "SELECT "+expr+";"). CombinedOutput() text := strings.TrimSpace(string(output)) if strings.Contains(text, "corrupt fossil delta") { return "", true, nil } if runErr == nil { return text, false, nil } return "", false, fmt.Errorf("fossil sql %.60s: %v: %s", expr, runErr, text) } // refuseEmptyDelta stops a zero-length delta on its way to fossil, whose // delta_apply and delta_output_size crash on one (SIGSEGV). func refuseEmptyDelta(d []byte) error { if len(d) == 0 { return fmt.Errorf("conformance: a zero-length delta must not be sent to fossil") } return nil } // applyDelta is delta_apply(src, d): the output, or rejected. func (f *fossilReference) applyDelta(src, d []byte) (out []byte, accepted bool, err error) { if err := refuseEmptyDelta(d); err != nil { return nil, false, err } text, rejected, err := f.deltaSQL(fmt.Sprintf("hex(delta_apply(%s,%s))", blobLiteral(src), blobLiteral(d))) if err != nil || rejected { return nil, false, err } out, err = hex.DecodeString(text) return out, err == nil, err } // deltaSize is delta_output_size(d). fossil keeps the size in a signed int, // so a header of 2^31 or more is reported as rejected. func (f *fossilReference) deltaSize(d []byte) (size uint64, accepted bool, err error) { if err := refuseEmptyDelta(d); err != nil { return 0, false, err } text, rejected, err := f.deltaSQL(fmt.Sprintf("delta_output_size(%s)", blobLiteral(d))) if err != nil || rejected { return 0, false, err } size, err = strconv.ParseUint(text, 10, 64) return size, err == nil, err } // createDelta is delta_create(src, tgt), fossil's own encoder. func (f *fossilReference) createDelta(src, tgt []byte) ([]byte, error) { text, rejected, err := f.deltaSQL(fmt.Sprintf("hex(delta_create(%s,%s))", blobLiteral(src), blobLiteral(tgt))) if err != nil { return nil, err } if rejected { return nil, fmt.Errorf("fossil delta_create rejected its input") } return hex.DecodeString(text) }