package imgcache import ( "errors" "os" "path/filepath" "strings" "testing" ) // errTestStatfsFailed is returned by the failing free-space probe below. var errTestStatfsFailed = errors.New("statfs failed") // 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) } 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) } 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()) } } // TestComputeDefaultMaxBytesProbesTheCacheDirectory verifies that the // probe is pointed at /cache/ and that the directory exists // by then, so statfs measures the filesystem that holds the cache. func TestComputeDefaultMaxBytesProbesTheCacheDirectory(t *testing.T) { t.Parallel() cache, stateDir := newEvictionTestCache(t, 1<<30) wantCacheDir := filepath.Join(stateDir, "cache") var probedPath string _, err := cache.computeDefaultMaxBytes(t.Context(), func(path string) (uint64, error) { probedPath = path info, err := os.Stat(path) if err != nil || !info.IsDir() { t.Errorf("cache directory %q missing when probed: info=%v err=%v", path, info, err) } return 4294967296, nil }) if err != nil { t.Fatalf("computeDefaultMaxBytes returned error: %v", err) } if probedPath != wantCacheDir { t.Errorf("free space probed at %q, want cache directory %q", probedPath, wantCacheDir) } } // TestNewCacheUsesDefaultMaxBytesOnlyWhenAsked verifies that NewCache // replaces MaxBytes with the computed default when UseDefaultMaxBytes // is set, and keeps MaxBytes as given otherwise. func TestNewCacheUsesDefaultMaxBytesOnlyWhenAsked(t *testing.T) { t.Parallel() withDefault, err := NewCache(evictionTestDB(t), CacheConfig{ StateDir: t.TempDir(), MaxBytes: 1024, UseDefaultMaxBytes: true, }) if err != nil { t.Fatalf("NewCache with UseDefaultMaxBytes failed: %v", err) } if withDefault.config.MaxBytes < DefaultCacheMaxBytesFloor { t.Errorf("MaxBytes with UseDefaultMaxBytes = %d, want the default, "+ "at least %d", withDefault.config.MaxBytes, DefaultCacheMaxBytesFloor) } explicit, err := NewCache(evictionTestDB(t), CacheConfig{ StateDir: t.TempDir(), MaxBytes: 1024, }) if err != nil { t.Fatalf("NewCache failed: %v", err) } if explicit.config.MaxBytes != 1024 { t.Errorf("MaxBytes without UseDefaultMaxBytes = %d, want 1024", explicit.config.MaxBytes) } }