Artifact b4bdc613049db112ced1337c5644fb9ca8c3e47cc9783972c4f346566f6894f3:
- File
conformance/harness_test.go
— part of check-in
[57a558fcbf]
at
2026-10-10 23:52:34
on branch trunk
— test(conformance): harness for an RFC conformance suite (#262) (#264)
Adds the conformance package that the per-draft suites build on. Nothing in the repository has yet checked go-libfossil against the Fossil drafts requirement by requirement.
- A Case names its requirement. A Gap (an issue) or Deviation (a reason) marks a known difference. A marked case still runs: while it fails it is skipped, and once it conforms it fails until the mark is removed, so the list of known failures cannot go stale. - Requirements tables pin the SHA-256 of the drafts and errata they were written against. Those are copied unmodified into conformance/rfcs by scripts/refresh-conformance-rfcs.sh. A changed draft fails the coverage test until its table has been re-read and its pin moved. - Coverage is derived from the cases rather than recorded: - COVERED: every case conforms. - PARTIAL: the table's row says what its cases leave out. - DEVIATION: a case is marked as a deliberate difference. - FAILING: a case is marked with an open gap. - GAP: no case yet.
CONFORMANCE_REPORT=<path> writes the requirement-by-requirement report as Markdown. - fossil(1) is run on every case's input, through test-parse-manifest, so a case's expectation is checked against fossil each time the suite runs. withZ appends the Z card, so a case that edits one card never also breaks the checksum.
The harness's own rules are tested in harness_self_test.go. No suites are registered yet; the artifact-format suite is next (#263). (user: [email protected], size: 2611) [annotate] [blame] [check-ins using]
package conformance import ( "fmt" "strings" "testing" ) // Case is what every conformance case carries, whatever it checks. type Case struct { // ID names the case: "<suite>.<section>.<what>", e.g. // "artifact.4.7.6.unsorted". Where libfossil's suite has the same case, // the ID matches its, so the two implementations can be compared. ID string // Req is the requirement ID from the suite's requirements table. Req string // Gap is the issue tracking a known failure, e.g. "#270". The case still // runs: while it fails it is skipped, and once it passes it fails until // Gap is removed. Gap string // Deviation says why go-libfossil deliberately does not do what the // draft requires. It behaves like Gap, but there is nothing to fix. Deviation string } // outcome is how one case run is reported. type outcome int const ( outcomePass outcome = iota outcomeFail outcomeSkip ) // judge decides how a case is reported, given the problem its check found // (nil when go-libfossil conforms). It is kept apart from check so the gap // rules are testable without a *testing.T. func judge(c Case, problem error) (outcome, string) { if c.ID == "" { panic("conformance: case without an ID") } if c.Gap != "" && c.Deviation != "" { panic("conformance: case " + c.ID + " is marked both Gap and Deviation") } marked, mark := c.Gap, "gap "+c.Gap if c.Deviation != "" { marked, mark = c.Deviation, "deviation: "+c.Deviation } if problem == nil { if marked == "" { return outcomePass, "" } return outcomeFail, fmt.Sprintf("%s now conforms; remove the mark (%s)", c.ID, mark) } if marked == "" { return outcomeFail, fmt.Sprintf("%s [%s]: %v", c.ID, c.Req, problem) } return outcomeSkip, fmt.Sprintf("%s (%s): %v", c.ID, mark, problem) } // check runs one case as a subtest. conforms returns nil when go-libfossil // does what the case expects, or a one-line description of what it did // instead. func check(t *testing.T, c Case, conforms func() error) { t.Helper() t.Run(c.ID, func(t *testing.T) { t.Helper() result, message := judge(c, conforms()) switch result { case outcomePass: case outcomeFail: t.Fatal(message) case outcomeSkip: t.Skip(message) default: panic(fmt.Sprintf("conformance: unknown outcome %d", result)) } }) } // expectf builds the problem a check reports: what was expected, what // happened. func expectf(format string, args ...any) error { message := fmt.Sprintf(format, args...) if strings.ContainsRune(message, '\n') { message = strings.ReplaceAll(message, "\n", `\n`) } return fmt.Errorf("%s", message) }