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()) } } }