vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
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 | } |