internal/httpd/diff_test.go
174 lines · 4552 bytes
1package httpd
2
3import (
4 "strings"
5 "testing"
6)
7
8const samplePatch = `diff --git a/main.go b/main.go
9index 1234567..89abcde 100644
10--- a/main.go
11+++ b/main.go
12@@ -1,6 +1,7 @@
13 package main
14
15-func old() string {
16- return "a"
17+func replaced() string {
18+ // a comment
19+ return "b"
20 }
21diff --git a/notes.txt b/README.md
22similarity index 60%
23rename from notes.txt
24rename to README.md
25--- a/notes.txt
26+++ b/README.md
27@@ -1 +1 @@
28-old title
29+new title
30diff --git a/logo.png b/logo.png
31new file mode 100644
32index 0000000..1111111
33Binary files /dev/null and b/logo.png differ
34`
35
36func TestParseDiffFiles(t *testing.T) {
37 files := parseDiff(samplePatch)
38 if len(files) != 3 {
39 t.Fatalf("got %d files, want 3", len(files))
40 }
41
42 if got := files[0].Path; got != "main.go" {
43 t.Errorf("file 0 path = %q", got)
44 }
45 if files[0].Adds != 3 || files[0].Dels != 2 {
46 t.Errorf("main.go stat = +%d -%d, want +3 -2", files[0].Adds, files[0].Dels)
47 }
48 if files[0].Status != "modified" {
49 t.Errorf("main.go status = %q", files[0].Status)
50 }
51
52 if files[1].Status != "renamed" || files[1].OldPath != "notes.txt" || files[1].Path != "README.md" {
53 t.Errorf("rename = %q %q -> %q", files[1].Status, files[1].OldPath, files[1].Path)
54 }
55
56 if !files[2].Binary || files[2].Status != "added" || files[2].Path != "logo.png" {
57 t.Errorf("binary add = %+v", files[2])
58 }
59 if len(files[2].Lines) != 0 {
60 t.Errorf("binary file has %d lines, want none", len(files[2].Lines))
61 }
62
63 if st := statOf(files); st.Files != 3 || st.Adds != 4 || st.Dels != 3 {
64 t.Errorf("stat = %+v, want 3 files +4 -3", st)
65 }
66}
67
68// Line numbers anchor review threads, so an off-by-one here silently moves
69// every comment on a merge request.
70func TestParseDiffLineNumbers(t *testing.T) {
71 lines := parseDiff(samplePatch)[0].Lines
72 type want struct {
73 class string
74 old, new int64
75 content string
76 }
77 wants := []want{
78 {"hunk", 0, 0, ""},
79 {"ctx", 1, 1, "package main"},
80 {"ctx", 2, 2, ""},
81 {"del", 3, 0, "func old() string {"},
82 {"del", 4, 0, "\treturn \"a\""},
83 {"add", 0, 3, "func replaced() string {"},
84 {"add", 0, 4, "\t// a comment"},
85 {"add", 0, 5, "\treturn \"b\""},
86 {"ctx", 5, 6, "}"},
87 }
88 if len(lines) != len(wants) {
89 t.Fatalf("got %d lines, want %d: %+v", len(lines), len(wants), lines)
90 }
91 for i, w := range wants {
92 got := lines[i]
93 if got.Class != w.class || got.OldLine != w.old || got.NewLine != w.new {
94 t.Errorf("line %d = %s old=%d new=%d, want %s old=%d new=%d",
95 i, got.Class, got.OldLine, got.NewLine, w.class, w.old, w.new)
96 }
97 if w.class != "hunk" && got.Content != w.content {
98 t.Errorf("line %d content = %q, want %q", i, got.Content, w.content)
99 }
100 }
101}
102
103// Highlighting runs per hunk side and is mapped back line by line; the
104// mapping is what breaks, so check that every code line got markup and that
105// it still says what the source said.
106func TestParseDiffHighlighting(t *testing.T) {
107 for _, l := range parseDiff(samplePatch)[0].Lines {
108 if l.Class == "hunk" || l.Content == "" {
109 continue
110 }
111 if l.Code == "" {
112 t.Errorf("%s line %q got no highlighted markup", l.Class, l.Content)
113 continue
114 }
115 if text := strings.TrimSpace(stripTags(string(l.Code))); text != strings.TrimSpace(l.Content) {
116 t.Errorf("highlighted %q reads as %q", l.Content, text)
117 }
118 }
119}
120
121// A file whose type chroma does not know renders as plain text rather than
122// being guessed at.
123func TestParseDiffUnknownType(t *testing.T) {
124 files := parseDiff(`diff --git a/x.zzz b/x.zzz
125--- a/x.zzz
126+++ b/x.zzz
127@@ -1 +1 @@
128-before
129+after
130`)
131 if len(files) != 1 {
132 t.Fatalf("got %d files", len(files))
133 }
134 for _, l := range files[0].Lines {
135 if l.Class == "hunk" {
136 continue
137 }
138 if l.Code != "" {
139 t.Errorf("unknown type got markup: %q", l.Code)
140 }
141 }
142}
143
144// splitHighlighted has to close and reopen spans that straddle a newline,
145// or one unterminated tag swallows the rest of the file.
146func TestSplitHighlightedBalancesTags(t *testing.T) {
147 got := splitHighlighted(`<span class="c">line one
148line two</span>plain`)
149 want := []string{`<span class="c">line one</span>`, `<span class="c">line two</span>plain`}
150 if len(got) != len(want) {
151 t.Fatalf("got %d lines: %q", len(got), got)
152 }
153 for i := range want {
154 if got[i] != want[i] {
155 t.Errorf("line %d = %q, want %q", i, got[i], want[i])
156 }
157 }
158}
159
160func stripTags(s string) string {
161 var b strings.Builder
162 depth := 0
163 for _, r := range s {
164 switch {
165 case r == '<':
166 depth++
167 case r == '>':
168 depth--
169 case depth == 0:
170 b.WriteRune(r)
171 }
172 }
173 return strings.ReplaceAll(b.String(), """, `"`)
174}