| @@ -0,0 +1,166 @@ |
| 1 | package render |
| 2 | |
| 3 | import ( |
| 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 | |
| 17 | type fakeFormat struct { |
| 18 | calls atomic.Int32 |
| 19 | width atomic.Int32 |
| 20 | fail bool |
| 21 | } |
| 22 | |
| 23 | func (f *fakeFormat) Name() string { return "fake" } |
| 24 | func (f *fakeFormat) Match(string) bool { return true } |
| 25 | func (f *fakeFormat) Kind() format.Kind { return format.KindImage } |
| 26 | func (f *fakeFormat) Metadata(string) (format.Meta, error) { return format.Meta{}, nil } |
| 27 | func (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 | |
| 39 | func 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 | |
| 48 | func 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 | |
| 61 | func 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 | |
| 92 | func 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 | |
| 98 | func 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 | |
| 110 | func 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 | |
| 127 | func 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 | |
| 141 | func 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 | lib := &library.Library{Albums: []*library.Album{{Slug: "tokyo", Items: []*library.Item{cur}}}} |
| 155 | if err := r.Prune(lib); err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | if _, err := os.Stat(r.Path(cur, 480)); err != nil { |
| 159 | t.Fatalf("current derivative removed: %v", err) |
| 160 | } |
| 161 | for _, it := range []*library.Item{old, gone} { |
| 162 | if _, err := os.Stat(filepath.Dir(r.Path(it, 480))); !os.IsNotExist(err) { |
| 163 | t.Fatalf("stale dir kept: %s", filepath.Dir(r.Path(it, 480))) |
| 164 | } |
| 165 | } |
| 166 | } |