htmgo/framework/h/cache/ttl_store_test.go

264 lines
5.7 KiB
Go
Raw Normal View History

package cache
import (
"sync"
"testing"
"time"
)
func TestTTLStore_SetAndGet(t *testing.T) {
store := NewTTLStore[string, string]()
defer store.Close()
// Test basic set and get
store.Set("key1", "value1", 1*time.Hour)
val, found := store.Get("key1")
if !found {
t.Error("Expected to find key1")
}
if val != "value1" {
t.Errorf("Expected value1, got %s", val)
}
// Test getting non-existent key
val, found = store.Get("nonexistent")
if found {
t.Error("Expected not to find nonexistent key")
}
if val != "" {
t.Errorf("Expected empty string for non-existent key, got %s", val)
}
}
func TestTTLStore_Expiration(t *testing.T) {
store := NewTTLStore[string, string]()
defer store.Close()
// Set with short TTL
store.Set("shortlived", "value", 100*time.Millisecond)
// Should exist immediately
val, found := store.Get("shortlived")
if !found {
t.Error("Expected to find shortlived key immediately after setting")
}
if val != "value" {
t.Errorf("Expected value, got %s", val)
}
// Wait for expiration
time.Sleep(150 * time.Millisecond)
// Should be expired now
val, found = store.Get("shortlived")
if found {
t.Error("Expected key to be expired")
}
if val != "" {
t.Errorf("Expected empty string for expired key, got %s", val)
}
}
func TestTTLStore_Delete(t *testing.T) {
store := NewTTLStore[string, string]()
defer store.Close()
store.Set("key1", "value1", 1*time.Hour)
// Verify it exists
_, found := store.Get("key1")
if !found {
t.Error("Expected to find key1 before deletion")
}
// Delete it
store.Delete("key1")
// Verify it's gone
_, found = store.Get("key1")
if found {
t.Error("Expected key1 to be deleted")
}
// Delete non-existent key should not panic
store.Delete("nonexistent")
}
func TestTTLStore_Purge(t *testing.T) {
store := NewTTLStore[string, string]()
defer store.Close()
// Add multiple items
store.Set("key1", "value1", 1*time.Hour)
store.Set("key2", "value2", 1*time.Hour)
store.Set("key3", "value3", 1*time.Hour)
// Verify they exist
for i := 1; i <= 3; i++ {
key := "key" + string(rune('0'+i))
_, found := store.Get(key)
if !found {
t.Errorf("Expected to find %s before purge", key)
}
}
// Purge all
store.Purge()
// Verify all are gone
for i := 1; i <= 3; i++ {
key := "key" + string(rune('0'+i))
_, found := store.Get(key)
if found {
t.Errorf("Expected %s to be purged", key)
}
}
}
func TestTTLStore_ConcurrentAccess(t *testing.T) {
store := NewTTLStore[int, int]()
defer store.Close()
const numGoroutines = 100
const numOperations = 1000
var wg sync.WaitGroup
wg.Add(numGoroutines)
// Concurrent writes and reads
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
key := (id * numOperations) + j
store.Set(key, key*2, 1*time.Hour)
// Immediately read it back
val, found := store.Get(key)
if !found {
t.Errorf("Goroutine %d: Expected to find key %d", id, key)
}
if val != key*2 {
t.Errorf("Goroutine %d: Expected value %d, got %d", id, key*2, val)
}
}
}(i)
}
wg.Wait()
}
func TestTTLStore_UpdateExisting(t *testing.T) {
store := NewTTLStore[string, string]()
defer store.Close()
// Set initial value
store.Set("key1", "value1", 100*time.Millisecond)
// Update with new value and longer TTL
store.Set("key1", "value2", 1*time.Hour)
// Verify new value
val, found := store.Get("key1")
if !found {
t.Error("Expected to find key1 after update")
}
if val != "value2" {
t.Errorf("Expected value2, got %s", val)
}
// Wait for original TTL to pass
time.Sleep(150 * time.Millisecond)
// Should still exist with new TTL
val, found = store.Get("key1")
if !found {
t.Error("Expected key1 to still exist with new TTL")
}
if val != "value2" {
t.Errorf("Expected value2, got %s", val)
}
}
func TestTTLStore_CleanupGoroutine(t *testing.T) {
// This test verifies that expired entries are cleaned up automatically
store := NewTTLStore[string, string]()
defer store.Close()
// Add many short-lived entries
for i := 0; i < 100; i++ {
key := "key" + string(rune('0'+i))
store.Set(key, "value", 100*time.Millisecond)
}
// Cast to access internal state for testing
ttlStore := store.(*TTLStore[string, string])
// Check initial count
ttlStore.mutex.RLock()
initialCount := len(ttlStore.cache)
ttlStore.mutex.RUnlock()
if initialCount != 100 {
t.Errorf("Expected 100 entries initially, got %d", initialCount)
}
// Wait for expiration and cleanup cycle
// In test mode, cleanup runs every second
time.Sleep(1200 * time.Millisecond)
// Check that entries were cleaned up
ttlStore.mutex.RLock()
finalCount := len(ttlStore.cache)
ttlStore.mutex.RUnlock()
if finalCount != 0 {
t.Errorf("Expected 0 entries after cleanup, got %d", finalCount)
}
}
func TestTTLStore_Close(t *testing.T) {
store := NewTTLStore[string, string]()
// Close should not panic
store.Close()
// Multiple closes should not panic
store.Close()
store.Close()
}
func TestTTLStore_DifferentTypes(t *testing.T) {
// Test with different key and value types
intStore := NewTTLStore[int, string]()
defer intStore.Close()
intStore.Set(42, "answer", 1*time.Hour)
val, found := intStore.Get(42)
if !found || val != "answer" {
t.Error("Failed with int key")
}
// Test with struct values
type User struct {
ID int
Name string
}
userStore := NewTTLStore[string, User]()
defer userStore.Close()
user := User{ID: 1, Name: "Alice"}
userStore.Set("user1", user, 1*time.Hour)
retrievedUser, found := userStore.Get("user1")
if !found {
t.Error("Failed to retrieve user")
}
if retrievedUser.ID != 1 || retrievedUser.Name != "Alice" {
t.Error("Retrieved user data doesn't match")
}
}