package imgcache import ( "errors" "os" "path/filepath" "strings" "testing" ) // Static errors returned by the stub free-space probes below. var ( errTestStatfsFailed = errors.New("statfs failed") errTestProbeNotExpected = errors.New("probe must not be called") ) // TestComputeDefaultMaxBytesCountsWhatTheCacheHolds verifies that the // default limit is 75% of the free space plus what the cache already // holds, so a cache filled to its limit keeps that limit across a // restart instead of shrinking to 75% of the space left free. func TestComputeDefaultMaxBytesCountsWhatTheCacheHolds(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<30) // Empty cache, 4 GiB free -> 3 GiB default. got, err := cache.computeDefaultMaxBytes(t.Context(), func(string) (uint64, error) { return 4294967296, nil }) if err != nil { t.Fatalf("computeDefaultMaxBytes returned error: %v", err) } t.Logf("default for an empty cache with 4 GiB free: %d", got) if got != 3221225472 { t.Errorf("default for an empty cache = %d, want 3221225472 (75%% of 4 GiB)", got) } // The cache now holds those 3 GiB, which leaves 1 GiB free. _, err = cache.db.ExecContext(t.Context(), `INSERT INTO variant_content (cache_key, size_bytes, content_type) VALUES (?, ?, ?)`, string(testVariantKeyOne), 3221225472, testContentTypeWebP, ) if err != nil { t.Fatalf("failed to insert variant accounting row: %v", err) } got, err = cache.computeDefaultMaxBytes(t.Context(), func(string) (uint64, error) { return 1073741824, nil }) if err != nil { t.Fatalf("computeDefaultMaxBytes returned error: %v", err) } t.Logf("default for a cache holding 3 GiB with 1 GiB free: %d", got) if got != 3221225472 { t.Errorf("default for a cache holding 3 GiB with 1 GiB free = %d, "+ "want 3221225472 (75%% of 1 GiB + 3 GiB)", got) } } // TestComputeDefaultMaxBytesAppliesFloor verifies that when 75% of the // free space plus what the cache holds is below 500 MiB, the default // is floored at DefaultCacheMaxBytesFloor. func TestComputeDefaultMaxBytesAppliesFloor(t *testing.T) { t.Parallel() cases := []struct { name string freeBytes uint64 }{ {name: "100 MiB free", freeBytes: 104857600}, {name: "zero free", freeBytes: 0}, {name: "just below floor threshold", freeBytes: 699050665}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<30) got, err := cache.computeDefaultMaxBytes(t.Context(), func(string) (uint64, error) { return tc.freeBytes, nil }) if err != nil { t.Fatalf("computeDefaultMaxBytes returned error: %v", err) } if got != DefaultCacheMaxBytesFloor { t.Errorf("computeDefaultMaxBytes = %d, want floor %d", got, DefaultCacheMaxBytesFloor) } }) } } // TestComputeDefaultMaxBytesPropagatesProbeError verifies that a // failing free-space probe produces an error naming cache_max_bytes, // instead of a silently wrong default. func TestComputeDefaultMaxBytesPropagatesProbeError(t *testing.T) { t.Parallel() cache, _ := newEvictionTestCache(t, 1<<30) _, err := cache.computeDefaultMaxBytes(t.Context(), func(string) (uint64, error) { return 0, errTestStatfsFailed }) if err == nil { t.Fatal("probe failure must produce an error, got nil") } t.Logf("got expected error: %v", err) if !strings.Contains(err.Error(), "cache_max_bytes") { t.Errorf("error %q does not name cache_max_bytes", err.Error()) } } // TestNewCacheComputesDefaultMaxBytesWhenAsked verifies that with // UseDefaultMaxBytes set, the cache's limit becomes the computed // default, and that the probe is pointed at /cache/, which // must be created first so statfs measures the right filesystem. func TestNewCacheComputesDefaultMaxBytesWhenAsked(t *testing.T) { t.Parallel() stateDir := t.TempDir() wantCacheDir := filepath.Join(stateDir, "cache") var probedPath string // 4 GiB free -> 3 GiB default. probe := func(path string) (uint64, error) { probedPath = path info, err := os.Stat(path) if err != nil || !info.IsDir() { t.Errorf("cache directory %q was not created before probing: info=%v err=%v", path, info, err) } return 4294967296, nil } cache, err := newCache(evictionTestDB(t), CacheConfig{ StateDir: stateDir, UseDefaultMaxBytes: true, }, probe) if err != nil { t.Fatalf("newCache returned error: %v", err) } if cache.config.MaxBytes != 3221225472 { t.Errorf("MaxBytes = %d, want computed default 3221225472", cache.config.MaxBytes) } if probedPath != wantCacheDir { t.Errorf("free space probed at %q, want cache directory %q", probedPath, wantCacheDir) } } // TestNewCacheKeepsExplicitMaxBytes verifies that without // UseDefaultMaxBytes the cache keeps MaxBytes exactly as given and // never consults the free-space probe. func TestNewCacheKeepsExplicitMaxBytes(t *testing.T) { t.Parallel() probe := func(string) (uint64, error) { t.Error("free-space probe must not be consulted for explicit values") return 0, errTestProbeNotExpected } cache, err := newCache(evictionTestDB(t), CacheConfig{ StateDir: t.TempDir(), MaxBytes: 1024, }, probe) if err != nil { t.Fatalf("newCache returned error: %v", err) } if cache.config.MaxBytes != 1024 { t.Errorf("MaxBytes = %d, want explicit 1024 (no floor, no recompute)", cache.config.MaxBytes) } }