mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
5.0 KiB
Go
210 lines
5.0 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCacheBasicOperations(t *testing.T) {
|
|
ctx := context.Background()
|
|
config := DefaultConfig()
|
|
config.DefaultTTL = 100 * time.Millisecond
|
|
config.CleanupInterval = 50 * time.Millisecond
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test Set and Get
|
|
cache.Set(ctx, "key1", "value1")
|
|
if val, ok := cache.Get(ctx, "key1"); !ok || val != "value1" {
|
|
t.Errorf("Expected 'value1', got %v, exists: %v", val, ok)
|
|
}
|
|
|
|
// Test SetWithTTL
|
|
cache.SetWithTTL(ctx, "key2", "value2", 200*time.Millisecond)
|
|
if val, ok := cache.Get(ctx, "key2"); !ok || val != "value2" {
|
|
t.Errorf("Expected 'value2', got %v, exists: %v", val, ok)
|
|
}
|
|
|
|
// Test Delete
|
|
cache.Delete(ctx, "key1")
|
|
if _, ok := cache.Get(ctx, "key1"); ok {
|
|
t.Errorf("Key 'key1' should have been deleted")
|
|
}
|
|
|
|
// Test automatic expiration
|
|
time.Sleep(150 * time.Millisecond)
|
|
if _, ok := cache.Get(ctx, "key1"); ok {
|
|
t.Errorf("Key 'key1' should have expired")
|
|
}
|
|
// key2 should still be valid (200ms TTL)
|
|
if _, ok := cache.Get(ctx, "key2"); !ok {
|
|
t.Errorf("Key 'key2' should still be valid")
|
|
}
|
|
|
|
// Wait for key2 to expire
|
|
time.Sleep(100 * time.Millisecond)
|
|
if _, ok := cache.Get(ctx, "key2"); ok {
|
|
t.Errorf("Key 'key2' should have expired")
|
|
}
|
|
|
|
// Test Clear
|
|
cache.Set(ctx, "key3", "value3")
|
|
cache.Clear(ctx)
|
|
if _, ok := cache.Get(ctx, "key3"); ok {
|
|
t.Errorf("Cache should be empty after Clear()")
|
|
}
|
|
}
|
|
|
|
func TestCacheEviction(t *testing.T) {
|
|
ctx := context.Background()
|
|
config := DefaultConfig()
|
|
config.MaxItems = 5
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add 5 items (max capacity)
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
cache.Set(ctx, key, i)
|
|
}
|
|
|
|
// Verify all 5 items are in the cache
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
if _, ok := cache.Get(ctx, key); !ok {
|
|
t.Errorf("Key '%s' should be in the cache", key)
|
|
}
|
|
}
|
|
|
|
// Add 2 more items to trigger eviction
|
|
cache.Set(ctx, "keyA", "valueA")
|
|
cache.Set(ctx, "keyB", "valueB")
|
|
|
|
// Verify size is still within limits
|
|
if cache.Size() > int64(config.MaxItems) {
|
|
t.Errorf("Cache size %d exceeds limit %d", cache.Size(), config.MaxItems)
|
|
}
|
|
|
|
// Some of the original keys should have been evicted
|
|
evictedCount := 0
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
if _, ok := cache.Get(ctx, key); !ok {
|
|
evictedCount++
|
|
}
|
|
}
|
|
|
|
if evictedCount == 0 {
|
|
t.Errorf("No keys were evicted despite exceeding max items")
|
|
}
|
|
|
|
// The newer keys should still be present
|
|
if _, ok := cache.Get(ctx, "keyA"); !ok {
|
|
t.Errorf("Key 'keyA' should be in the cache")
|
|
}
|
|
if _, ok := cache.Get(ctx, "keyB"); !ok {
|
|
t.Errorf("Key 'keyB' should be in the cache")
|
|
}
|
|
}
|
|
|
|
func TestCacheConcurrency(t *testing.T) {
|
|
ctx := context.Background()
|
|
cache := NewDefault()
|
|
defer cache.Close()
|
|
|
|
const goroutines = 10
|
|
const operationsPerGoroutine = 100
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
baseKey := fmt.Sprintf("worker%d-", id)
|
|
|
|
// Set operations
|
|
for j := 0; j < operationsPerGoroutine; j++ {
|
|
key := fmt.Sprintf("%skey%d", baseKey, j)
|
|
value := fmt.Sprintf("value%d-%d", id, j)
|
|
cache.Set(ctx, key, value)
|
|
}
|
|
|
|
// Get operations
|
|
for j := 0; j < operationsPerGoroutine; j++ {
|
|
key := fmt.Sprintf("%skey%d", baseKey, j)
|
|
val, ok := cache.Get(ctx, key)
|
|
if !ok {
|
|
t.Errorf("Key '%s' should exist in cache", key)
|
|
continue
|
|
}
|
|
expected := fmt.Sprintf("value%d-%d", id, j)
|
|
if val != expected {
|
|
t.Errorf("For key '%s', expected '%s', got '%s'", key, expected, val)
|
|
}
|
|
}
|
|
|
|
// Delete half the keys
|
|
for j := 0; j < operationsPerGoroutine/2; j++ {
|
|
key := fmt.Sprintf("%skey%d", baseKey, j)
|
|
cache.Delete(ctx, key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify size and deletion
|
|
var totalKeysExpected int64 = goroutines * operationsPerGoroutine / 2
|
|
if cache.Size() != totalKeysExpected {
|
|
t.Errorf("Expected cache size to be %d, got %d", totalKeysExpected, cache.Size())
|
|
}
|
|
}
|
|
|
|
func TestEvictionCallback(t *testing.T) {
|
|
ctx := context.Background()
|
|
evicted := make(map[string]interface{})
|
|
evictedMu := sync.Mutex{}
|
|
|
|
config := DefaultConfig()
|
|
config.DefaultTTL = 50 * time.Millisecond
|
|
config.CleanupInterval = 25 * time.Millisecond
|
|
config.OnEviction = func(key string, value interface{}) {
|
|
evictedMu.Lock()
|
|
evicted[key] = value
|
|
evictedMu.Unlock()
|
|
}
|
|
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add items
|
|
cache.Set(ctx, "key1", "value1")
|
|
cache.Set(ctx, "key2", "value2")
|
|
|
|
// Manually delete
|
|
cache.Delete(ctx, "key1")
|
|
|
|
// Verify manual deletion triggered callback
|
|
time.Sleep(10 * time.Millisecond) // Small delay to ensure callback processed
|
|
evictedMu.Lock()
|
|
if evicted["key1"] != "value1" {
|
|
t.Errorf("Eviction callback not triggered for manual deletion")
|
|
}
|
|
evictedMu.Unlock()
|
|
|
|
// Wait for automatic expiration
|
|
time.Sleep(60 * time.Millisecond)
|
|
|
|
// Verify TTL expiration triggered callback
|
|
evictedMu.Lock()
|
|
if evicted["key2"] != "value2" {
|
|
t.Errorf("Eviction callback not triggered for TTL expiration")
|
|
}
|
|
evictedMu.Unlock()
|
|
}
|