Artifact e89c7509570e8878f5c908bbbae0ef4089f12093e2d990634a5f91de2b408df2:
- File
conformance/artifact_test.go
— part of check-in
[d52a6fe050]
at
2026-10-11 00:42:57
on branch trunk
— test(conformance): artifact-format conformance suite (#263) (#276)
* test(conformance): artifact-format conformance suite (#263)
Adds the suite for draft-fossil-artifact-format-00 with errata 4.1.2, 4.2 and 4.6.4, so the parser, writer, naming, hash policy and delta manifest code are checked requirement by requirement instead of by scattered unit tests that each encode one reading of fossil.
- requirements/artifact.tsv: 101 requirements from Sections 3 to 8, pinned to the draft and the three errata. - Parse cases are a known-good base artifact per type plus one edit, with the Z card recomputed, so the edit alone explains a rejection. Every case also runs through fossil test-parse-manifest; fossil 2.28 agrees with all of them. Card legality, duplicates, arity and hash positions are generated from the bases and the draft's own tables. - Accepted inputs must re-encode byte for byte unless marked non-canonical. - API cases cover naming, verification, the comparator, the writer, the R card, the hash policy and delta-manifest enumeration. - FuzzArtifactRoundTrip checks parse/marshal/parse stability, and TestArtifactCorpus (CONFORMANCE_REPOS) parses every artifact fossil crosslinked in real repositories.
267 cases go-libfossil fails are marked with a "#0" placeholder in artifact_gaps_test.go, grouped by the code at fault, for issues to be filed against.
* test(conformance): tighten the artifact suite after review (#263)
A rejection only counts when the rule the case names makes it, so every rejection case that is not about the Z card must now pass deck.VerifyZ first. Inputs go-libfossil rejected only through the Z check (framing, residue, sterilizer, unstripped wrappers, an extra Z token) become gaps, along with VerifyZ itself, which rejects an absent Z card and reads the last byte where Section 6.2 says it must not.
- Cases can name the substring fossil's rejection reason must contain; the unknown-letter cases sit in sorted position, the sterilizer cases are filed under card order, which is what fossil rejects them for, and generated legality cases take their requirement from the draft's table. - The ratchet that never persists the hash policy is a gap: fossil opening the repository still reads auto. - New rows for 4.7.9, 6.2 (absent Z, last byte), 6.3.4 and 7.1. - The audit refuses the #0 placeholder, a test keeps the gap map and the fuzz test's forgiven losses tied to live gap cases, and one Z-card helper replaces two.
* test(conformance): mark artifact gaps with their issues (#263)
Replace the placeholder gap marks with the issues that track each fix (#265-#275), and keep coverage_test.go within 100 columns. (user: [email protected], size: 11100) [annotate] [blame] [check-ins using]
package conformance import ( "bytes" "fmt" "strings" "testing" "github.com/danmestas/go-libfossil/internal/deck" ) // The artifact suite checks draft-fossil-artifact-format-00. Most of its // cases are byte strings given to deck.Parse; each is built from one of the // base artifacts below by a single edit, so the edit is the only thing that // can explain a rejection, and every one is also given to fossil(1). // artifactCase is one input to deck.Parse and what the draft says about it. type artifactCase struct { Case // input is the whole artifact. Build it with withZ unless the case is // about the Z card itself. input []byte accept bool // check, when set, inspects an accepted parse. check func(*deck.Deck) error // fossil is set only when fossil(1) gives the opposite verdict; it // cites the erratum or the reason the case does not follow fossil. fossil string // nonCanonical marks an accepted input Marshal need not reproduce byte // for byte, such as a bare U card that reads back as "U anonymous". nonCanonical bool // zCase marks a rejection that is the Z card's to make. Every other // rejection must pass deck.VerifyZ first, so the rule the case targets, // not the checksum, is what rejects it. zCase bool // fossilSays, when set, is a substring fossil's reason for rejecting // the input must contain, for cases where several rules could reject it. // fossil is pinned at 2.28 in CI, so its wording is stable. fossilSays string } // Hash tokens. Their values only need to be well formed and, where a run of // them must ascend, in order: h1 < h2 < ... < h9. const ( h1 = "0123456789abcdef0123456789abcdef01234567" h2 = "1123456789abcdef0123456789abcdef01234567" h3 = "2123456789abcdef0123456789abcdef01234567" h4 = "3123456789abcdef0123456789abcdef01234567" h5 = "4123456789abcdef0123456789abcdef01234567" h6 = "5123456789abcdef0123456789abcdef01234567" h7 = "6123456789abcdef0123456789abcdef01234567" h8 = "7123456789abcdef0123456789abcdef01234567" h9 = "8123456789abcdef0123456789abcdef01234567" h64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" md5Token = "0123456789abcdef0123456789abcdef" ts = "2026-01-02T03:04:05.000" ) // One base per artifact type, plus a delta manifest, each as its cards in // order. Every base uses the cards its type allows in the form Marshal // writes, so TestArtifactBases can require an exact re-encoding. var ( manifestCards = []string{ "C base\\smanifest\n", "D " + ts + "\n", "F a.txt " + h1 + "\n", "F b/c.txt " + h2 + " x\n", "F d.txt " + h3 + " w old.txt\n", "P " + h4 + " " + h5 + "\n", "Q +" + h6 + " " + h7 + "\n", "R " + md5Token + "\n", "T *branch * trunk\n", "T +sym-trunk *\n", "U alice\n", } deltaCards = []string{ "B " + h1 + "\n", "C base\\sdelta\n", "D " + ts + "\n", "F a.txt " + h2 + "\n", "F gone.txt\n", "P " + h1 + "\n", "U alice\n", } clusterCards = []string{"M " + h1 + "\n", "M " + h2 + "\n"} controlCards = []string{ "D " + ts + "\n", "T +bgcolor " + h1 + " blue\n", "T -sym-old " + h2 + "\n", "U alice\n", } wikiCards = []string{ "C edit\\scomment\n", "D " + ts + "\n", "L Home\\sPage\n", "N text/x-markdown\n", "P " + h1 + "\n", "U alice\n", "W 12\nhello\nworld\n\n", } ticketCards = []string{ "D " + ts + "\n", "J +comment more\\stext\n", "J status Open\n", "K " + h1 + "\n", "U alice\n", } attachmentCards = []string{ "A report.txt " + h1 + " " + h2 + "\n", "C attached\\sa\\sreport\n", "D " + ts + "\n", "N text/plain\n", "U alice\n", } eventCards = []string{ "C a\\stechnote\n", "D " + ts + "\n", "E 2026-01-02T03:04:05 " + h1 + "\n", "N text/x-markdown\n", "P " + h2 + "\n", "T +bgcolor * red\n", "U alice\n", "W 4\nnote\n", } forumCards = []string{ "D " + ts + "\n", "G " + h1 + "\n", "I " + h2 + "\n", "N text/x-markdown\n", "P " + h3 + "\n", "U alice\n", "W 5\nreply\n", } ) // The bases as artifact bodies, without their Z card. var ( baseManifest = strings.Join(manifestCards, "") baseDelta = strings.Join(deltaCards, "") baseCluster = strings.Join(clusterCards, "") baseControl = strings.Join(controlCards, "") baseWiki = strings.Join(wikiCards, "") baseTicket = strings.Join(ticketCards, "") baseAttachment = strings.Join(attachmentCards, "") baseEvent = strings.Join(eventCards, "") baseForum = strings.Join(forumCards, "") ) // artifactBase is one base and the draft's card table for its type // (Section 5.2, Table 4). type artifactBase struct { name string cards []string goType deck.ArtifactType allowed string required string } var artifactBases = []artifactBase{ {"manifest", manifestCards, deck.Checkin, "BCDFNPQRTUZ", "DZ"}, {"delta", deltaCards, deck.Checkin, "BCDFNPQRTUZ", "DZ"}, {"cluster", clusterCards, deck.Cluster, "MZ", "MZ"}, {"control", controlCards, deck.Control, "DTUZ", "DTUZ"}, {"wiki", wikiCards, deck.Wiki, "CDLNPUWZ", "DLUWZ"}, {"ticket", ticketCards, deck.Ticket, "DJKUZ", "DJKUZ"}, {"attachment", attachmentCards, deck.Attachment, "ACDNUZ", "ADZ"}, {"event", eventCards, deck.Event, "CDENPTUWZ", "DEWZ"}, {"forum", forumCards, deck.ForumPost, "DGHINPUWZ", "DUWZ"}, } // edit replaces the one occurrence of old in base. It panics unless old // occurs exactly once, so a case cannot silently edit the wrong place. func edit(base, old, replacement string) string { if n := strings.Count(base, old); n != 1 { panic(fmt.Sprintf("conformance: edit: %q occurs %d times in the base", old, n)) } return strings.Replace(base, old, replacement, 1) } // conforms runs one artifact case through fossil(1), when present, and // deck.Parse. fossil goes first: a case whose expectation fossil // contradicts is wrong whatever go-libfossil does. func (c artifactCase) conforms(ref *fossilReference) error { if ref != nil { if err := c.fossilAgrees(ref); err != nil { return err } } if !c.accept { return c.rejectedByRule() } d, err := deck.Parse(c.input) if err != nil { return expectf("deck.Parse rejected %q: %v", c.input, err) } if c.check != nil { if err := runCheck(c.check, d); err != nil { return err } } if c.nonCanonical { return nil } encoded, err := d.Marshal() if err != nil { return expectf("Marshal: %v", err) } if !bytes.Equal(encoded, c.input) { return expectf("Marshal(Parse(input)) = %q, want the input %q", encoded, c.input) } return nil } // rejectedByRule checks that deck.Parse rejects the input, and that the Z // check did not do it for the rule the case is about. func (c artifactCase) rejectedByRule() error { if !c.zCase { if err := deck.VerifyZ(c.input); err != nil { return expectf("rejected by the Z check, not by the rule: %v", err) } } if _, err := deck.Parse(c.input); err == nil { return expectf("deck.Parse accepted %q, want rejection", c.input) } return nil } // fossilAgrees checks fossil's verdict against the case: the same as accept, // or the opposite when the case says why it departs from fossil. func (c artifactCase) fossilAgrees(ref *fossilReference) error { accepted, reason, err := ref.parseArtifact(c.input) if err != nil { return err } expected := c.accept if c.fossil != "" { expected = !c.accept } if accepted == expected { if !accepted && !strings.Contains(reason, c.fossilSays) { return expectf("fossil(1) rejects for %q, not %q", reason, c.fossilSays) } return nil } if c.fossil != "" { return expectf("fossil(1) now agrees with the case (accepted=%v); remove fossil: %q", accepted, c.fossil) } if accepted { return expectf("fossil(1) accepts the input; the case expects rejection") } return expectf("fossil(1) rejects the input (%s); the case expects acceptance", reason) } func TestArtifactBases(t *testing.T) { ref := newFossilReference(t) for _, b := range artifactBases { t.Run(b.name, func(t *testing.T) { input := withZ(strings.Join(b.cards, "")) if ref != nil { accepted, reason, err := ref.parseArtifact(input) if err != nil || !accepted { t.Fatalf("fossil(1) rejects the %s base: %v %s", b.name, err, reason) } } d, err := deck.Parse(input) if err != nil { t.Fatalf("deck.Parse rejects the %s base: %v", b.name, err) } if d.Type != b.goType { t.Fatalf("deck.Parse typed the %s base %d, want %d", b.name, d.Type, b.goType) } encoded, err := d.Marshal() if err != nil { t.Fatal(err) } if !bytes.Equal(encoded, input) { t.Fatalf("Marshal(Parse(base)) = %q, want %q", encoded, input) } }) } } func TestArtifactParse(t *testing.T) { ref := newFossilReference(t) for _, c := range artifactCases() { check(t, c.Case, func() error { return c.conforms(ref) }) } } // artifactCases is every deck.Parse case: the hand-written tables, then the // ones generated from the bases. func artifactCases() []artifactCase { var all []artifactCase for _, table := range [][]artifactCase{ framingCases, signatureCases, grammarCases, escapeCases, orderCases, pathnameCases(), wikiNameCases(), reservedCases, timestampCases(), cardCases, typeCases(), checksumCases, duplicateCases(), arityCases(), hashCases(), legalityCases(), } { all = append(all, table...) } for i := range all { all[i].Gap = artifactGaps[all[i].ID] } return all } // artifactSuiteCases is what the coverage test audits for this suite. func artifactSuiteCases() []Case { var all []Case for _, c := range artifactCases() { all = append(all, c.Case) } for _, c := range apiCases() { all = append(all, c.Case) } return all } // accepted builds an acceptance case. func accepted(id, req, body string, check func(*deck.Deck) error) artifactCase { c := artifactCase{Case: Case{ID: id, Req: req}, input: withZ(body), accept: true} c.check = check return c } // rejected builds a rejection case. func rejected(id, req, body string) artifactCase { return artifactCase{Case: Case{ID: id, Req: req}, input: withZ(body)} } // loose is accepted for inputs Marshal writes differently. func loose(id, req, body string, check func(*deck.Deck) error) artifactCase { c := accepted(id, req, body, check) c.nonCanonical = true return c } // want compares one parsed field with the value the draft gives it. func want(field string, got, expected any) error { if fmt.Sprint(got) == fmt.Sprint(expected) { return nil } return expectf("%s = %q, want %q", field, fmt.Sprint(got), fmt.Sprint(expected)) } // wantType checks the type deck.Parse assigned. func wantType(expected deck.ArtifactType) func(*deck.Deck) error { return func(d *deck.Deck) error { return want("Type", int(d.Type), int(expected)) } } // runCheck runs a field check, turning an index out of range on a deck // with fewer cards than the draft gives it into a reported problem. func runCheck(check func(*deck.Deck) error, d *deck.Deck) (err error) { defer func() { if r := recover(); r != nil { err = expectf("checking the parsed deck: %v", r) } }() return check(d) }