Artifact c72caf3d567742aa22741c273128e3a994f105dfad6b02021ced9c9d77a96648:
- File
conformance/harness_self_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: 5442) [annotate] [blame] [check-ins using]
package conformance import ( "errors" "os" "path/filepath" "strings" "testing" ) // The harness decides what a run means, so its own rules are tested here: // how a marked case is reported, how tables are read and audited, and how // a requirement's status is derived from its cases. func TestJudge(t *testing.T) { problem := errors.New("rejected") tests := []struct { name string c Case err error want outcome }{ {"conforms", Case{ID: "x.1"}, nil, outcomePass}, {"fails", Case{ID: "x.1"}, problem, outcomeFail}, {"known gap still failing", Case{ID: "x.1", Gap: "#1"}, problem, outcomeSkip}, {"known gap now conforming", Case{ID: "x.1", Gap: "#1"}, nil, outcomeFail}, {"deviation still present", Case{ID: "x.1", Deviation: "why"}, problem, outcomeSkip}, {"deviation gone", Case{ID: "x.1", Deviation: "why"}, nil, outcomeFail}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, message := judge(tt.c, tt.err) if got != tt.want { t.Fatalf("judge = %d (%q), want %d", got, message, tt.want) } if got != outcomePass && !strings.Contains(message, tt.c.ID) { t.Fatalf("message %q does not name the case", message) } }) } } func TestJudgeRefusesDoubleMark(t *testing.T) { defer func() { if recover() == nil { t.Fatal("a case marked both Gap and Deviation was accepted") } }() judge(Case{ID: "x.1", Gap: "#1", Deviation: "why"}, nil) } func TestParseTable(t *testing.T) { data := "# a comment\n# draft\td.txt\tabc\n# erratum\te.md\tdef\n\n" + "R-1\t3.1\tNames are hashes.\n" + "R-2\t3.2\tLengths are fixed.\tonly SHA1 checked\n" tbl, err := parseTable([]byte(data)) if err != nil { t.Fatal(err) } if len(tbl.sources) != 2 || tbl.sources[1].Path != "erratum/e.md" { t.Fatalf("sources = %+v", tbl.sources) } if len(tbl.requirements) != 2 || tbl.requirements[1].Scope != "only SHA1 checked" { t.Fatalf("requirements = %+v", tbl.requirements) } for _, bad := range []string{ "R-1\t3.1\tNo pin.\n", "# draft\td.txt\tabc\nR-1\t3.1\n", "# draft\td.txt\nR-1\t3.1\tx\n", "# draft\td.txt\tabc\nR-1\t\tx\n", } { if _, err := parseTable([]byte(bad)); err == nil { t.Errorf("parseTable accepted %q", bad) } } } func TestVerifySources(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "d.txt"), []byte("draft\n"), 0o644); err != nil { t.Fatal(err) } // SHA-256 of "draft\n". good := table{sources: []source{{Path: "d.txt", SHA256: "7eb2ca55b87a4d45d66a63f76db11f9b4aa9106472a62b5865060f9fd8eadaaa"}}} if problems := verifySources(good, dir); len(problems) != 0 { t.Fatalf("a correct pin gave %v", problems) } stale := table{sources: []source{{Path: "d.txt", SHA256: "00"}}} problems := verifySources(stale, dir) if len(problems) != 1 || !strings.Contains(problems[0].Error(), "move the pin") { t.Fatalf("a stale pin gave %v", problems) } missing := table{sources: []source{{Path: "absent.txt", SHA256: "00"}}} if problems := verifySources(missing, dir); len(problems) != 1 { t.Fatalf("a missing draft gave %v", problems) } } func TestAudit(t *testing.T) { tbl := table{requirements: []requirement{{ID: "R-1"}, {ID: "R-1"}}} s := suite{name: "x", cases: []Case{ {ID: "x.a", Req: "R-1"}, {ID: "x.a", Req: "R-1"}, {ID: "y.b", Req: "R-1"}, {ID: "x.c", Req: "R-9"}, {ID: "x.d", Req: "R-1", Gap: "270"}, {ID: "x.e", Req: "R-1", Gap: "#0"}, }} problems := audit(s, tbl) for _, want := range []string{"R-1 listed twice", "x.a listed twice", `y.b does not start`, `unknown requirement "R-9"`, "must be an issue reference", "Gap #0 is a placeholder"} { found := false for _, p := range problems { found = found || strings.Contains(p.Error(), want) } if !found { t.Errorf("audit missed %q in %v", want, problems) } } } func TestAssess(t *testing.T) { row := requirement{ID: "R-1"} scoped := requirement{ID: "R-1", Scope: "SHA1 only"} tests := []struct { name string row requirement cases []Case want status }{ {"no cases", row, nil, statusGap}, {"all conform", row, []Case{{ID: "a"}}, statusCovered}, {"scoped", scoped, []Case{{ID: "a"}}, statusPartial}, {"deviation", scoped, []Case{{ID: "a", Deviation: "why"}}, statusDeviation}, {"failing outranks deviation", row, []Case{{ID: "a", Deviation: "why"}, {ID: "b", Gap: "#1"}}, statusFailing}, } for _, tt := range tests { if got, _ := assess(tt.row, tt.cases); got != tt.want { t.Errorf("%s: assess = %s, want %s", tt.name, got, tt.want) } } } func TestWithZ(t *testing.T) { // The Z card fossil computed for this body (fossil test-parse-manifest // reports "expected 74f3096fa00a0995e85bb466be787372" for it). got := string(withZ("C x\nD 2026-01-02T03:04:05\n")) if want := "C x\nD 2026-01-02T03:04:05\nZ 74f3096fa00a0995e85bb466be787372\n"; got != want { t.Fatalf("withZ = %q, want %q", got, want) } } func TestFossilReference(t *testing.T) { ref := newFossilReference(t) if ref == nil { t.Skip("fossil not found") } accepted, reason, err := ref.parseArtifact(withZ("C x\nD 2026-01-02T03:04:05\n")) if err != nil || !accepted { t.Fatalf("fossil rejected a valid control artifact: %v %q", err, reason) } accepted, reason, err = ref.parseArtifact(withZ("D 2026-01-02T03:04:05\nC x\n")) if err != nil || accepted || reason == "" { t.Fatalf("fossil verdict on misordered cards: accepted=%v reason=%q err=%v", accepted, reason, err) } }