新增功能: - stream 模块: 流式传输支持,优化大文件和实时数据传输 - Goroutine 池: 限制并发数量,减少调度开销 - 优雅升级: 零停机热升级,继承父进程监听器 - sendfile: 零拷贝文件传输,大文件直接从内核传输 重构改进: - App 结构体封装,支持热升级和信号处理 - 配置结构字段对齐和代码清理 - 完善错误处理和日志记录 Co-Authored-By: Claude <noreply@anthropic.com>
348 lines
7.7 KiB
Go
348 lines
7.7 KiB
Go
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")
|
|
}
|
|
}
|