Files
MeBox/internal/service/cache_cleanup_test.go
T
truewhile 623d87f12b 优化
2026-10-29 23:47:18 +08:00

265 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"go.uber.org/zap"
)
func TestPruneImageCache_UnderLimit(t *testing.T) {
dir := t.TempDir()
file1 := filepath.Join(dir, "img1")
file2 := filepath.Join(dir, "img2")
if err := os.WriteFile(file1, make([]byte, 100), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file2, make([]byte, 200), 0o600); err != nil {
t.Fatal(err)
}
// Max limit is 500 bytes, total is 300 bytes -> no prune
res, err := PruneImageCache(dir, 500)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.DeletedFiles != 0 {
t.Fatalf("expected 0 deleted files, got %d", res.DeletedFiles)
}
if res.TotalFilesBefore != 2 || res.TotalBytesBefore != 300 || res.RemainingBytes != 300 {
t.Fatalf("unexpected stats: %+v", res)
}
}
func TestPruneImageCache_OverLimitLRU(t *testing.T) {
dir := t.TempDir()
now := time.Now()
// Create 4 files of 100 bytes each, with distinct mtime
fOldest := filepath.Join(dir, "oldest")
fMidOld := filepath.Join(dir, "mid_old")
fMidNew := filepath.Join(dir, "mid_new")
fNewest := filepath.Join(dir, "newest")
for _, f := range []string{fOldest, fMidOld, fMidNew, fNewest} {
if err := os.WriteFile(f, make([]byte, 100), 0o600); err != nil {
t.Fatal(err)
}
}
_ = os.Chtimes(fOldest, now.Add(-4*time.Hour), now.Add(-4*time.Hour))
_ = os.Chtimes(fMidOld, now.Add(-3*time.Hour), now.Add(-3*time.Hour))
_ = os.Chtimes(fMidNew, now.Add(-2*time.Hour), now.Add(-2*time.Hour))
_ = os.Chtimes(fNewest, now.Add(-1*time.Hour), now.Add(-1*time.Hour))
// Total = 400 bytes. Max limit = 300 bytes.
// Target = 300 * 80 / 100 = 240 bytes.
// Deleting oldest (100) brings total to 300 (> 240).
// Deleting mid_old (100) brings total to 200 (<= 240).
// Total deleted = 2 files (200 bytes), remaining = 200 bytes.
res, err := PruneImageCache(dir, 300)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.DeletedFiles != 2 {
t.Fatalf("expected 2 deleted files, got %d", res.DeletedFiles)
}
if res.FreedBytes != 200 {
t.Fatalf("expected 200 freed bytes, got %d", res.FreedBytes)
}
if res.RemainingBytes != 200 {
t.Fatalf("expected 200 remaining bytes, got %d", res.RemainingBytes)
}
// Verify oldest and mid_old were deleted, mid_new and newest still exist
if _, err := os.Stat(fOldest); !os.IsNotExist(err) {
t.Fatalf("expected oldest file to be deleted, got err=%v", err)
}
if _, err := os.Stat(fMidOld); !os.IsNotExist(err) {
t.Fatalf("expected mid_old file to be deleted, got err=%v", err)
}
if _, err := os.Stat(fMidNew); err != nil {
t.Fatalf("expected mid_new file to exist, got err=%v", err)
}
if _, err := os.Stat(fNewest); err != nil {
t.Fatalf("expected newest file to exist, got err=%v", err)
}
}
func TestPruneImageCache_SkipsTmpFiles(t *testing.T) {
dir := t.TempDir()
fTmp := filepath.Join(dir, "img-123.tmp")
fImg := filepath.Join(dir, "cached_img")
if err := os.WriteFile(fTmp, make([]byte, 500), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(fImg, make([]byte, 100), 0o600); err != nil {
t.Fatal(err)
}
// Limit is 200 bytes. fTmp (500) is ignored, only fImg (100) is counted <= 200.
res, err := PruneImageCache(dir, 200)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.DeletedFiles != 0 {
t.Fatalf("expected 0 deleted files, got %d", res.DeletedFiles)
}
if _, err := os.Stat(fTmp); err != nil {
t.Fatalf("expected tmp file to remain untouched, got %v", err)
}
}
func TestPruneImageCache_ZeroOrNegativeLimit(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "img")
if err := os.WriteFile(f, make([]byte, 100), 0o600); err != nil {
t.Fatal(err)
}
res, err := PruneImageCache(dir, 0)
if err != nil || res.DeletedFiles != 0 {
t.Fatalf("expected no-op for 0 limit, got %+v, err=%v", res, err)
}
res, err = PruneImageCache(dir, -10)
if err != nil || res.DeletedFiles != 0 {
t.Fatalf("expected no-op for negative limit, got %+v, err=%v", res, err)
}
}
func TestSchedulerJobCleanCache(t *testing.T) {
cacheRoot := t.TempDir()
imagesDir := filepath.Join(cacheRoot, "images")
if err := os.MkdirAll(imagesDir, 0o750); err != nil {
t.Fatal(err)
}
f := filepath.Join(imagesDir, "old_poster")
if err := os.WriteFile(f, make([]byte, 2*1024*1024), 0o600); err != nil {
t.Fatal(err)
}
scheduler := NewSchedulerService(zap.NewNop(), nil, nil, nil, nil, nil, cacheRoot)
// Set limit to 1MB; our file is 2MB -> should be pruned
scheduler.SetCacheCleanupPolicyProvider(func() CacheCleanupPolicy {
return CacheCleanupPolicy{TotalBytes: 1 * 1024 * 1024}
})
if err := scheduler.jobCleanCache(context.Background()); err != nil {
t.Fatalf("jobCleanCache failed: %v", err)
}
if _, err := os.Stat(f); !os.IsNotExist(err) {
t.Fatalf("expected file to be pruned, got err=%v", err)
}
}
// TestPruneCacheEvictionOrder 验证总量超限时严格按
// hls → images → library-covers 的顺序淘汰,且 reader-audio 不参与。
func TestPruneCacheEvictionOrder(t *testing.T) {
cacheRoot := t.TempDir()
now := time.Now()
audio := filepath.Join(cacheRoot, "reader-audio", "old.mp3")
hls := filepath.Join(cacheRoot, "hls", "job", "seg.ts")
image := filepath.Join(cacheRoot, "images", "originals", "poster.jpg")
cover := filepath.Join(cacheRoot, "library-covers", "cover.jpg")
for _, p := range []string{audio, hls, image, cover} {
writePoolFile(t, p, 1024*1024, now.Add(-time.Hour))
}
// 计入总量的只有 hls+images+cover = 3MB,上限 2MB -> 回落到 1.6MB:
// 按顺序删 hls、images 两个文件即可;reader-audio 与封面都应保留。
res, err := PruneCache(cacheRoot, 2*1024*1024, 0)
if err != nil {
t.Fatalf("prune failed: %v", err)
}
if res.DeletedFiles != 2 {
t.Fatalf("deleted files = %d, want 2", res.DeletedFiles)
}
mustExist(t, audio)
mustNotExist(t, hls)
mustNotExist(t, image)
mustExist(t, cover)
if _, ok := res.FreedByPool["reader-audio"]; ok {
t.Fatalf("reader-audio must not be part of the global pool, got %+v", res.FreedByPool)
}
if res.FreedByPool["hls"] != 1024*1024 || res.FreedByPool["images"] != 1024*1024 {
t.Fatalf("unexpected per-pool stats: %+v", res.FreedByPool)
}
}
// TestPruneCacheLeavesReaderAudioAlone reader-audio 有独立的 1GB 写入时淘汰,
// 不参与总容量统计:即使它单独就超过全局上限,全局任务也不应删它。
func TestPruneCacheLeavesReaderAudioAlone(t *testing.T) {
cacheRoot := t.TempDir()
audio := filepath.Join(cacheRoot, "reader-audio", "book", "chapter.mp3")
writePoolFile(t, audio, 4*1024*1024, time.Now().Add(-time.Hour))
res, err := PruneCache(cacheRoot, 1*1024*1024, 0)
if err != nil {
t.Fatalf("prune failed: %v", err)
}
if res.DeletedFiles != 0 {
t.Fatalf("deleted files = %d, want 0", res.DeletedFiles)
}
if res.TotalBytesBefore != 0 {
t.Fatalf("reader-audio should not count toward total, got %d bytes", res.TotalBytesBefore)
}
mustExist(t, audio)
}
// TestPruneCacheAppliesOriginalsSubQuota 验证总量没超时,图片原图子配额仍然生效。
func TestPruneCacheAppliesOriginalsSubQuota(t *testing.T) {
cacheRoot := t.TempDir()
now := time.Now()
oldOriginal := filepath.Join(cacheRoot, "images", "originals", "old.jpg")
newOriginal := filepath.Join(cacheRoot, "images", "originals", "new.jpg")
derived := currentVariantPath(filepath.Join(cacheRoot, "images", "resize"), "d", 480, 600, 80)
writePoolFile(t, oldOriginal, 800, now.Add(-2*time.Hour))
writePoolFile(t, newOriginal, 800, now.Add(-time.Hour))
writePoolFile(t, derived, 800, now.Add(-time.Hour))
// 总量上限很大(不触发全局淘汰),原图子配额 1000B -> 删到 800B。
res, err := PruneCache(cacheRoot, 100*1024*1024, 1000)
if err != nil {
t.Fatalf("prune failed: %v", err)
}
if res.DeletedFiles != 1 {
t.Fatalf("deleted files = %d, want 1", res.DeletedFiles)
}
mustNotExist(t, oldOriginal)
mustExist(t, newOriginal)
mustExist(t, derived)
}
// TestPruneCacheOldestFirstWithinPool 验证同一池内先删最旧的文件。
func TestPruneCacheOldestFirstWithinPool(t *testing.T) {
cacheRoot := t.TempDir()
now := time.Now()
oldHLS := filepath.Join(cacheRoot, "hls", "job-a", "seg1.ts")
newHLS := filepath.Join(cacheRoot, "hls", "job-b", "seg1.ts")
writePoolFile(t, oldHLS, 1024*1024, now.Add(-2*time.Hour))
writePoolFile(t, newHLS, 1024*1024, now.Add(-time.Minute))
// 上限 1.25MB -> 回落水位 1MB:删掉最旧的 1MB 后正好达标。
res, err := PruneCache(cacheRoot, 1280*1024, 0)
if err != nil {
t.Fatalf("prune failed: %v", err)
}
if res.DeletedFiles != 1 {
t.Fatalf("deleted files = %d, want 1", res.DeletedFiles)
}
mustNotExist(t, oldHLS)
mustExist(t, newHLS)
}