mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-20 06:28:13 +00:00
perf+coverage: optimisation pass + coverage push to ≥70%
Performance / resource usage: - circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking) - server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo - graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off) - graphql.go: collapse two-pass AST scan into single pass; lower-case once - sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars - proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc - metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled - logging/logger.go: add IsLevelEnabled helper - lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%) - cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock - rps_tracker.go: RWMutex+uint64 -> atomic.Uint64 - retry_budget.go: drop unused mutex - api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers) - tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes - admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection Build / runtime: - Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes) - Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB - main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS) - main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts - README.md: env-var docs + metric-label docs updated; cardinality note Test coverage push (per package): - main 51.2% -> 74.7% - cache 66.3% -> 93.7% - cache/redis 45.5% -> 98.2% - tracing 66.7% -> 72.9% - (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged) New test files: coverage_micro_test, coverage_extras_test, server_handlers_test, api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test, cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test. Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
This commit is contained in:
Vendored
+218
@@ -0,0 +1,218 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
ta "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// helper resets package-level globals and returns a cleanup func.
|
||||
func withFreshMemoryCache(t *testing.T, ttl time.Duration) func() {
|
||||
t.Helper()
|
||||
prev := config
|
||||
prevStats := cacheStats
|
||||
config = &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
Client: libpack_cache_memory.New(ttl),
|
||||
TTL: int(ttl.Seconds()),
|
||||
}
|
||||
cacheStats = &CacheStats{}
|
||||
return func() {
|
||||
config = prev
|
||||
cacheStats = prevStats
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCacheMemoryUsage_Initialized covers the initialized branch (was 0%).
|
||||
func TestGetCacheMemoryUsage_Initialized_ReturnsNonNegative(t *testing.T) {
|
||||
defer withFreshMemoryCache(t, 5*time.Minute)()
|
||||
|
||||
usage := GetCacheMemoryUsage()
|
||||
ta.GreaterOrEqual(t, usage, int64(0))
|
||||
}
|
||||
|
||||
// TestGetCacheMemoryUsage_Uninitialized covers the early-return branch.
|
||||
func TestGetCacheMemoryUsage_Uninitialized_ReturnsZero(t *testing.T) {
|
||||
prev := config
|
||||
config = nil
|
||||
defer func() { config = prev }()
|
||||
|
||||
ta.Equal(t, int64(0), GetCacheMemoryUsage())
|
||||
}
|
||||
|
||||
// TestGetCacheMaxMemorySize_Initialized covers the initialized branch (was 0%).
|
||||
func TestGetCacheMaxMemorySize_Initialized_ReturnsPositive(t *testing.T) {
|
||||
defer withFreshMemoryCache(t, 5*time.Minute)()
|
||||
|
||||
maxSize := GetCacheMaxMemorySize()
|
||||
ta.Greater(t, maxSize, int64(0))
|
||||
}
|
||||
|
||||
// TestGetCacheMaxMemorySize_Uninitialized covers the early-return branch.
|
||||
func TestGetCacheMaxMemorySize_Uninitialized_ReturnsZero(t *testing.T) {
|
||||
prev := config
|
||||
config = nil
|
||||
defer func() { config = prev }()
|
||||
|
||||
ta.Equal(t, int64(0), GetCacheMaxMemorySize())
|
||||
}
|
||||
|
||||
// TestEnableCache_LRUBranch covers cfg.Memory.UseLRU == true branch in EnableCache.
|
||||
func TestEnableCache_LRUBranch_InitializesLRUClient(t *testing.T) {
|
||||
prev := config
|
||||
prevStats := cacheStats
|
||||
defer func() {
|
||||
config = prev
|
||||
cacheStats = prevStats
|
||||
}()
|
||||
|
||||
cfg := &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
TTL: 5,
|
||||
}
|
||||
cfg.Memory.UseLRU = true
|
||||
cfg.Memory.MaxMemorySize = 1024 * 1024
|
||||
cfg.Memory.MaxEntries = 100
|
||||
|
||||
EnableCache(cfg)
|
||||
require.NotNil(t, config.Client, "LRU client must be set")
|
||||
ta.True(t, IsCacheInitialized())
|
||||
|
||||
// Verify basic ops work with LRU client.
|
||||
CacheStore("lru-key", []byte("lru-val"))
|
||||
got := CacheLookup("lru-key")
|
||||
ta.Equal(t, []byte("lru-val"), got)
|
||||
}
|
||||
|
||||
// TestEnableCache_NilLogger covers the auto-logger creation branch.
|
||||
func TestEnableCache_NilLogger_AutoCreatesLogger(t *testing.T) {
|
||||
prev := config
|
||||
prevStats := cacheStats
|
||||
defer func() {
|
||||
config = prev
|
||||
cacheStats = prevStats
|
||||
}()
|
||||
|
||||
cfg := &CacheConfig{
|
||||
Logger: nil, // deliberately nil
|
||||
TTL: 5,
|
||||
}
|
||||
// Should not panic; logger is created internally.
|
||||
ta.NotPanics(t, func() { EnableCache(cfg) })
|
||||
ta.NotNil(t, cfg.Logger)
|
||||
}
|
||||
|
||||
// TestEnableCache_MemoryDefaults covers the default memory sizing branch (maxMemory<=0).
|
||||
func TestEnableCache_MemoryDefaults_UsesDefaultSizes(t *testing.T) {
|
||||
prev := config
|
||||
prevStats := cacheStats
|
||||
defer func() {
|
||||
config = prev
|
||||
cacheStats = prevStats
|
||||
}()
|
||||
|
||||
cfg := &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
TTL: 5,
|
||||
}
|
||||
// MaxMemorySize and MaxEntries left at zero → defaults kick in.
|
||||
EnableCache(cfg)
|
||||
require.NotNil(t, config.Client)
|
||||
ta.Greater(t, GetCacheMaxMemorySize(), int64(0))
|
||||
}
|
||||
|
||||
// TestEnableCache_RedisFallback covers the Redis error → memory fallback branch.
|
||||
func TestEnableCache_RedisFallback_FallsBackToMemory(t *testing.T) {
|
||||
prev := config
|
||||
prevStats := cacheStats
|
||||
defer func() {
|
||||
config = prev
|
||||
cacheStats = prevStats
|
||||
}()
|
||||
|
||||
cfg := &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
TTL: 5,
|
||||
}
|
||||
cfg.Redis.Enable = true
|
||||
cfg.Redis.URL = "127.0.0.1:1" // unreachable port → connection error
|
||||
cfg.Redis.DB = 0
|
||||
|
||||
// Must not panic; should fall back to memory.
|
||||
ta.NotPanics(t, func() { EnableCache(cfg) })
|
||||
require.NotNil(t, config.Client, "fallback memory client must be set")
|
||||
|
||||
// Verify it actually works as a memory cache.
|
||||
CacheStore("fallback-key", []byte("fallback-val"))
|
||||
got := CacheLookup("fallback-key")
|
||||
ta.Equal(t, []byte("fallback-val"), got)
|
||||
}
|
||||
|
||||
// TestCacheStore_Uninitialized covers the early-return + log branch in CacheStore (line 238-242).
|
||||
func TestCacheStore_Uninitialized_DoesNotPanic(t *testing.T) {
|
||||
prev := config
|
||||
config = &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
Client: nil, // IsCacheInitialized() returns false
|
||||
}
|
||||
defer func() { config = prev }()
|
||||
|
||||
ta.NotPanics(t, func() {
|
||||
CacheStore("any-key", []byte("any-val"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestCacheClear_Uninitialized covers the early-return in CacheClear.
|
||||
func TestCacheClear_Uninitialized_DoesNotPanic(t *testing.T) {
|
||||
prev := config
|
||||
config = nil
|
||||
defer func() { config = prev }()
|
||||
|
||||
ta.NotPanics(t, func() { CacheClear() })
|
||||
}
|
||||
|
||||
// TestCacheDelete_ZeroStats covers the CAS loop branch where CachedQueries is already 0.
|
||||
func TestCacheDelete_ZeroStats_DoesNotDecrementBelowZero(t *testing.T) {
|
||||
defer withFreshMemoryCache(t, 5*time.Minute)()
|
||||
cacheStats.CachedQueries = 0 // already at zero
|
||||
|
||||
// Should not panic and stats should stay at 0.
|
||||
CacheDelete("nonexistent-key")
|
||||
ta.Equal(t, int64(0), cacheStats.CachedQueries)
|
||||
}
|
||||
|
||||
// TestEnableCache_Redis_HappyPath covers successful Redis init via miniredis.
|
||||
func TestEnableCache_Redis_HappyPath_StoresAndRetrieves(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
require.NoError(t, err)
|
||||
defer mr.Close()
|
||||
|
||||
prev := config
|
||||
prevStats := cacheStats
|
||||
defer func() {
|
||||
config = prev
|
||||
cacheStats = prevStats
|
||||
}()
|
||||
|
||||
cfg := &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
TTL: 5,
|
||||
}
|
||||
cfg.Redis.Enable = true
|
||||
cfg.Redis.URL = mr.Addr()
|
||||
cfg.Redis.DB = 0
|
||||
EnableCache(cfg)
|
||||
|
||||
require.True(t, IsCacheInitialized())
|
||||
CacheStore("r-key", []byte("r-val"))
|
||||
ta.Equal(t, []byte("r-val"), CacheLookup("r-key"))
|
||||
|
||||
// GetCacheMemoryUsage and GetCacheMaxMemorySize via Redis wrapper.
|
||||
ta.GreaterOrEqual(t, GetCacheMemoryUsage(), int64(0))
|
||||
ta.GreaterOrEqual(t, GetCacheMaxMemorySize(), int64(0))
|
||||
}
|
||||
Vendored
+34
-19
@@ -52,13 +52,9 @@ func NewLRUMemoryCache(maxMemorySize, maxEntries int64) *LRUMemoryCache {
|
||||
|
||||
// Set adds or updates an entry in the cache
|
||||
func (c *LRUMemoryCache) Set(key string, value []byte, ttl time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Calculate expiry time
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
|
||||
// Check if we should compress
|
||||
// Compress OUTSIDE the lock — gzip is CPU-bound and pool ops are
|
||||
// goroutine-safe. Result is just a byte slice, safe to hand to the
|
||||
// critical section below.
|
||||
compressed := false
|
||||
finalValue := value
|
||||
if len(value) > 1024 { // Compress if larger than 1KB
|
||||
@@ -69,6 +65,10 @@ func (c *LRUMemoryCache) Set(key string, value []byte, ttl time.Duration) {
|
||||
}
|
||||
|
||||
entrySize := int64(len(key) + len(finalValue) + 64) // 64 bytes overhead estimate
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Check if key exists
|
||||
if existing, exists := c.entries[key]; exists {
|
||||
@@ -107,34 +107,49 @@ func (c *LRUMemoryCache) Set(key string, value []byte, ttl time.Duration) {
|
||||
|
||||
// Get retrieves a value from the cache
|
||||
func (c *LRUMemoryCache) Get(key string) ([]byte, bool) {
|
||||
// Snapshot the stored bytes under the lock, then release before
|
||||
// decompressing — gzip is CPU-bound and must not serialise other ops.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entry, exists := c.entries[key]
|
||||
if !exists {
|
||||
c.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
// Check if expired (must use the entry's stored expiry while locked)
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
c.removeEntry(entry)
|
||||
c.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Move to front (most recently used)
|
||||
c.evictList.MoveToFront(entry.element)
|
||||
|
||||
// Decompress if needed
|
||||
if entry.compressed {
|
||||
if decompressed, err := c.decompress(entry.value); err == nil {
|
||||
return decompressed, true
|
||||
}
|
||||
// If decompression fails, remove the entry
|
||||
c.removeEntry(entry)
|
||||
return nil, false
|
||||
if !entry.compressed {
|
||||
// Uncompressed payload is immutable once stored, safe to return directly.
|
||||
value := entry.value
|
||||
c.mu.Unlock()
|
||||
return value, true
|
||||
}
|
||||
|
||||
return entry.value, true
|
||||
// Snapshot compressed bytes locally, drop lock, then decompress.
|
||||
compressedBytes := entry.value
|
||||
c.mu.Unlock()
|
||||
|
||||
decompressed, err := c.decompress(compressedBytes)
|
||||
if err == nil {
|
||||
return decompressed, true
|
||||
}
|
||||
|
||||
// Decompression failed — re-acquire lock to remove the bad entry,
|
||||
// but only if it still exists and still points at the same payload.
|
||||
c.mu.Lock()
|
||||
if cur, ok := c.entries[key]; ok && cur == entry {
|
||||
c.removeEntry(cur)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Delete removes an entry from the cache
|
||||
|
||||
Vendored
+334
@@ -0,0 +1,334 @@
|
||||
package libpack_cache_redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func newTestRedis(t *testing.T) (*RedisConfig, *miniredis.Miniredis) {
|
||||
t.Helper()
|
||||
s, err := miniredis.Run()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
rc, err := New(&RedisClientConfig{
|
||||
RedisServer: s.Addr(),
|
||||
Prefix: "pfx:",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return rc, s
|
||||
}
|
||||
|
||||
func newTestWrapper(t *testing.T) (*CacheWrapper, *miniredis.Miniredis) {
|
||||
t.Helper()
|
||||
rc, s := newTestRedis(t)
|
||||
w := NewCacheWrapper(rc, libpack_logger.New())
|
||||
return w, s
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// New — connection failure path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNew_ConnectionFailure_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := New(&RedisClientConfig{
|
||||
RedisServer: "127.0.0.1:1", // nothing listens here
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — GetMemoryUsage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetMemoryUsage_ConnectedServer_ReturnsZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, _ := newTestRedis(t)
|
||||
got := rc.GetMemoryUsage()
|
||||
// Implementation always returns 0 as a placeholder; assert the contract.
|
||||
assert.Equal(t, int64(0), got)
|
||||
}
|
||||
|
||||
func TestGetMemoryUsage_ClosedServer_ReturnsZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
s.Close() // simulate disconnection before cleanup fires
|
||||
got := rc.GetMemoryUsage()
|
||||
assert.Equal(t, int64(0), got)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — GetMaxMemorySize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetMaxMemorySize_AlwaysZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, _ := newTestRedis(t)
|
||||
assert.Equal(t, int64(0), rc.GetMaxMemorySize())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — Get error path (closed server)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGet_ClosedServer_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
// Set a key while server is up, then close.
|
||||
require.NoError(t, rc.Set("k", []byte("v"), 0))
|
||||
s.Close()
|
||||
|
||||
_, found, err := rc.Get("k")
|
||||
assert.Error(t, err)
|
||||
assert.False(t, found)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — CountQueries error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCountQueries_ClosedServer_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
s.Close()
|
||||
|
||||
count, err := rc.CountQueries()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — CountQueriesWithPattern error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCountQueriesWithPattern_ClosedServer_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
s.Close()
|
||||
|
||||
count, err := rc.CountQueriesWithPattern("*")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — TTL=0 (no expiry) vs expired key
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGet_MissingKey_ReturnsFalseNoError(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, _ := newTestRedis(t)
|
||||
val, found, err := rc.Get("nonexistent-key-xyz")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, found)
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestSet_TTLZero_KeyPersists(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
require.NoError(t, rc.Set("persist", []byte("yes"), 0))
|
||||
s.FastForward(24 * time.Hour)
|
||||
_, found, err := rc.Get("persist")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestSet_WithTTL_KeyExpires(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
require.NoError(t, rc.Set("expires", []byte("yes"), 1*time.Second))
|
||||
s.FastForward(2 * time.Second)
|
||||
_, found, err := rc.Get("expires")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, found)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — large value round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSet_LargeValue_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, _ := newTestRedis(t)
|
||||
large := make([]byte, 1<<16) // 64 KB
|
||||
for i := range large {
|
||||
large[i] = byte(i % 251)
|
||||
}
|
||||
require.NoError(t, rc.Set("big", large, 0))
|
||||
got, found, err := rc.Get("big")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, large, got)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redis.go — prefix isolation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestPrerendKeyName_PrefixIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := miniredis.Run()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
rc1, err := New(&RedisClientConfig{RedisServer: s.Addr(), Prefix: "a:"})
|
||||
require.NoError(t, err)
|
||||
rc2, err := New(&RedisClientConfig{RedisServer: s.Addr(), Prefix: "b:"})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, rc1.Set("key", []byte("one"), 0))
|
||||
require.NoError(t, rc2.Set("key", []byte("two"), 0))
|
||||
|
||||
v1, ok1, err1 := rc1.Get("key")
|
||||
assert.NoError(t, err1)
|
||||
assert.True(t, ok1)
|
||||
assert.Equal(t, []byte("one"), v1)
|
||||
|
||||
v2, ok2, err2 := rc2.Get("key")
|
||||
assert.NoError(t, err2)
|
||||
assert.True(t, ok2)
|
||||
assert.Equal(t, []byte("two"), v2)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wrapper.go — NewCacheWrapper with explicit logger
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNewCacheWrapper_WithLogger_UsesIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, _ := newTestRedis(t)
|
||||
logger := &libpack_logger.Logger{}
|
||||
w := NewCacheWrapper(rc, logger)
|
||||
assert.NotNil(t, w)
|
||||
}
|
||||
|
||||
func TestNewCacheWrapper_NilLogger_DoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, _ := newTestRedis(t)
|
||||
// NewCacheWrapper substitutes a zero-value Logger when nil is passed.
|
||||
// Only verify construction succeeds; don't exercise error paths through
|
||||
// this wrapper because zero-value Logger.output is nil and would panic.
|
||||
w := NewCacheWrapper(rc, nil)
|
||||
assert.NotNil(t, w)
|
||||
// Happy-path operations are safe even with the zero-value logger.
|
||||
w.Set("probe", []byte("ok"), 0)
|
||||
got, found := w.Get("probe")
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, []byte("ok"), got)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wrapper.go — Set / Get / Delete / Clear happy paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestWrapper_SetAndGet_HappyPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
w.Set("wkey", []byte("wval"), 0)
|
||||
got, found := w.Get("wkey")
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, []byte("wval"), got)
|
||||
}
|
||||
|
||||
func TestWrapper_Get_MissingKey_ReturnsFalse(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
val, found := w.Get("ghost")
|
||||
assert.False(t, found)
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestWrapper_Delete_RemovesKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
w.Set("del", []byte("gone"), 0)
|
||||
w.Delete("del")
|
||||
_, found := w.Get("del")
|
||||
assert.False(t, found)
|
||||
}
|
||||
|
||||
func TestWrapper_Clear_RemovesAllKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
w.Set("a", []byte("1"), 0)
|
||||
w.Set("b", []byte("2"), 0)
|
||||
w.Clear()
|
||||
assert.Equal(t, int64(0), w.CountQueries())
|
||||
}
|
||||
|
||||
func TestWrapper_CountQueries_ReturnsCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
w.Set("c1", []byte("x"), 0)
|
||||
w.Set("c2", []byte("y"), 0)
|
||||
assert.Equal(t, int64(2), w.CountQueries())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wrapper.go — GetMemoryUsage / GetMaxMemorySize always 0
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestWrapper_GetMemoryUsage_AlwaysZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
assert.Equal(t, int64(0), w.GetMemoryUsage())
|
||||
}
|
||||
|
||||
func TestWrapper_GetMaxMemorySize_AlwaysZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, _ := newTestWrapper(t)
|
||||
assert.Equal(t, int64(0), w.GetMaxMemorySize())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wrapper.go — error paths via closed server (logs, doesn't panic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestWrapper_Set_ClosedServer_LogsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, s := newTestWrapper(t)
|
||||
s.Close()
|
||||
// Must not panic; error is swallowed and logged.
|
||||
w.Set("k", []byte("v"), 0)
|
||||
}
|
||||
|
||||
func TestWrapper_Get_ClosedServer_ReturnsFalse(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, s := newTestWrapper(t)
|
||||
s.Close()
|
||||
val, found := w.Get("k")
|
||||
assert.False(t, found)
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestWrapper_Delete_ClosedServer_LogsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, s := newTestWrapper(t)
|
||||
s.Close()
|
||||
w.Delete("k") // must not panic
|
||||
}
|
||||
|
||||
func TestWrapper_Clear_ClosedServer_LogsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, s := newTestWrapper(t)
|
||||
s.Close()
|
||||
w.Clear() // must not panic
|
||||
}
|
||||
|
||||
func TestWrapper_CountQueries_ClosedServer_ReturnsZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
w, s := newTestWrapper(t)
|
||||
s.Close()
|
||||
assert.Equal(t, int64(0), w.CountQueries())
|
||||
}
|
||||
Reference in New Issue
Block a user