package thumbs import ( "bytes" "image" "image/color" "image/jpeg" "image/png" "io/fs" "os" "path/filepath" "strings" "testing" "time" ) func makePNG(t *testing.T, w, h int, transparent bool) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { for x := 0; x < w; x++ { // 左红右绿两半,方便验证盒均值没把通道混错 c := color.RGBA{255, 0, 0, 255} if x >= w/2 { c = color.RGBA{0, 255, 0, 255} } if transparent { c = color.RGBA{0, 0, 0, 0} } img.Set(x, y, c) } } var b bytes.Buffer if err := png.Encode(&b, img); err != nil { t.Fatal(err) } return b.Bytes() } func TestGenerateDownscale(t *testing.T) { src := makePNG(t, 800, 400, false) out, mime, pass, err := Generate(src, "image/png", 400) if err != nil { t.Fatal(err) } if pass { t.Fatal("800px 图在 w=400 不应直通") } if mime != "image/jpeg" { t.Fatalf("mime=%s", mime) } cfg, _, err := image.DecodeConfig(bytes.NewReader(out)) if err != nil { t.Fatal(err) } if cfg.Width != 400 || cfg.Height != 200 { t.Fatalf("got %dx%d, want 400x200", cfg.Width, cfg.Height) } img, err := jpeg.Decode(bytes.NewReader(out)) if err != nil { t.Fatal(err) } // 盒均值后左右两半的中心仍是红/绿(容差给 JPEG 质量 82 留余量) r, g, _, _ := img.At(100, 100).RGBA() if r>>8 < 200 || g>>8 > 60 { t.Fatalf("左半应偏红: r=%d g=%d", r>>8, g>>8) } r, g, _, _ = img.At(300, 100).RGBA() if g>>8 < 200 || r>>8 > 60 { t.Fatalf("右半应偏绿: r=%d g=%d", r>>8, g>>8) } } func TestGeneratePassThrough(t *testing.T) { src := makePNG(t, 300, 150, false) out, mime, pass, err := Generate(src, "image/png", 960) if err != nil { t.Fatal(err) } if !pass || mime != "image/png" || !bytes.Equal(out, src) { t.Fatalf("小图应原样直通 pass=%v mime=%s", pass, mime) } } func TestGenerateRejectsGarbage(t *testing.T) { if _, _, _, err := Generate([]byte("not an image"), "image/jpeg", 400); err == nil { t.Fatal("垃圾字节应报错") } } func TestFlattenAlphaUnpremultiply(t *testing.T) { // 50% 透明的纯红:alpha=128 预乘 R=128,还原应为 R≈255 img := image.NewNRGBA(image.Rect(0, 0, 1, 1)) img.Set(0, 0, color.NRGBA{255, 0, 0, 128}) flat := flatten(img, img.Bounds()) if flat[0] < 240 || flat[3] != 128 { t.Fatalf("unpremultiply wrong: r=%d a=%d", flat[0], flat[3]) } } func TestSweepEvictsOldest(t *testing.T) { s := NewStore(t.TempDir()) s.MaxBytes = 2500 // 每个文件 1000B,留得下 2 个 sha := func(i int) string { return strings.Repeat(string(rune('a'+i)), 64) } // 三个宽度:写入顺序即 mtime 顺序(下面再显式拨时间) for i := 0; i < 3; i++ { if _, err := s.Put(sha(i), 480, bytes.Repeat([]byte("x"), 1000), "image/jpeg"); err != nil { t.Fatal(err) } } // 跳过节流,让这次 Put 真的触发扫描 s.lastSweep = time.Time{} oldest := time.Now().Add(-3 * time.Hour) for i := 0; i < 2; i++ { // 前两个假装很久没人访问 os.Chtimes(s.jpegPath(sha(i), 480), oldest, oldest) } if _, err := s.Put(sha(3), 480, bytes.Repeat([]byte("x"), 1000), "image/jpeg"); err != nil { t.Fatal(err) } var total int64 var kept []string filepath.WalkDir(s.Dir, func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return nil } fi, _ := d.Info() total += fi.Size() kept = append(kept, d.Name()) return nil }) if total > s.MaxBytes { t.Fatalf("sweep 后仍超限: total=%d max=%d files=%v", total, s.MaxBytes, kept) } if len(kept) >= 4 { t.Fatalf("一个都没淘汰,说明扫描没跑: files=%v", kept) } // 最新写入的那个必须还在 if s.FindCached(sha(3), 480) == "" { t.Fatalf("刚写入的缓存被误删: %v", kept) } } func TestPurgeRemovesAllEntriesForFile(t *testing.T) { s := NewStore(t.TempDir()) sha := strings.Repeat("f", 64) for _, w := range []int{480, 960} { if _, err := s.Put(sha, w, []byte("data"), "image/jpeg"); err != nil { t.Fatal(err) } } s.MarkFailed(sha) other := strings.Repeat("a", 64) if _, err := s.Put(other, 480, []byte("keep"), "image/jpeg"); err != nil { t.Fatal(err) } s.Purge(sha) for _, w := range []int{480, 960} { if p := s.FindCached(sha, w); p != "" { t.Fatalf("purge 后仍残留 %s", p) } } if s.FailedRecently(sha) { t.Fatal("失败标记没清掉") } if s.FindCached(other, 480) == "" { t.Fatal("误删了别的文件的缓存") } }