v0.33.2 57f21d6

converter_test.go RawHistoryFolder

1package favicon
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"image"
8	"image/color"
9	"image/png"
10	"testing"
11
12	appport "github.com/bnema/dumber/internal/application/port"
13)
14
15func TestConverterPNGNormalizesAndSizes(t *testing.T) {
16	conv := NewImageConverter()
17	out, err := conv.Convert(context.Background(), testPNG(t, 64, 48), "image/png", []int{32})
18	if err != nil {
19		t.Fatal(err)
20	}
21	if _, decodeErr := png.Decode(bytes.NewReader(out.PNG)); decodeErr != nil {
22		t.Fatalf("normalized png invalid: %v", decodeErr)
23	}
24	img, err := png.Decode(bytes.NewReader(out.SizedPNG[32]))
25	if err != nil {
26		t.Fatal(err)
27	}
28	if img.Bounds().Dx() != 32 || img.Bounds().Dy() != 32 {
29		t.Fatalf("size = %v", img.Bounds())
30	}
31}
32
33func TestConverterAcceptedFaviconFormatsProducePNGs(t *testing.T) {
34	conv := NewImageConverter()
35	for _, tc := range []struct {
36		name, ct string
37		data     []byte
38	}{
39		{"png", "image/png", testPNG(t, 16, 16)},
40		{"ico with png payload", "image/x-icon", testICOWithPNG(t, testPNG(t, 16, 16))},
41		{"ico with bitmap payload", "image/x-icon", testICOWithDIB(t)},
42	} {
43		t.Run(tc.name, func(t *testing.T) {
44			out, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32})
45			if err != nil {
46				t.Fatal(err)
47			}
48			if _, decodeErr := png.Decode(bytes.NewReader(out.PNG)); decodeErr != nil {
49				t.Fatalf("normalized png invalid: %v", decodeErr)
50			}
51			img, err := png.Decode(bytes.NewReader(out.SizedPNG[32]))
52			if err != nil {
53				t.Fatal(err)
54			}
55			if img.Bounds().Dx() != 32 || img.Bounds().Dy() != 32 {
56				t.Fatalf("size = %v", img.Bounds())
57			}
58		})
59	}
60}
61
62func TestConverterRejectsFormatsThatWouldProducePlaceholders(t *testing.T) {
63	conv := NewImageConverter()
64	for _, tc := range []struct {
65		name, ct string
66		data     []byte
67	}{
68		{"webp", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
69		{"empty svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"></svg>`)},
70	} {
71		t.Run(tc.name, func(t *testing.T) {
72			if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
73				t.Fatalf("err = %v, want favicon miss", err)
74			}
75		})
76	}
77}
78
79func TestConverterRasterizesSVG(t *testing.T) {
80	conv := NewImageConverter()
81	svg := []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
82  <rect x="0" y="0" width="32" height="16" fill="#ff0000"/>
83  <rect x="0" y="16" width="32" height="16" fill="#0000ff"/>
84</svg>`)
85	out, err := conv.Convert(context.Background(), svg, "image/svg+xml", []int{32})
86	if err != nil {
87		t.Fatal(err)
88	}
89	img, err := png.Decode(bytes.NewReader(out.SizedPNG[32]))
90	if err != nil {
91		t.Fatal(err)
92	}
93	if img.Bounds().Dx() != 32 || img.Bounds().Dy() != 32 {
94		t.Fatalf("size = %v", img.Bounds())
95	}
96	r, _, b, a := img.At(16, 4).RGBA()
97	if a == 0 || r>>8 < 200 || b>>8 > 50 {
98		t.Fatalf("top pixel = r%d b%d a%d, want red", r>>8, b>>8, a>>8)
99	}
100	r, _, b, a = img.At(16, 28).RGBA()
101	if a == 0 || b>>8 < 200 || r>>8 > 50 {
102		t.Fatalf("bottom pixel = r%d b%d a%d, want blue", r>>8, b>>8, a>>8)
103	}
104}
105
106func TestConverterRejectsRecursiveSVGUse(t *testing.T) {
107	conv := NewImageConverter()
108	for _, tc := range []struct{ name, svg string }{
109		{"self cycle", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><g id="a"><use href="#a"/></g></defs><use href="#a"/></svg>`},
110		{"mutual cycle", `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><g id="a"><use xlink:href="#b"/></g><g id="b"><use xlink:href="#a"/></g></defs><use xlink:href="#a"/></svg>`},
111		{"use targets use", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><use id="a" href="#a"/></defs><use href="#a"/></svg>`},
112		{"flat def run after sibling id", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><g id="a"></g><use href="#a"/></defs><use href="#a"/></svg>`},
113		{"anonymous def", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><use href="#"/></defs><use href="#"/></svg>`},
114	} {
115		t.Run(tc.name, func(t *testing.T) {
116			if _, err := conv.Convert(context.Background(), []byte(tc.svg), "image/svg+xml", []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
117				t.Fatalf("err = %v, want favicon miss", err)
118			}
119		})
120	}
121}
122
123func TestConverterRendersFlatSVGUse(t *testing.T) {
124	svg := []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><rect id="r" width="16" height="16" fill="#00ff00"/></defs><use href="#r"/></svg>`)
125	if _, err := NewImageConverter().Convert(context.Background(), svg, "image/svg+xml", []int{32}); err != nil {
126		t.Fatal(err)
127	}
128}
129
130func TestConverterMalformedInputsAreMisses(t *testing.T) {
131	conv := NewImageConverter()
132	for _, tc := range []struct {
133		name, ct string
134		data     []byte
135	}{
136		{"ico", "image/x-icon", []byte{0, 0, 1, 0}},
137		{"svg", "image/svg+xml", []byte(`not svg`)},
138		{"webp", "image/webp", []byte("not webp")},
139		{"malformed", "image/png", []byte("not an image")},
140	} {
141		t.Run(tc.name, func(t *testing.T) {
142			if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
143				t.Fatalf("err = %v", err)
144			}
145		})
146	}
147}
148
149func testICOWithDIB(t *testing.T) []byte {
150	t.Helper()
151	const width, height, bitCount = 2, 2, 32
152	var dib bytes.Buffer
153	writeLE32 := func(v uint32) { dib.Write([]byte{byte(v), byte(v >> 8), byte(v >> 16), byte(v >> 24)}) }
154	writeLE16 := func(v uint16) { dib.Write([]byte{byte(v), byte(v >> 8)}) }
155	writeLE32(40)                 // BITMAPINFOHEADER size
156	writeLE32(width)              // width
157	writeLE32(height * 2)         // xor + and mask height
158	writeLE16(1)                  // planes
159	writeLE16(bitCount)           // bit count
160	writeLE32(0)                  // BI_RGB
161	writeLE32(width * height * 4) // image size
162	writeLE32(0)
163	writeLE32(0) // pels per meter
164	writeLE32(0)
165	writeLE32(0)                                      // colors
166	dib.Write([]byte{0, 0, 255, 255, 0, 255, 0, 255}) // bottom row BGRA
167	dib.Write([]byte{255, 0, 0, 255, 0, 0, 0, 255})   // top row BGRA
168	return testICOWithPayload(t, dib.Bytes(), bitCount)
169}
170
171func testICOWithPNG(t *testing.T, pngData []byte) []byte {
172	t.Helper()
173	return testICOWithPayload(t, pngData, 32)
174}
175
176func testICOWithPayload(t *testing.T, payload []byte, bitCount byte) []byte {
177	t.Helper()
178	buf := bytes.NewBuffer(nil)
179	buf.Write([]byte{0, 0, 1, 0, 1, 0})  // reserved, type icon, count
180	buf.Write([]byte{16, 16, 0, 0})      // width, height, colors, reserved
181	buf.Write([]byte{1, 0, bitCount, 0}) // planes, bit count
182	size := uint32(len(payload))
183	offset := uint32(6 + 16)
184	buf.Write([]byte{byte(size), byte(size >> 8), byte(size >> 16), byte(size >> 24)})
185	buf.Write([]byte{byte(offset), byte(offset >> 8), byte(offset >> 16), byte(offset >> 24)})
186	buf.Write(payload)
187	return buf.Bytes()
188}
189
190func testPNG(t *testing.T, w, h int) []byte {
191	t.Helper()
192	img := image.NewRGBA(image.Rect(0, 0, w, h))
193	for y := range h {
194		for x := range w {
195			img.Set(x, y, color.RGBA{R: 200, A: 255})
196		}
197	}
198	var buf bytes.Buffer
199	if err := png.Encode(&buf, img); err != nil {
200		t.Fatal(err)
201	}
202	return buf.Bytes()
203}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close