Artifact 0f347b37d1a52f6e0feedc3e34958145ce9c1dcc520958df40f65070918dfa76:
- File
conformance/repostate_tags_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: 8402) [annotate] [blame] [check-ins using]
package conformance import "strings" // Derived-state cases for tag types, direct application, propagation and // blocking (Section 5), all on chain: c1 to c4. // foo is a case comparing only the foo tag's rows. func foo(id, req string, arts []rsArt, want []string, ignoring ...string) rsCase { return rsCase{ Case: Case{ID: id, Req: req}, arts: arts, tables: []string{"tagxref"}, tags: []string{"foo"}, want: rows("tagxref", want...), ignoring: ignoring, rebuild: true, } } // fooOn is what *foo on c2 at second 9 derives: c2's own row, inherited // by c3 and c4. var fooOn = []string{"foo c2 2 t c2 'v' @9", "foo c3 2 0 c2 'v' @9", "foo c4 2 0 c2 'v' @9"} func tagTypeCases() []rsCase { typed := rsCase{ Case: Case{ID: "repostate.5.1.type-codes", Req: "RS-5.1-1"}, arts: with(chain(), control("t", 9, "*xyz {c2}", "+yes {c2}", "-zap {c2}")), tables: []string{"tagxref"}, tags: []string{"xyz", "yes", "zap"}, want: rows("tagxref", "zap c2 0 t c2 NULL @9", "yes c2 1 t c2 NULL @9", "xyz c2 2 t c2 NULL @9", "xyz c3 2 0 c2 NULL @9", "xyz c4 2 0 c2 NULL @9"), ignoring: []string{"-tagxref: yes c2 1 t c2 NULL @9", "+tagxref: yes c2 2 t c2 NULL @9"}, rebuild: true, } badType := rsCase{ Case: Case{ID: "repostate.5.1.bad-prefix", Req: "RS-5.1-2"}, arts: with(chain()[:2], literal("t", "D "+rsTime(9)+"\nT ~x {c2}\nU u\n")), tables: []string{"event", "tagxref"}, tags: []string{"x", "~x"}, want: rows("event", "c1 ci @1", "c2 ci @2"), // Read as any type, ~ would give a row for x or ~x on c2. ignoring: []string{"+event: t g @9", "+tagxref: x c2 1 t c2 NULL @9"}, rebuild: true, } return []rsCase{typed, badType, foo("repostate.5.2.one-row", "RS-5.2-1", with(chain(), control("t1", 8, "*foo {c2} a"), control("t2", 9, "*foo {c2} v")), []string{"foo c2 2 t2 c2 'v' @9", "foo c3 2 0 c2 'v' @9", "foo c4 2 0 c2 'v' @9"}, "+tagxref: foo c2 2 t1 c2 'a' @8"), foo("repostate.5.3.older-tag-skipped", "RS-5.3-1", with(chain(), control("t1", 9, "*foo {c2} v"), control("t2", 8, "*foo {c2} old")), []string{"foo c2 2 t1 c2 'v' @9", "foo c3 2 0 c2 'v' @9", "foo c4 2 0 c2 'v' @9"}, "-tagxref: foo c2 2 t1 c2 'v' @9", "+tagxref: foo c2 2 t2 c2 'old' @8"), foo("repostate.5.3.cancel-older-than-inherited", "RS-5.3-1", with(chain(), control("t1", 9, "*foo {c1} v"), control("t2", 8, "-foo {c2}")), []string{"foo c1 2 t1 c1 'v' @9", "foo c2 2 0 c1 'v' @9", "foo c3 2 0 c1 'v' @9", "foo c4 2 0 c1 'v' @9"}, "-tagxref: foo c2 2 0 c1 'v' @9", "+tagxref: foo c2 0 t2 c2 NULL @8"), equalFirstWins(), foo("repostate.5.3.direct-row-shape", "RS-5.3-2", with(chain(), control("t", 9, "*foo {c2} v")), fooOn, "-tagxref: foo c2 2 t c2 'v' @9", "+tagxref: foo c2 2 0 c2 'v' @9"), foo("repostate.5.3.cancel-clears-value", "RS-5.3-3", with(chain(), control("t1", 8, "*foo {c2} v"), control("t2", 9, "-foo {c2}")), []string{"foo c2 0 t2 c2 NULL @9"}, "-tagxref: foo c2 0 t2 c2 NULL @9", "+tagxref: foo c2 0 t2 c2 'v' @9"), foo("repostate.5.3.singleton-local", "RS-5.3-4", with(chain(), control("t1", 8, "*foo {c1} v"), control("t2", 9, "+foo {c2} s")), []string{"foo c1 2 t1 c1 'v' @8", "foo c2 1 t2 c2 's' @9"}, "+tagxref: foo c3 2 0 c1 'v' @8"), } } // equalFirstWins applies two tags with equal times. Section 5.3 leaves the // winner implementation-defined; fossil(1) keeps the first, and so does // the case. func equalFirstWins() rsCase { c := foo("repostate.5.3.equal-tag-first-wins", "RS-5.3-1", with(chain(), control("t1", 9, "*foo {c2} v"), control("t2", 9, "*foo {c2} second")), []string{"foo c2 2 t1 c2 'v' @9", "foo c3 2 0 c2 'v' @9", "foo c4 2 0 c2 'v' @9"}, "-tagxref: foo c2 2 t1 c2 'v' @9", "+tagxref: foo c2 2 t2 c2 'second' @9") c.each, c.rebuild = true, false return c } // fooFromC1 is *foo on c1 at second 9, inherited down the chain. func fooFromC1() rsArt { return control("t", 9, "*foo {c1} v") } func propagationCases() []rsCase { return []rsCase{ foo("repostate.5.4.merge-no-propagation", "RS-5.4-1", with(merged(), control("t", 9, "*foo {b1} v")), []string{"foo b1 2 t b1 'v' @9"}, "+tagxref: foo m 2 0 b1 'v' @9"), repropagateSingleton(), foo("repostate.5.4.inherited-tag", "RS-5.4-5", with(chain(), control("t", 9, "*foo {c2} v")), fooOn, "-tagxref: foo c3 2 0 c2 'v' @9", "+tagxref: foo c3 2 0 c3 'v' @3"), foo("repostate.5.4.walk-transitive", "RS-5.4-7", with(chain(), control("t", 9, "*foo {c2} v")), fooOn, "-tagxref: foo c4 2 0 c2 'v' @9"), foo("repostate.5.4.cancel-propagation", "RS-5.4-6", with(chain(), control("t1", 8, "*foo {c1} v"), control("t2", 9, "-foo {c3}")), []string{"foo c1 2 t1 c1 'v' @8", "foo c2 2 0 c1 'v' @8", "foo c3 0 t2 c3 NULL @9"}, "+tagxref: foo c4 2 0 c1 'v' @8"), doctored("repostate.5.4.equal-mtime-retained", "RS-5.4-3", []rsArt{control("t1", 8, "*foo {c1} a"), control("t2", 9, "*foo {c2} b")}, "UPDATE tagxref SET mtime = julianday('"+rsTime(9)+"')", "c3", "c4")( []string{"foo c1 2 t1 c1 'a' @8", "foo c2 2 t2 c2 'b' @9", "foo c3 2 0 c1 'a' @9", "foo c4 2 0 c1 'a' @9"}, "-tagxref: foo c3 2 0 c1 'a' @9", "+tagxref: foo c3 2 0 c2 'b' @9"), // Erratum repo-state-00-s5.4-cancel-halt (Reported): a cancel walk // halts at c3, which has no foo row, so c4 keeps its row. fossil // 2.28 does so; the draft text would walk on and delete it. doctored("repostate.5.4.cancel-rowless", "RS-5.4-4", []rsArt{fooFromC1(), control("x", 20, "-foo {c1}")}, "DELETE FROM tagxref", "c3")( []string{"foo c1 0 x c1 NULL @20", "foo c4 2 0 c1 'v' @9"}, "-tagxref: foo c4 2 0 c1 'v' @9"), doctored("repostate.5.4.cancel-skips-newer-inherited", "RS-5.4-3", []rsArt{fooFromC1(), control("x", 20, "-foo {c1}")}, "UPDATE tagxref SET mtime = 2500000", "c3", "c4")( []string{"foo c1 0 x c1 NULL @20", "foo c3 2 0 c1 'v' 5133240000000", "foo c4 2 0 c1 'v' 5133240000000"}, "-tagxref: foo c3 2 0 c1 'v' 5133240000000", "-tagxref: foo c4 2 0 c1 'v' 5133240000000"), } } // repropagateSingleton adds c5 under c2 after a singleton on c2: crosslinking // c5 re-propagates c2's tags, and the singleton must reach c5 as a cancel. func repropagateSingleton() rsCase { c := foo("repostate.5.4.repropagate-singleton", "RS-5.4-2", with(chain()[:2], control("t1", 8, "*foo {c1} v"), control("t2", 9, "+foo {c2} s"), checkin("c5", 5, of("c2"))), []string{"foo c1 2 t1 c1 'v' @8", "foo c2 1 t2 c2 's' @9"}, "+tagxref: foo c5 2 0 c2 's' @9") c.each = true return c } // doctored loads chain and the first of more, then edits the foo rows of // the check-ins named in on with edit, an UPDATE or DELETE of tagxref, to // reach a state no known artifact sequence reaches, and loads the rest of // more one at a time. Edited rows stay consistent along the chain: an // inherited row newer than its child's would be re-propagated by the next // crosslink, a change no requirement here is about. func doctored( id, req string, more []rsArt, edit string, on ...string, ) func(want []string, ignoring ...string) rsCase { return func(want []string, ignoring ...string) rsCase { c := foo(id, req, with(chain(), more...), want, ignoring...) c.rebuild = false uuids := "'{" + strings.Join(on, "}', '{") + "}'" c.doctor = &rsDoctor{at: len(chain()) + 1, sql: edit + " WHERE tagid = (SELECT tagid FROM tag WHERE tagname = 'foo')" + " AND rid IN (SELECT rid FROM blob WHERE uuid IN (" + uuids + "))"} return c } } func blockingCases() []rsCase { return []rsCase{ foo("repostate.5.5.direct-override-barrier", "RS-5.5-1", with(chain(), control("t1", 8, "*foo {c3} A"), control("t2", 9, "*foo {c1} B")), []string{"foo c1 2 t2 c1 'B' @9", "foo c2 2 0 c1 'B' @9", "foo c3 2 t1 c3 'A' @8", "foo c4 2 0 c3 'A' @8"}, "-tagxref: foo c3 2 t1 c3 'A' @8", "+tagxref: foo c3 2 0 c1 'B' @9"), foo("repostate.5.5.cancel-blocks", "RS-5.5-2", with(chain(), control("t1", 8, "-foo {c3}"), control("t2", 9, "*foo {c1} v")), []string{"foo c1 2 t2 c1 'v' @9", "foo c2 2 0 c1 'v' @9", "foo c3 0 t1 c3 NULL @8"}, "+tagxref: foo c4 2 0 c1 'v' @9"), foo("repostate.5.5.singleton-barrier", "RS-5.5-3", with(chain(), control("t1", 8, "+foo {c3} s"), control("t2", 9, "*foo {c1} v")), []string{"foo c1 2 t2 c1 'v' @9", "foo c2 2 0 c1 'v' @9", "foo c3 1 t1 c3 's' @8"}, "+tagxref: foo c4 2 0 c1 'v' @9"), } }