package main import ( "bytes" "image" "image/jpeg" "os" "path/filepath" "testing" ) // testPhoto returns a JPEG-decoded (so *image.YCbCr, 4:2:0) image with an odd // size and a non-zero origin to exercise the plane offset arithmetic. func testPhoto(t testing.TB, w, h int) *image.YCbCr { t.Helper() src := image.NewRGBA(image.Rect(0, 0, w, h)) for i := range src.Pix { src.Pix[i] = uint8(i*7 + i/w) } var buf bytes.Buffer if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: 90}); err != nil { t.Fatal(err) } img, err := jpeg.Decode(&buf) if err != nil { t.Fatal(err) } return img.(*image.YCbCr).SubImage(image.Rect(3, 5, w, h)).(*image.YCbCr) } func TestResizeBoxYCbCrMatchesGeneric(t *testing.T) { src := testPhoto(t, 1001, 667) fast := resizeBox(src, 120).(*image.RGBA) // Hide the concrete type so resizeBox takes the generic At() path. slow := resizeBox(struct{ image.Image }{src}, 120).(*image.RGBA) if fast.Bounds() != slow.Bounds() { t.Fatalf("bounds %v != %v", fast.Bounds(), slow.Bounds()) } for i := range fast.Pix { if fast.Pix[i] != slow.Pix[i] { t.Fatalf("pixel byte %d: fast %d, generic %d", i, fast.Pix[i], slow.Pix[i]) } } } func BenchmarkResizeBox(b *testing.B) { src := testPhoto(b, 4000, 3000) b.Run("ycbcr", func(b *testing.B) { for i := 0; i < b.N; i++ { resizeBox(src, 300) } }) b.Run("generic", func(b *testing.B) { for i := 0; i < b.N; i++ { resizeBox(struct{ image.Image }{src}, 300) } }) } // After a restart (empty memory cache) the digest comes from the on-disk index // without reading the source file. func TestSourceDigestPersists(t *testing.T) { old := thumbCacheDir thumbCacheDir = t.TempDir() t.Cleanup(func() { thumbCacheDir = old }) src := filepath.Join(t.TempDir(), "a.jpg") if err := os.WriteFile(src, []byte("photo"), 0644); err != nil { t.Fatal(err) } info, _ := os.Stat(src) h1, data, err := sourceDigest(src, info) if err != nil || data == nil { t.Fatalf("first call: %v, data=%v", err, data) } digestCacheMu.Lock() delete(digestCache, src) digestCacheMu.Unlock() // Removing the source proves the second call does not read it again. if err := os.Remove(src); err != nil { t.Fatal(err) } h2, data, err := sourceDigest(src, info) if err != nil || data != nil || h2 != h1 { t.Fatalf("after restart: h=%s data=%v err=%v, want %s from index", h2, data, err, h1) } }