internal/render/render_test.go
185 lines · 4455 bytes
1package render
2
3import (
4 "errors"
5 "io"
6 "os"
7 "path/filepath"
8 "sync"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "gitbay.org/cmc/gallery/internal/format"
14 "gitbay.org/cmc/gallery/internal/library"
15)
16
17type fakeFormat struct {
18 calls atomic.Int32
19 width atomic.Int32
20 fail bool
21}
22
23func (f *fakeFormat) Name() string { return "fake" }
24func (f *fakeFormat) Match(string) bool { return true }
25func (f *fakeFormat) Kind() format.Kind { return format.KindImage }
26func (f *fakeFormat) Metadata(string) (format.Meta, error) { return format.Meta{}, nil }
27func (f *fakeFormat) Resize(_ string, w int, dst io.Writer) error {
28 f.calls.Add(1)
29 f.width.Store(int32(w))
30 time.Sleep(20 * time.Millisecond)
31 io.WriteString(dst, "partial")
32 if f.fail {
33 return errors.New("magick failed")
34 }
35 io.WriteString(dst, "-done")
36 return nil
37}
38
39func item(f format.Format) *library.Item {
40 return &library.Item{
41 Album: "tokyo", Name: "a.jpg", Path: "/src/a.jpg",
42 ModTime: time.Unix(100, 0), Size: 42,
43 Meta: format.Meta{Width: 3000, Height: 2000},
44 Format: f,
45 }
46}
47
48func TestAllowed(t *testing.T) {
49 for _, w := range Widths {
50 if !Allowed(w) {
51 t.Errorf("%d should be allowed", w)
52 }
53 }
54 for _, w := range []int{0, 1, 961, 4000} {
55 if Allowed(w) {
56 t.Errorf("%d should not be allowed", w)
57 }
58 }
59}
60
61func TestGetGeneratesOnce(t *testing.T) {
62 f := &fakeFormat{}
63 r := New(t.TempDir(), 4)
64 it := item(f)
65
66 var wg sync.WaitGroup
67 for range 10 {
68 wg.Add(1)
69 go func() {
70 defer wg.Done()
71 if _, err := r.Get(it, 960); err != nil {
72 t.Error(err)
73 }
74 }()
75 }
76 wg.Wait()
77 p, err := r.Get(it, 960)
78 if err != nil {
79 t.Fatal(err)
80 }
81 if n := f.calls.Load(); n != 1 {
82 t.Fatalf("Resize calls = %d, want 1", n)
83 }
84 if b, _ := os.ReadFile(p); string(b) != "partial-done" {
85 t.Fatalf("content = %q", b)
86 }
87 if p != r.Path(it, 960) {
88 t.Fatalf("Get path %s != Path %s", p, r.Path(it, 960))
89 }
90}
91
92func TestGetRejectsWidth(t *testing.T) {
93 if _, err := New(t.TempDir(), 1).Get(item(&fakeFormat{}), 961); err == nil {
94 t.Fatal("want error")
95 }
96}
97
98func TestGetClampsToSource(t *testing.T) {
99 f := &fakeFormat{}
100 it := item(f)
101 it.Meta.Width = 700
102 if _, err := New(t.TempDir(), 1).Get(it, 960); err != nil {
103 t.Fatal(err)
104 }
105 if w := f.width.Load(); w != 700 {
106 t.Fatalf("resized to %d, want 700", w)
107 }
108}
109
110func TestGetFailureLeavesNothing(t *testing.T) {
111 f := &fakeFormat{fail: true}
112 r := New(t.TempDir(), 1)
113 it := item(f)
114 if _, err := r.Get(it, 480); err == nil {
115 t.Fatal("want error")
116 }
117 entries, _ := os.ReadDir(filepath.Dir(r.Path(it, 480)))
118 if len(entries) != 0 {
119 t.Fatalf("left behind: %v", entries)
120 }
121 r.Get(it, 480)
122 if n := f.calls.Load(); n != 2 {
123 t.Fatalf("failure was cached: calls = %d", n)
124 }
125}
126
127func TestPathChangesWithSource(t *testing.T) {
128 r := New("/cache", 1)
129 a := item(nil)
130 b := item(nil)
131 b.ModTime = time.Unix(200, 0)
132 if r.Path(a, 480) == r.Path(b, 480) {
133 t.Fatal("path must change with mtime")
134 }
135 want := filepath.Join("/cache", "img", "tokyo", "a.jpg", "100000000000-42", "480.jpg")
136 if got := r.Path(a, 480); got != want {
137 t.Fatalf("Path = %s, want %s", got, want)
138 }
139}
140
141func TestPrune(t *testing.T) {
142 f := &fakeFormat{}
143 r := New(t.TempDir(), 1)
144 cur := item(f)
145 old := item(f)
146 old.ModTime = time.Unix(50, 0)
147 gone := item(f)
148 gone.Name = "gone.jpg"
149 for _, it := range []*library.Item{cur, old, gone} {
150 if _, err := r.Get(it, 480); err != nil {
151 t.Fatal(err)
152 }
153 }
154 past := time.Now().Add(-2 * pruneGrace)
155 for _, it := range []*library.Item{cur, old, gone} {
156 os.Chtimes(filepath.Dir(r.Path(it, 480)), past, past)
157 }
158 lib := &library.Library{Albums: []*library.Album{{Slug: "tokyo", Items: []*library.Item{cur}}}}
159 if err := r.Prune(lib); err != nil {
160 t.Fatal(err)
161 }
162 if _, err := os.Stat(r.Path(cur, 480)); err != nil {
163 t.Fatalf("current derivative removed: %v", err)
164 }
165 for _, it := range []*library.Item{old, gone} {
166 if _, err := os.Stat(filepath.Dir(r.Path(it, 480))); !os.IsNotExist(err) {
167 t.Fatalf("stale dir kept: %s", filepath.Dir(r.Path(it, 480)))
168 }
169 }
170}
171
172func TestPruneKeepsRecentlyModifiedStaleDir(t *testing.T) {
173 f := &fakeFormat{}
174 r := New(t.TempDir(), 1)
175 old := item(f)
176 if _, err := r.Get(old, 480); err != nil {
177 t.Fatal(err)
178 }
179 if err := r.Prune(&library.Library{}); err != nil {
180 t.Fatal(err)
181 }
182 if _, err := os.Stat(r.Path(old, 480)); err != nil {
183 t.Fatalf("recently written stale dir removed: %v", err)
184 }
185}