package api import ( "errors" "sync" "sync/atomic" "testing" ) func TestThumbnailCacheDeduplicatesConcurrentBuilds(t *testing.T) { c := newThumbnailCache() key := thumbnailCacheKey{Path: "photo.jpg", Size: 10, ModTime: 20} var builds atomic.Int32 const workers = 24 var wg sync.WaitGroup results := make([][]byte, workers) errs := make([]error, workers) wg.Add(workers) for i := 0; i < workers; i++ { go func(i int) { defer wg.Done() results[i], errs[i] = c.Do(key, func() ([]byte, error) { builds.Add(1) return []byte("thumbnail"), nil }) }(i) } wg.Wait() if builds.Load() != 1 { t.Fatalf("thumbnail builder ran %d times, want 1", builds.Load()) } for i := range results { if errs[i] != nil || string(results[i]) != "thumbnail" { t.Fatalf("worker %d got result %q, err=%v", i, results[i], errs[i]) } } } func TestThumbnailCacheDoesNotCacheErrors(t *testing.T) { c := newThumbnailCache() key := thumbnailCacheKey{Path: "broken.jpg"} wantErr := errors.New("decode failed") var builds atomic.Int32 for i := 0; i < 2; i++ { _, err := c.Do(key, func() ([]byte, error) { builds.Add(1) return nil, wantErr }) if !errors.Is(err, wantErr) { t.Fatalf("got err %v, want %v", err, wantErr) } } if builds.Load() != 2 { t.Fatalf("error result was cached; builder ran %d times", builds.Load()) } } func TestThumbnailCacheEvictsByBound(t *testing.T) { c := newThumbnailCache() c.maxItems = 2 for i := 0; i < 3; i++ { key := thumbnailCacheKey{Path: string(rune('a' + i))} if _, err := c.Do(key, func() ([]byte, error) { return []byte("x"), nil }); err != nil { t.Fatal(err) } } c.mu.Lock() defer c.mu.Unlock() if len(c.items) != 2 || c.order.Len() != 2 { t.Fatalf("cache size=%d/list=%d, want 2/2", len(c.items), c.order.Len()) } } func TestThumbnailCacheEvictsByBytes(t *testing.T) { c := newThumbnailCache() c.maxBytes = 3 for i := 0; i < 2; i++ { key := thumbnailCacheKey{Path: string(rune('x' + i))} if _, err := c.Do(key, func() ([]byte, error) { return []byte("xx"), nil }); err != nil { t.Fatal(err) } } c.mu.Lock() defer c.mu.Unlock() if c.bytes > c.maxBytes || len(c.items) != 1 { t.Fatalf("cache bytes=%d items=%d, want <=%d/1", c.bytes, len(c.items), c.maxBytes) } }