A directory where a variant .meta file goes, and dropped tables in the test database, make the variant .meta write, the two Stats count queries and the stats counter updates fail. Each test hands the cache or NewVariantStorage a logger writing to a buffer, checks the warn line, and checks the store and Stats calls still succeed. Model: opus-5-5
186 lines
3.8 KiB
Go
186 lines
3.8 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"log/slog"
|
|
"math"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/database"
|
|
)
|
|
|
|
func setupStatsTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
|
|
db, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = database.ApplyMigrations(context.Background(), db, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
return db
|
|
}
|
|
|
|
func TestStats_HitRateIsRatio(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := setupStatsTestDB(t)
|
|
dir := t.TempDir()
|
|
|
|
cache, err := NewCache(db, CacheConfig{
|
|
StateDir: dir,
|
|
CacheTTL: time.Hour,
|
|
NegativeTTL: 5 * time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Set some hit/miss counts and a transform_count
|
|
_, err = db.ExecContext(ctx, `
|
|
UPDATE cache_stats
|
|
SET hit_count = 75, miss_count = 25, transform_count = 9999
|
|
WHERE id = 1
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stats, err := cache.Stats(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if stats.HitCount != 75 {
|
|
t.Errorf("HitCount = %d, want 75", stats.HitCount)
|
|
}
|
|
|
|
if stats.MissCount != 25 {
|
|
t.Errorf("MissCount = %d, want 25", stats.MissCount)
|
|
}
|
|
|
|
// HitRate should be 0.75, NOT 9999 (transform_count)
|
|
expectedRate := 0.75
|
|
if math.Abs(stats.HitRate-expectedRate) > 0.001 {
|
|
t.Errorf("HitRate = %f, want %f (was it scanning transform_count?)",
|
|
stats.HitRate, expectedRate)
|
|
}
|
|
}
|
|
|
|
func TestStats_ZeroCounts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := setupStatsTestDB(t)
|
|
dir := t.TempDir()
|
|
|
|
cache, err := NewCache(db, CacheConfig{
|
|
StateDir: dir,
|
|
CacheTTL: time.Hour,
|
|
NegativeTTL: 5 * time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stats, err := cache.Stats(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// With zero hits and misses, HitRate should be 0, not some garbage value
|
|
if stats.HitRate != 0.0 {
|
|
t.Errorf("HitRate = %f, want 0.0 for zero counts", stats.HitRate)
|
|
}
|
|
}
|
|
|
|
// TestStats_LogsFailedCountQueries verifies that a failed item count query
|
|
// and a failed size query are each logged at warn and Stats still succeeds.
|
|
func TestStats_LogsFailedCountQueries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := setupStatsTestDB(t)
|
|
|
|
var logBuf bytes.Buffer
|
|
|
|
cache, err := NewCache(db, CacheConfig{
|
|
StateDir: t.TempDir(),
|
|
CacheTTL: time.Hour,
|
|
NegativeTTL: 5 * time.Minute,
|
|
Logger: slog.New(slog.NewJSONHandler(&logBuf, nil)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = db.ExecContext(t.Context(),
|
|
`DROP TABLE request_cache; DROP TABLE output_content`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = cache.Stats(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("Stats() error = %v, want nil", err)
|
|
}
|
|
|
|
for _, msg := range []string{
|
|
"failed to count cache items for stats",
|
|
"failed to sum cache size for stats",
|
|
} {
|
|
want := `"level":"WARN","msg":"` + msg + `"`
|
|
if !strings.Contains(logBuf.String(), want) {
|
|
t.Errorf("log missing %s; got %q", want, logBuf.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIncrementStats_LogsFailedUpdates verifies that a failed hit or miss
|
|
// count update and a failed upstream fetch count update are each logged at
|
|
// warn.
|
|
func TestIncrementStats_LogsFailedUpdates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := setupStatsTestDB(t)
|
|
|
|
var logBuf bytes.Buffer
|
|
|
|
cache, err := NewCache(db, CacheConfig{
|
|
StateDir: t.TempDir(),
|
|
CacheTTL: time.Hour,
|
|
NegativeTTL: 5 * time.Minute,
|
|
Logger: slog.New(slog.NewJSONHandler(&logBuf, nil)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = db.ExecContext(t.Context(), `DROP TABLE cache_stats`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cache.IncrementStats(t.Context(), false, 1024)
|
|
|
|
for _, msg := range []string{
|
|
"failed to count cache hit or miss",
|
|
"failed to count upstream fetch",
|
|
} {
|
|
want := `"level":"WARN","msg":"` + msg + `"`
|
|
if !strings.Contains(logBuf.String(), want) {
|
|
t.Errorf("log missing %s; got %q", want, logBuf.String())
|
|
}
|
|
}
|
|
}
|