package render import ( "errors" "io" "os" "path/filepath" "sync" "sync/atomic" "testing" "time" "gitbay.org/cmc/gallery/internal/format" "gitbay.org/cmc/gallery/internal/library" ) type fakeFormat struct { calls atomic.Int32 width atomic.Int32 fail bool } func (f *fakeFormat) Name() string { return "fake" } func (f *fakeFormat) Match(string) bool { return true } func (f *fakeFormat) Kind() format.Kind { return format.KindImage } func (f *fakeFormat) Metadata(string) (format.Meta, error) { return format.Meta{}, nil } func (f *fakeFormat) Resize(_ string, w int, dst io.Writer) error { f.calls.Add(1) f.width.Store(int32(w)) time.Sleep(20 * time.Millisecond) io.WriteString(dst, "partial") if f.fail { return errors.New("magick failed") } io.WriteString(dst, "-done") return nil } func item(f format.Format) *library.Item { return &library.Item{ Album: "tokyo", Name: "a.jpg", Path: "/src/a.jpg", ModTime: time.Unix(100, 0), Size: 42, Meta: format.Meta{Width: 3000, Height: 2000}, Format: f, } } func TestAllowed(t *testing.T) { for _, w := range Widths { if !Allowed(w) { t.Errorf("%d should be allowed", w) } } for _, w := range []int{0, 1, 961, 4000} { if Allowed(w) { t.Errorf("%d should not be allowed", w) } } } func TestGetGeneratesOnce(t *testing.T) { f := &fakeFormat{} r := New(t.TempDir(), 4) it := item(f) var wg sync.WaitGroup for range 10 { wg.Add(1) go func() { defer wg.Done() if _, err := r.Get(it, 960); err != nil { t.Error(err) } }() } wg.Wait() p, err := r.Get(it, 960) if err != nil { t.Fatal(err) } if n := f.calls.Load(); n != 1 { t.Fatalf("Resize calls = %d, want 1", n) } if b, _ := os.ReadFile(p); string(b) != "partial-done" { t.Fatalf("content = %q", b) } if p != r.Path(it, 960) { t.Fatalf("Get path %s != Path %s", p, r.Path(it, 960)) } } func TestGetRejectsWidth(t *testing.T) { if _, err := New(t.TempDir(), 1).Get(item(&fakeFormat{}), 961); err == nil { t.Fatal("want error") } } func TestGetClampsToSource(t *testing.T) { f := &fakeFormat{} it := item(f) it.Meta.Width = 700 if _, err := New(t.TempDir(), 1).Get(it, 960); err != nil { t.Fatal(err) } if w := f.width.Load(); w != 700 { t.Fatalf("resized to %d, want 700", w) } } func TestGetFailureLeavesNothing(t *testing.T) { f := &fakeFormat{fail: true} r := New(t.TempDir(), 1) it := item(f) if _, err := r.Get(it, 480); err == nil { t.Fatal("want error") } entries, _ := os.ReadDir(filepath.Dir(r.Path(it, 480))) if len(entries) != 0 { t.Fatalf("left behind: %v", entries) } r.Get(it, 480) if n := f.calls.Load(); n != 2 { t.Fatalf("failure was cached: calls = %d", n) } } func TestPathChangesWithSource(t *testing.T) { r := New("/cache", 1) a := item(nil) b := item(nil) b.ModTime = time.Unix(200, 0) if r.Path(a, 480) == r.Path(b, 480) { t.Fatal("path must change with mtime") } want := filepath.Join("/cache", "img", "tokyo", "a.jpg", "100000000000-42", "480.jpg") if got := r.Path(a, 480); got != want { t.Fatalf("Path = %s, want %s", got, want) } } func TestPrune(t *testing.T) { f := &fakeFormat{} r := New(t.TempDir(), 1) cur := item(f) old := item(f) old.ModTime = time.Unix(50, 0) gone := item(f) gone.Name = "gone.jpg" for _, it := range []*library.Item{cur, old, gone} { if _, err := r.Get(it, 480); err != nil { t.Fatal(err) } } past := time.Now().Add(-2 * pruneGrace) for _, it := range []*library.Item{cur, old, gone} { os.Chtimes(filepath.Dir(r.Path(it, 480)), past, past) } lib := &library.Library{Albums: []*library.Album{{Slug: "tokyo", Items: []*library.Item{cur}}}} if err := r.Prune(lib); err != nil { t.Fatal(err) } if _, err := os.Stat(r.Path(cur, 480)); err != nil { t.Fatalf("current derivative removed: %v", err) } for _, it := range []*library.Item{old, gone} { if _, err := os.Stat(filepath.Dir(r.Path(it, 480))); !os.IsNotExist(err) { t.Fatalf("stale dir kept: %s", filepath.Dir(r.Path(it, 480))) } } } func TestPruneKeepsRecentlyModifiedStaleDir(t *testing.T) { f := &fakeFormat{} r := New(t.TempDir(), 1) old := item(f) if _, err := r.Get(old, 480); err != nil { t.Fatal(err) } if err := r.Prune(&library.Library{}); err != nil { t.Fatal(err) } if _, err := os.Stat(r.Path(old, 480)); err != nil { t.Fatalf("recently written stale dir removed: %v", err) } }