internal/render/render_test.go

main
gallery/internal/render/render_test.go history · blame · raw

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}