Artifact 1d9abe9151ab3d28898befad5531a2d892a0b2d8d6de6a92dde791f53d17d7e8:
- File
conformance/artifact_test.go
— part of check-in
[8cc3d7c5e4]
at
2026-10-13 02:23:50
on branch test/conformance-01-drafts
— test(conformance): check the -01 drafts of artifact, repo-state and sync
Re-pin the conformance suite to draft-fossil-artifact-format-01, draft-fossil-repo-state-01 and draft-fossil-sync-protocol-01, copied byte for byte from zit check-in 100351e26c. The delta format has no -01 and stays on -00. The -00 errata are folded into -01 and are no longer pinned; the six -01 errata are pinned for the fossil 2.28 behavior they record. conformance/rfcs/README.md records where each copy came from.
The requirement tables are re-read against -01, and new cases cover the date tag's event time (repo-state ยง4.2 step 6), the Julian-day grammar, a delta manifest naming a delta, dropped reserved F cards, the trailing-punctuation retry, truncated payloads, errors N, a file card after a refused push, the end of a request after a failed login, and the answer to a gimme for private content.
No library behavior changes. Where -01 departs from fossil 2.28 and go-libfossil follows fossil (a cancelled, unparsable or 0 date tag), the cases are marked as deviations. The non-conformances the re-pin exposes are marked as gaps, tracked in #370, #371, #372, #373, #374 and #375. (user: [email protected], size: 11100) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "bytes" "fmt" "strings" "testing" "github.com/danmestas/go-libfossil/internal/deck" ) // The artifact suite checks draft-fossil-artifact-format-01. 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) }