Files
pixa/internal/imgcache/cachesize_internal_test.go
T
clawbot f1455ae361
check / check (push) Failing after 2s
Count what the cache holds in the default cache_max_bytes (closes #184)
The default limit was 75% of the space free at startup. The cache's own
files are not free space, so a fuller cache got a smaller limit after a
restart and eviction then deleted most of it. The default is now 75% of
the sum of the free space and what the cache already holds by its own
size accounting, at least 500 MiB. The cache works it out when it
opens, after the database is open, so the computation and its tests
moved from internal/config to internal/imgcache; the config only
records whether cache_max_bytes was set.

Model: opus-5-5
2026-10-04 14:27:31 +00:00

179 lines
5.2 KiB
Go

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 <state_dir>/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)
}
}