Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · metadata/fuzz_test.go

Line attribution on main · 145 line(s)

d9f5a930
Keenan Nemetz
10d ago
1 package metadata
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 // The paths a fuzzed document arrives at. They have to satisfy the tree shape the
9 // loader expects, so only the file contents move.
10 const (
11 fuzzIssuePath = IssuesDir + "/01M5XCJ4VYKJXTBJXC387WE922/" + issueName
12 fuzzCommentPath = IssuesDir + "/01M5XCJ4VYKJXTBJXC387WE922/comments/" + "01M5XCHRQFT0FTM0CJYRYWVD3V" + ".md"
13 )
14
15 // FuzzParseIssue drives the reader that turns repository bytes into an issue. A
16 // push decides what it sees, so a panic here is an outage and a disagreement
17 // between reading and writing is worse: the server rewrites the file on every
18 // edit, and an issue it can read but not write back loses whatever it could not
19 // express.
20 //
21 // The property is therefore a round trip on the accepted inputs only. Refusing is
22 // always allowed; accepting and then failing to write is not.
23 func FuzzParseIssue(f *testing.F) {
24 for _, seed := range []string{
25 "---\ntitle: An example\nauthor: keenan\ncreated: 2026-10-30T00:00:00Z\nstate: open\n---\nA body.\n",
26 "---\ntitle: An example\nauthor: keenan\ncreated: 2026-10-30T00:00:00Z\nlabels:\n - bug\n---\n",
27 // A body that starts like frontmatter is the shape that breaks a naive
28 // reader, and a title holding a newline is the shape that breaks a writer.
29 "---\ntitle: T\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n---\ntitle: inner\n---\n",
30 "---\ntitle: 'line one\n line two'\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n",
31 "---\ntitle: T\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\r\nbody\r\n",
32 "---\ntitle: T\nauthor: a\ncreated: 2026-10-30T00:00:00Z\nstate: wedged\n---\n",
33 "---\ntitle: T\ntitle: again\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n",
34 "---\ntitle: T\nauthor: a\ncreated: not-a-time\n---\n",
35 "---\n---\n",
36 "",
37 "no frontmatter at all\n",
38 "---\ntitle: T\nauthor: a\ncreated: 2026-10-30T00:00:00Z\nlabels: [a, a, a]\n---\nbody\x00with a nul\n",
39 "---\ntitle: \xff\xfe\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n",
40 } {
41 f.Add(seed)
42 }
43 f.Fuzz(func(t *testing.T, raw string) {
44 issues, err := ParseIssues(map[string]string{fuzzIssuePath: raw})
45 if err != nil {
46 return
47 }
48 if len(issues) != 1 {
49 t.Fatalf("one issue file gave %d issues", len(issues))
50 }
51 iss := issues[0]
52 rendered, err := renderIssue(iss)
53 if err != nil {
54 t.Fatalf("an accepted issue does not render: %v", err)
55 }
56 again, err := ParseIssues(map[string]string{fuzzIssuePath: string(rendered)})
57 if err != nil {
58 t.Fatalf("the issue written by renderIssue is refused: %v\n%s", err, rendered)
59 }
60 if len(again) != 1 {
61 t.Fatalf("the issue written by renderIssue loaded as %d issues", len(again))
62 }
63 got := again[0]
64 if got.Title != iss.Title || got.State != iss.State || got.Author != iss.Author {
65 t.Fatalf("%q: the round trip changed the header: %+v then %+v", raw, iss, got)
66 }
67 if !got.Created.Equal(iss.Created) {
68 t.Fatalf("%q: created became %s, was %s", raw, got.Created, iss.Created)
69 }
70 // The writer trims the trailing newlines of a body, so the comparison is on
71 // the trimmed form: what matters is that no other byte moves.
72 if body := strings.TrimRight(got.Description, "\n"); body != strings.TrimRight(iss.Description, "\n") {
73 t.Fatalf("%q: the round trip changed the body: %q then %q", raw, iss.Description, got.Description)
74 }
75 if strings.Join(got.Labels, ",") != strings.Join(iss.Labels, ",") {
76 t.Fatalf("%q: labels became %v, were %v", raw, got.Labels, iss.Labels)
77 }
78 // Writing an issue twice has to be a fixed point, or every edit rewrites the
79 // file and the diff shows work nobody did.
80 renderedAgain, err := renderIssue(got)
81 if err != nil {
82 t.Fatalf("the second write failed: %v", err)
83 }
84 if string(renderedAgain) != string(rendered) {
85 t.Fatalf("%q: writing twice differed:\nfirst:\n%s\nsecond:\n%s", raw, rendered, renderedAgain)
86 }
87 })
88 }
89
90 // FuzzParseComment is the same property for a comment, which is edited far more
91 // often than an issue body and holds code blocks, quoted frontmatter, and
92 // whatever a person pastes.
93 func FuzzParseComment(f *testing.F) {
94 for _, seed := range []string{
95 "---\nauthor: keenan\ncreated: 2026-10-30T00:00:00Z\n---\nA reply.\n",
96 "---\nauthor: keenan\ncreated: 2026-10-30T00:00:00Z\nreply_to: 01M5XCHRQFT0FTM0CJYRYWVD3V\n---\nA reply.\n",
97 "---\nauthor: keenan\ncreated: 2026-10-30T00:00:00Z\ndeleted: true\n---\n",
98 "---\nauthor: keenan\ncreated: 2026-10-30T00:00:00Z\npath: a/b.go\nside: right\nline: 12\nblob: 6beaa53\n---\nx\n",
99 // A body that looks like frontmatter, and one that ends where the writer
100 // puts its delimiter.
101 "---\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n---\nauthor: b\ncreated: 2026-10-30T00:00:00Z\n---\n",
102 "---\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n```\n---\n```\n",
103 "---\nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n",
104 "---\nauthor: \nauthor: a\ncreated: 2026-10-30T00:00:00Z\n---\n",
105 "---\nauthor: a\ncreated: 2026-10-30T00:00:00Z\nline: not a number\n---\n",
106 "",
107 "---\n---\n",
108 } {
109 f.Add(seed)
110 }
111 f.Fuzz(func(t *testing.T, raw string) {
112 c, problem := ParseComment("01M5XCHRQFT0FTM0CJYRYWVD3V", raw, fuzzCommentPath)
113 if problem != "" {
114 return
115 }
116 rendered, err := RenderComment(&c)
117 if err != nil {
118 t.Fatalf("an accepted comment does not render: %v", err)
119 }
120 again, problem := ParseComment(c.ID, string(rendered), fuzzCommentPath)
121 if problem != "" {
122 t.Fatalf("the comment written by RenderComment is refused: %s\n%s", problem, rendered)
123 }
124 if again.Author != c.Author || again.ReplyTo != c.ReplyTo || again.Deleted != c.Deleted {
125 t.Fatalf("%q: the round trip changed the header: %+v then %+v", raw, c, again)
126 }
127 // A file the server wrote ends with a newline, and Split keeps it in the body.
128 // A hand-written file that ends without one is therefore normalized on the
129 // first write: the body gains nothing but that newline, which is why the
130 // comparison is on the trimmed form. Every caller trims before it writes, so
131 // this is the shape of the format, not a difference anyone sees.
132 if body := strings.TrimRight(again.Body, "\n"); body != strings.TrimRight(c.Body, "\n") {
133 t.Fatalf("%q: the body became %q, was %q", raw, again.Body, c.Body)
134 }
135 // The fixed point is the real invariant: once the file has been written by the
136 // server, reading and writing it again must not change a byte.
137 renderedAgain, err := RenderComment(&again)
138 if err != nil {
139 t.Fatalf("the second write failed: %v", err)
140 }
141 if string(renderedAgain) != string(rendered) {
142 t.Fatalf("%q: writing twice differed:\nfirst:\n%s\nsecond:\n%s", raw, rendered, renderedAgain)
143 }
144 })
145 }