lolly/internal/cache/cache_test.go
xfy f2352ab9cc docs(config,stream,logging,handler,proxy,cache,server,ssl,middleware): 为核心模块添加详细 GoDoc 文档注释
- config: 为 Config 和所有子配置结构添加完整文档,包含使用示例和注意事项
- stream: 为负载均衡器和服务器添加详细的参数、返回值和功能说明
- logging: 为日志格式化和输出函数添加文档,说明支持的变量替换
- handler: 为路由器、静态文件和 sendfile 处理器添加文档
- proxy: 为健康检查器和代理功能添加完整文档
- cache/server/ssl/middleware: 补充相关模块的文档注释
- config.example.yaml: 添加可信代理配置、加密套件示例,更新压缩级别说明

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-07 15:36:09 +08:00

358 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package cache 提供缓存功能的测试。
//
// 该文件测试缓存模块的各项功能,包括:
// - 文件缓存创建和配置
// - 代理缓存规则和匹配
// - 缓存设置和获取
// - 过期和淘汰策略
// - 路径匹配功能
//
// 作者xfy
package cache
import (
"testing"
"time"
)
func TestNewFileCache(t *testing.T) {
fc := NewFileCache(100, 1024*1024, 30*time.Second)
if fc == nil {
t.Error("Expected non-nil FileCache")
}
}
func TestFileCacheSetGet(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
path := "/test/file.txt"
data := []byte("Hello, World!")
err := fc.Set(path, data, int64(len(data)), time.Now())
if err != nil {
t.Errorf("Set() error: %v", err)
}
entry, ok := fc.Get(path)
if !ok {
t.Error("Expected to find cached entry")
}
if string(entry.Data) != "Hello, World!" {
t.Errorf("Expected data 'Hello, World!', got %s", entry.Data)
}
}
func TestFileCacheDelete(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
_ = fc.Set("/test.txt", []byte("data"), 4, time.Now())
fc.Delete("/test.txt")
_, ok := fc.Get("/test.txt")
if ok {
t.Error("Expected entry to be deleted")
}
}
func TestFileCacheLRUEviction(t *testing.T) {
// 最大 3 个条目
fc := NewFileCache(3, 0, 1*time.Hour)
_ = fc.Set("/a", []byte("a"), 1, time.Now())
_ = fc.Set("/b", []byte("b"), 1, time.Now())
_ = fc.Set("/c", []byte("c"), 1, time.Now())
// 再添加一个,应该淘汰 /a
_ = fc.Set("/d", []byte("d"), 1, time.Now())
_, ok := fc.Get("/a")
if ok {
t.Error("Expected /a to be evicted")
}
// b, c, d 应该还在
for _, path := range []string{"b", "c", "d"} {
_, ok := fc.Get("/" + path)
if !ok {
t.Errorf("Expected /%s to exist", path)
}
}
}
func TestFileCacheSizeEviction(t *testing.T) {
// 最大 10 字节
fc := NewFileCache(0, 10, 1*time.Hour)
_ = fc.Set("/a", []byte("12345"), 5, time.Now())
_ = fc.Set("/b", []byte("12345"), 5, time.Now())
// 再添加 6 字节,应该淘汰一个
_ = fc.Set("/c", []byte("123456"), 6, time.Now())
stats := fc.Stats()
if stats.Size > 10 {
t.Errorf("Expected size <= 10, got %d", stats.Size)
}
}
func TestFileCacheInactiveEviction(t *testing.T) {
fc := NewFileCache(10, 1024, 100*time.Millisecond)
_ = fc.Set("/test", []byte("data"), 4, time.Now())
// 立即获取应该成功
_, ok := fc.Get("/test")
if !ok {
t.Error("Expected entry to exist")
}
// 等待过期
time.Sleep(150 * time.Millisecond)
// 再次获取应该失败(因过期被删除)
_, ok = fc.Get("/test")
if ok {
t.Error("Expected entry to be expired")
}
}
func TestFileCacheClear(t *testing.T) {
fc := NewFileCache(10, 1024, 1*time.Hour)
_ = fc.Set("/a", []byte("a"), 1, time.Now())
_ = fc.Set("/b", []byte("b"), 1, time.Now())
fc.Clear()
stats := fc.Stats()
if stats.Entries != 0 {
t.Errorf("Expected 0 entries after clear, got %d", stats.Entries)
}
}
func TestFileCacheStats(t *testing.T) {
fc := NewFileCache(100, 1024, 1*time.Hour)
_ = fc.Set("/a", []byte("12345"), 5, time.Now())
_ = fc.Set("/b", []byte("12345"), 5, time.Now())
stats := fc.Stats()
if stats.Entries != 2 {
t.Errorf("Expected 2 entries, got %d", stats.Entries)
}
if stats.Size != 10 {
t.Errorf("Expected size 10, got %d", stats.Size)
}
}
func TestNewProxyCache(t *testing.T) {
rules := []ProxyCacheRule{
{Path: "/api/", Methods: []string{"GET"}, MaxAge: 10 * time.Minute},
}
pc := NewProxyCache(rules, true, 60*time.Second)
if pc == nil {
t.Error("Expected non-nil ProxyCache")
}
}
func TestProxyCacheSetGet(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
key := "test-key"
data := []byte("response body")
headers := map[string]string{"Content-Type": "application/json"}
pc.Set(key, data, headers, 200, 10*time.Minute)
entry, ok, stale := pc.Get(key)
if !ok {
t.Error("Expected to find cached entry")
}
if stale {
t.Error("Expected entry to be fresh")
}
if string(entry.Data) != "response body" {
t.Errorf("Expected data 'response body', got %s", entry.Data)
}
if entry.Status != 200 {
t.Errorf("Expected status 200, got %d", entry.Status)
}
}
func TestProxyCacheExpiration(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
key := "expire-test"
pc.Set(key, []byte("data"), nil, 200, 100*time.Millisecond)
// 立即获取应该成功
_, ok, _ := pc.Get(key)
if !ok {
t.Error("Expected entry to exist")
}
// 等待过期
time.Sleep(150 * time.Millisecond)
_, ok, _ = pc.Get(key)
if ok {
t.Error("Expected entry to be expired")
}
}
func TestProxyCacheStaleWhileRevalidate(t *testing.T) {
pc := NewProxyCache(nil, false, 200*time.Millisecond)
key := "stale-test"
pc.Set(key, []byte("data"), nil, 200, 100*time.Millisecond)
// 等待过期但仍在 stale 时间内
time.Sleep(150 * time.Millisecond)
entry, ok, stale := pc.Get(key)
if !ok {
t.Error("Expected stale entry to be usable")
}
if !stale {
t.Error("Expected entry to be marked as stale")
}
if entry == nil {
t.Error("Expected stale entry data")
}
}
func TestProxyCacheLock(t *testing.T) {
pc := NewProxyCache(nil, true, 0)
key := "lock-test"
// 获取锁
ch := pc.AcquireLock(key)
if ch != nil {
t.Error("Expected to acquire lock (nil chan)")
}
// 第二次获取应该返回等待 chan
ch2 := pc.AcquireLock(key)
if ch2 == nil {
t.Error("Expected waiting chan when lock is held")
}
// 设置缓存并释放锁
pc.Set(key, []byte("data"), nil, 200, 10*time.Minute)
// 现在应该能获取缓存
_, ok, _ := pc.Get(key)
if !ok {
t.Error("Expected cache entry after lock release")
}
}
func TestProxyCacheMatchRule(t *testing.T) {
rules := []ProxyCacheRule{
{Path: "/api/", Methods: []string{"GET"}, Statuses: []int{200}, MaxAge: 10 * time.Minute},
{Path: "/static/*", Methods: []string{"GET"}, MaxAge: 1 * time.Hour},
}
pc := NewProxyCache(rules, false, 0)
tests := []struct {
path string
method string
status int
want bool
}{
{"api/users", "GET", 200, true},
{"api/users", "POST", 200, false}, // POST 不在 Methods
{"api/users", "GET", 404, false}, // 404 不在 Statuses
{"static/css/style.css", "GET", 200, true},
{"other/path", "GET", 200, false}, // 不匹配任何规则
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
// 添加前缀 / 到 path
fullPath := "/" + tt.path
rule := pc.MatchRule(fullPath, tt.method, tt.status)
if (rule != nil) != tt.want {
t.Errorf("MatchRule(%s, %s, %d) want %v", fullPath, tt.method, tt.status, tt.want)
}
})
}
}
func TestProxyCacheDelete(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
pc.Set("key1", []byte("data"), nil, 200, 10*time.Minute)
pc.Delete("key1")
_, ok, _ := pc.Get("key1")
if ok {
t.Error("Expected entry to be deleted")
}
}
func TestProxyCacheClear(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
pc.Set("a", []byte("a"), nil, 200, 10*time.Minute)
pc.Set("b", []byte("b"), nil, 200, 10*time.Minute)
pc.Clear()
stats := pc.Stats()
if stats.Entries != 0 {
t.Errorf("Expected 0 entries, got %d", stats.Entries)
}
}
func TestPathMatch(t *testing.T) {
tests := []struct {
pattern string
path string
want bool
}{
{"*", "/anything", true},
{"api/*", "/api/users", true},
{"api/*", "/api/", true},
{"api/*", "/other", false},
{"/exact", "/exact", true},
{"/exact", "/exact/other", false},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.path, func(t *testing.T) {
// 添加前缀 / 如果 pattern 没有
pattern := tt.pattern
if pattern[0] != '/' && pattern != "*" {
pattern = "/" + pattern
}
result := pathMatch(pattern, tt.path)
if result != tt.want {
t.Errorf("pathMatch(%s, %s) = %v, want %v", pattern, tt.path, result, tt.want)
}
})
}
}
func TestContains(t *testing.T) {
if !contains([]string{"GET", "POST"}, "GET") {
t.Error("Expected to find GET")
}
if contains([]string{"GET", "POST"}, "DELETE") {
t.Error("Expected not to find DELETE")
}
}
func TestContainsInt(t *testing.T) {
if !containsInt([]int{200, 301, 302}, 200) {
t.Error("Expected to find 200")
}
if containsInt([]int{200, 301, 302}, 404) {
t.Error("Expected not to find 404")
}
}