Artifact bccafd02d3568e2d7fda92771a164b2906a164bff65b3ebed36cc7f693e26058:
- File
conformance/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: 3511) [annotate] [blame] [check-ins using]
package conformance import ( "context" "crypto/md5" "encoding/hex" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/testutil" ) // fossilReference runs fossil(1) on the same input a case gives // go-libfossil, so every case's expectation is checked against fossil on // every run rather than once, when the case was written. type fossilReference struct { bin string dir string count int } // newFossilReference finds fossil(1). Without it the cases still run against // go-libfossil, but nothing checks their expectations; CI sets // REQUIRE_FOSSIL_BIN=1 so a missing binary fails there instead. func newFossilReference(t *testing.T) *fossilReference { t.Helper() bin := testutil.FossilBinary() if bin == "" { if os.Getenv("REQUIRE_FOSSIL_BIN") == "1" { t.Fatal("REQUIRE_FOSSIL_BIN=1 but no fossil binary found") } t.Log("fossil not found: case expectations are not checked against fossil(1)") return nil } dir := t.TempDir() if err := os.Mkdir(filepath.Join(dir, "home"), 0o755); err != nil { t.Fatal(err) } return &fossilReference{bin: bin, dir: dir} } // command builds a fossil(1) command whose global configuration lives in // the reference's own directory. fossil records every checkout it opens, // and more, in a per-user database (~/.fossil or ~/.config/fossil.db); a // test must not write there. The last value of a repeated variable wins. func (f *fossilReference) command(ctx context.Context, args ...string) *exec.Cmd { home := filepath.Join(f.dir, "home") cmd := exec.CommandContext(ctx, f.bin, args...) cmd.Env = append(os.Environ(), "HOME="+home, "FOSSIL_HOME="+home, "XDG_CONFIG_HOME="+home, "USER=u") return cmd } // parseArtifact reports whether fossil's manifest parser accepts data, and // fossil's reason when it does not. test-parse-manifest exits 0 either way, // so the verdict is read from its output. func (f *fossilReference) parseArtifact(data []byte) (accepted bool, reason string, err error) { if f == nil { panic("conformance: parseArtifact without fossil") } f.count++ path := filepath.Join(f.dir, fmt.Sprintf("artifact-%d", f.count)) if err := os.WriteFile(path, data, 0o644); err != nil { return false, "", err } output, err := f.command(context.Background(), "test-parse-manifest", path).CombinedOutput() if err != nil { return false, "", fmt.Errorf("fossil test-parse-manifest: %v: %s", err, output) } text := string(output) if strings.Contains(text, "manifest_parse() worked") { return true, "", nil } for _, line := range strings.Split(text, "\n") { if strings.HasPrefix(line, "ERROR: ") { return false, strings.TrimPrefix(line, "ERROR: "), nil } } return false, "", fmt.Errorf("fossil test-parse-manifest: unrecognized output %q", text) } // withZ appends the Z card for body: the MD5 of every byte before it. Cases // build artifacts without one so that a single edit never also breaks the // checksum, which would make any rejection look deserved. func withZ(body string) []byte { if body != "" && !strings.HasSuffix(body, "\n") { panic("conformance: artifact body must end in a newline") } return []byte(body + zCard(body)) } // zCard is the Z card line for data: "Z", a space, the lowercase MD5 of // data, LF. Unlike withZ it accepts any data, for cases that put the Z // card somewhere a well-formed artifact would not. func zCard(data string) string { sum := md5.Sum([]byte(data)) return "Z " + hex.EncodeToString(sum[:]) + "\n" }