Artifact 8a911d99394d296fdb2fb87f6c7ffd85a8eacc00d2bc59565227282130e7fc38:
- File
conformance/repostate_api_cases_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: 5704) [annotate] [blame] [check-ins using]
package conformance import ( "fmt" "strings" ) func rsAPICases() []rsAPICase { var all []rsAPICase for _, table := range [][]rsAPICase{ resolveCases(), resolveFormCases(), leafAPICases(), commitLeafCases(), branchAPICases(), writerAPICases(), } { all = append(all, table...) } for i := range all { all[i].Gap = repostateGaps[all[i].ID] } return all } // forked is c1 with two trunk children, a and b, b the newer; b closes // itself when closed is set. func forked(closed bool) []rsArt { var tags []string if closed { tags = []string{"+closed *"} } return []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("a", 2, of("c1")), checkin("b", 3, of("c1"), tags...), } } // c2Prefix is the first n hex digits of chain's c2, which depends only on // c1 and c2, so a script may use it before it is built. func c2Prefix(n int) string { return rsBuild(chain()).hashes["c2"][:n] } func resolveCases() []rsAPICase { return []rsAPICase{ // Tagged later, c1 carries the newer tag row; c3 has the newer // check-in time, which is what the rule compares. resolves("repostate.8.2.event-mtime", "RS-8.2-1", with(chain(), control("t1", 8, "+sym-rel {c3}"), control("t2", 9, "+sym-rel {c1}")), "rel", "c3"), // c3 and c2 inherit sym-trunk from c1, with c1's tag time; only // their own times tell them apart. resolves("repostate.8.2.trunk-chain", "RS-8.2-1", []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("c3", 9, of("c1")), checkin("c2", 2, of("c1")), }, "trunk", "c3"), resolves("repostate.8.2.closed-newer", "RS-8.2-2", forked(true), "trunk", "b"), // c2, the newest trunk check-in, has a child, so is no leaf. resolves("repostate.8.2.not-leaf", "RS-8.2-3", []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("c2", 9, of("c1")), checkin("c3", 3, of("c2")), }, "trunk", "c2"), resolves("repostate.8.2.unknown", "RS-8.2-4", chain(), "nosuch", ""), resolves("repostate.8.2.no-trunk-sym", "RS-8.2-4", []rsArt{checkin("c1", 1, nil), checkin("c2", 2, of("c1"))}, "trunk", ""), resolves("repostate.9.fork-symbol", "RS-9-2", forked(false), "trunk", "b"), { Case: Case{ID: "repostate.8.5.resolution-not-leaves", Req: "RS-8.5-1"}, arts: forked(true), run: func(e *rsEnv) error { if err := resolves("", "", nil, "trunk", "b").run(e); err != nil { return err } return openLeaves("", "", nil, "trunk", "a").run(e) }, }, } } func resolveFormCases() []rsAPICase { release := with(chain(), control("t1", 8, "+sym-release {c3}")) // A tag named like c2's hash prefix, on c1. hexTag := with(chain(), control("t1", 8, "+sym-"+c2Prefix(6)+" {c1}")) mergedIn := []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("b1", 2, of("c1"), rsFeat...), checkin("c2", 3, of("c1")), checkin("b2", 4, of("b1", "c2")), checkin("b3", 5, of("b2")), } return []rsAPICase{ resolves("repostate.8.3.rid", "RS-8.3-1", chain(), "rid:{rid:c2}", "c2"), // Without the prefix the name would be read as c2's hash. resolves("repostate.8.3.tag-prefix", "RS-8.3-2", hexTag, "tag:"+c2Prefix(6), "c1"), // tip is the newest check-in on any branch, here b3 on feat. resolves("repostate.8.3.tip", "RS-8.3-3", with(branched(), checkin("b3", 6, of("b2"))), "tip", "b3"), resolves("repostate.8.3.root", "RS-8.3-5", branched(), "root:feat", "c1"), resolves("repostate.8.3.start", "RS-8.3-6", with(branched(), checkin("b3", 6, of("b2"))), "start:feat", "b1"), resolves("repostate.8.3.merge-in", "RS-8.3-7", mergedIn, "merge-in:feat", "c2"), resolves("repostate.8.3.name-timestamp", "RS-8.3-8", chain(), "trunk:2026-01-01T00:00:03", "c3"), resolves("repostate.8.3.hash-before-name", "RS-8.3-9", hexTag, c2Prefix(6), "c2"), resolves("repostate.8.3.ambiguous-prefix", "RS-8.3-10", ambiguous(), ambiguousPrefix(), ""), // fossil retries "release." but not a three-letter stem such as // "rel."; the case uses the longer name. resolves("repostate.8.3.trailing-punct", "RS-8.3-11", release, "release.", "c3"), } } // ambiguous is chain[:1] and a second root whose hash shares its first four // hex digits with c1's: the comment is varied until it does. func ambiguous() []rsArt { c1 := chain()[0] prefix := rsBuild([]rsArt{c1}).hashes["c1"][:4] for n := 0; n < 1<<22; n++ { twin := checkin("twin", 2, nil, rsRoot...) twin.body = strings.Replace(twin.body, "C twin", fmt.Sprintf("C twin%d", n), 1) if rsBuild([]rsArt{twin}).hashes["twin"][:4] == prefix { return []rsArt{c1, twin} } } panic("conformance: no artifact shares c1's hash prefix") } func ambiguousPrefix() string { return rsBuild(chain()[:1]).hashes["c1"][:4] } func leafAPICases() []rsAPICase { forkedChain := with(chain(), checkin("f", 5, of("c3"))) closedC4 := with(forkedChain, control("t", 9, "+closed {c4}")) return []rsAPICase{ openLeaves("repostate.7.4.closed-leaf", "RS-7.4-1", closedC4, "trunk", "f"), openLeaves("repostate.7.4.reopened", "RS-7.4-1", with(closedC4, control("t2", 10, "-closed {c4}")), "trunk", "c4", "f"), openLeaves("repostate.7.5.branch-open-leaves", "RS-7.5-1", merged(), "feat", "b1"), // BranchLeaves selects a branch's members by their active branch // row: b2, which inherits feat, is on feat; c2, a leaf too, is not. openLeaves("repostate.6.1.branch-membership", "RS-6.1-1", branched(), "feat", "b2"), openLeaves("repostate.9.branch-not-open", "RS-9-3", with(branched(), control("t", 9, "+closed {b2}")), "feat"), wouldFork("repostate.7.5.ambiguity-non-leaf", "RS-7.5-2", forkedChain, "c3", false), wouldFork("repostate.7.5.ambiguity-open", "RS-7.5-3", forkedChain, "f", true), wouldFork("repostate.7.5.ambiguity-closed", "RS-7.5-3", closedC4, "f", false), } }