package imgcache import ( "bytes" "errors" "fmt" "io" "os" "sync" "testing" "time" ) // webpRequest returns a request for a 100x100 WebP variant of path. func webpRequest(path string) *ImageRequest { return &ImageRequest{ SourceHost: testHostCDN, SourcePath: path, Size: Size{Width: 100, Height: 100}, Format: FormatWebP, Quality: 85, FitMode: FitCover, } } // assertVariantServed checks that GetVariant serves key with the given // content and the image/webp content type storeEvictionTestVariant stores. func assertVariantServed(t *testing.T, cache *Cache, key VariantKey, content []byte) { t.Helper() reader, size, contentType, err := cache.GetVariant(key) if err != nil { t.Fatalf("GetVariant(%s) error = %v", key, err) } defer func() { _ = reader.Close() }() got, err := io.ReadAll(reader) if err != nil { t.Fatalf("reading variant %s: %v", key, err) } if !bytes.Equal(got, content) { t.Errorf("GetVariant(%s) content = %q, want %q", key, got, content) } if size != int64(len(content)) { t.Errorf("GetVariant(%s) size = %d, want %d", key, size, len(content)) } if contentType != testContentTypeWebP { t.Errorf("GetVariant(%s) content type = %q, want %q", key, contentType, testContentTypeWebP) } } // assertVariantNotFound checks that GetVariant refuses key with // ErrNotFound. func assertVariantNotFound(t *testing.T, cache *Cache, key VariantKey) { t.Helper() reader, _, _, err := cache.GetVariant(key) if err == nil { _ = reader.Close() } if !errors.Is(err, ErrNotFound) { t.Errorf("GetVariant(%s) error = %v, want ErrNotFound", key, err) } } // assertLookupMisses checks that Lookup reports request as a miss. func assertLookupMisses(t *testing.T, cache *Cache, request *ImageRequest) { t.Helper() lookup, err := cache.Lookup(t.Context(), request) if err != nil { t.Fatalf("Lookup(%s) error = %v", request.SourcePath, err) } if lookup.Hit { t.Errorf("Lookup(%s) is a hit, want a miss", request.SourcePath) } } // TestSecondHitDoesNotReadMetaFile checks that once a variant has been // stored or read, a hit takes its content type from memory: with the // .meta file deleted, GetVariant must still return the stored content // type rather than the application/octet-stream it uses without one. func TestSecondHitDoesNotReadMetaFile(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<20) content := []byte("webp variant bytes") storeEvictionTestVariant(t, cache, testVariantKeyOne, content) // A second Cache on the same state directory starts with nothing in // memory, as pixad does after a restart, so its first read uses the // .meta file. restarted, err := NewCache(cache.db, cache.config) if err != nil { t.Fatalf("NewCache() error = %v", err) } assertVariantServed(t, restarted, testVariantKeyOne, content) err = os.Remove(cache.variants.keyToPath(testVariantKeyOne) + ".meta") if err != nil { t.Fatalf("removing .meta file: %v", err) } assertVariantServed(t, cache, testVariantKeyOne, content) assertVariantServed(t, restarted, testVariantKeyOne, content) } // TestReadDuringStoreKeepsStoredContentType checks that a GetVariant // which began before StoreVariant finished cannot replace the content // type the store kept in memory. Such a read can find the variant file // but not yet its .meta file, and so gets application/octet-stream. The // test deletes the .meta file after the store, then runs the part of // GetVariant that comes after its check of memory. func TestReadDuringStoreKeepsStoredContentType(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<20) content := []byte("webp variant bytes") storeEvictionTestVariant(t, cache, testVariantKeyOne, content) err := os.Remove(cache.variants.keyToPath(testVariantKeyOne) + ".meta") if err != nil { t.Fatalf("removing .meta file: %v", err) } reader, _, contentType, err := cache.loadVariantWithMeta(testVariantKeyOne) if err != nil { t.Fatalf("loadVariantWithMeta(%s) error = %v", testVariantKeyOne, err) } _ = reader.Close() t.Logf("the read without a .meta file got content type %q", contentType) assertVariantServed(t, cache, testVariantKeyOne, content) } // TestEvictedVariantIsNotServed checks that a variant the evictor // removed is a miss and cannot be read, although it had been stored // and served before. func TestEvictedVariantIsNotServed(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1500) oldRequest := webpRequest("/old.jpg") newRequest := webpRequest("/new.jpg") oldKey := CacheKey(oldRequest) newKey := CacheKey(newRequest) oldContent := bytes.Repeat([]byte{0x01}, 1000) newContent := bytes.Repeat([]byte{0x02}, 1000) storeEvictionTestVariant(t, cache, oldKey, oldContent) storeEvictionTestVariant(t, cache, newKey, newContent) assertVariantServed(t, cache, oldKey, oldContent) setVariantLastAccessed(t, cache, oldKey, time.Now().Add(-time.Hour)) err := cache.EvictToLimit(t.Context()) if err != nil { t.Fatalf("EvictToLimit() error = %v", err) } assertLookupMisses(t, cache, oldRequest) assertVariantNotFound(t, cache, oldKey) assertVariantServed(t, cache, newKey, newContent) } // TestVariantDeletedFromDiskIsNotServed checks that a variant whose // file was deleted by something other than the evictor cannot be read, // and is a miss afterwards. func TestVariantDeletedFromDiskIsNotServed(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<20) request := webpRequest("/deleted.jpg") key := CacheKey(request) content := []byte("webp variant bytes") storeEvictionTestVariant(t, cache, key, content) assertVariantServed(t, cache, key, content) err := os.Remove(cache.variants.keyToPath(key)) if err != nil { t.Fatalf("removing variant file: %v", err) } assertVariantNotFound(t, cache, key) assertLookupMisses(t, cache, request) } // TestConcurrentVariantStoreReadAndEvict stores, reads and evicts // variants from several goroutines at once, for the race detector. func TestConcurrentVariantStoreReadAndEvict(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<20) ctx := t.Context() var wg sync.WaitGroup for goroutine := range 8 { wg.Go(func() { key := VariantKey(fmt.Sprintf("aabbccdd01%02d", goroutine)) content := []byte(key) for range 20 { err := cache.StoreVariant( ctx, key, bytes.NewReader(content), testContentTypeWebP) if err != nil { t.Errorf("StoreVariant(%s) error = %v", key, err) return } reader, _, contentType, err := cache.GetVariant(key) if err != nil { t.Errorf("GetVariant(%s) error = %v", key, err) return } _ = reader.Close() if contentType != testContentTypeWebP { t.Errorf("GetVariant(%s) content type = %q, want %q", key, contentType, testContentTypeWebP) } err = cache.evictVariant(ctx, key) if err != nil { t.Errorf("evictVariant(%s) error = %v", key, err) return } assertVariantNotFound(t, cache, key) } }) } wg.Wait() }