lolly/internal/resolver/resolver_test.go
xfy bd5a2c0202 feat(resolver): 为 DNS 缓存添加 LRU 淘汰机制
将 sync.Map 替换为 map + RWMutex,实现基于 cache_size 的 LRU 淘汰:
- 添加 lruOrder 链表追踪访问顺序
- 新增 storeCache 方法处理缓存存储和淘汰
- 添加 evictLRULocked、moveToFrontLocked 辅助方法
- 新增 TestCacheSizeLimit、TestLRUEvictionOrder 等测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 11:31:34 +08:00

737 lines
18 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 resolver
import (
"context"
"fmt"
"sync"
"testing"
"time"
"rua.plus/lolly/internal/config"
)
// TestNewResolver 测试解析器创建。
func TestNewResolver(t *testing.T) {
// 测试启用状态
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
IPv6: false,
}
r := New(cfg)
if r == nil {
t.Fatal("New() should return non-nil resolver")
}
// 验证是 DNSResolver 类型
dnsR, ok := r.(*DNSResolver)
if !ok {
t.Fatal("New() should return *DNSResolver when enabled")
}
if !dnsR.config.Enabled {
t.Error("config.Enabled should be true")
}
// 测试禁用状态
cfgDisabed := &config.ResolverConfig{
Enabled: false,
}
rDisabled := New(cfgDisabed)
if _, ok := rDisabled.(*noopResolver); !ok {
t.Error("New() should return *noopResolver when disabled")
}
}
// TestNewResolverDefaults 测试默认值设置。
func TestNewResolverDefaults(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
// 不设置 Valid 和 Timeout
}
r := New(cfg).(*DNSResolver)
if r.config.Valid != 30*time.Second {
t.Errorf("expected default Valid=30s, got %v", r.config.Valid)
}
if r.config.Timeout != 5*time.Second {
t.Errorf("expected default Timeout=5s, got %v", r.config.Timeout)
}
}
// TestLookupHostWithIP 测试 IP 地址直接返回。
func TestLookupHostWithIP(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
}
r := New(cfg).(*DNSResolver)
// 测试 IPv4 地址直接返回
ips, err := r.LookupHost(context.Background(), "127.0.0.1")
if err != nil {
t.Fatalf("LookupHost failed: %v", err)
}
if len(ips) != 1 || ips[0] != "127.0.0.1" {
t.Errorf("expected [127.0.0.1], got %v", ips)
}
// 测试 IPv6 地址直接返回
ips, err = r.LookupHost(context.Background(), "::1")
if err != nil {
t.Fatalf("LookupHost failed: %v", err)
}
if len(ips) != 1 || ips[0] != "::1" {
t.Errorf("expected [::1], got %v", ips)
}
}
// TestCache 测试缓存功能。
func TestCache(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{}, // 空地址,使用系统 DNS
Valid: 1 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
}
r := New(cfg).(*DNSResolver)
// 模拟缓存条目
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1", "192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 测试缓存命中
ctx := context.Background()
ips, err := r.LookupHostWithCache(ctx, "test.example.com")
if err != nil {
t.Fatalf("LookupHostWithCache failed: %v", err)
}
if len(ips) != 2 {
t.Errorf("expected 2 IPs, got %d", len(ips))
}
// 验证缓存命中统计
if r.GetCacheHits() != 1 {
t.Errorf("expected 1 cache hit, got %d", r.GetCacheHits())
}
// 测试缓存过期
// 更新缓存条目为过期
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(-1 * time.Second), // 已过期
})
// 由于使用系统 DNS可能会失败但应该尝试查询
_, _ = r.LookupHostWithCache(ctx, "test.example.com")
// 应该有缓存未命中(因为过期了)
if r.GetCacheMisses() != 1 {
t.Errorf("expected 1 cache miss, got %d", r.GetCacheMisses())
}
}
// TestIsCached 测试缓存状态检查。
func TestIsCached(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加未过期的缓存
r.storeCache("active.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 添加已过期的缓存
r.storeCache("expired.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(-1 * time.Second),
})
if !r.IsCached("active.example.com") {
t.Error("IsCached should return true for active entry")
}
if r.IsCached("expired.example.com") {
t.Error("IsCached should return false for expired entry")
}
if r.IsCached("unknown.example.com") {
t.Error("IsCached should return false for unknown entry")
}
}
// TestCacheHitRate 测试缓存命中率计算。
func TestCacheHitRate(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 初始命中率应为 0
if r.GetHitRate() != 0 {
t.Errorf("expected 0 hit rate, got %f", r.GetHitRate())
}
// 模拟缓存命中
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 3 次命中
for i := 0; i < 3; i++ {
_, _ = r.LookupHostWithCache(context.Background(), "test.example.com")
}
// 1 次未命中(新域名)
_, _ = r.LookupHostWithCache(context.Background(), "unknown.example.com")
// 命中率应为 3/4 = 0.75
hitRate := r.GetHitRate()
if hitRate != 0.75 {
t.Errorf("expected 0.75 hit rate, got %f", hitRate)
}
}
// TestStats 测试统计信息。
func TestStats(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加缓存条目
r.storeCache("test1.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("test2.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test1.example.com"] = struct{}{}
r.refreshHosts["test2.example.com"] = struct{}{}
r.mu.Unlock()
// 触发缓存命中
_, _ = r.LookupHostWithCache(context.Background(), "test1.example.com")
stats := r.Stats()
if stats.CacheHits != 1 {
t.Errorf("expected 1 cache hit, got %d", stats.CacheHits)
}
if stats.CacheEntries != 2 {
t.Errorf("expected 2 cache entries, got %d", stats.CacheEntries)
}
}
// TestResetStats 测试统计重置。
func TestResetStats(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加统计数据
r.hits.Store(10)
r.misses.Store(5)
r.errors.Store(2)
r.latencyNs.Store(1000000)
r.count.Store(10)
r.ResetStats()
if r.GetCacheHits() != 0 {
t.Errorf("expected 0 hits after reset, got %d", r.GetCacheHits())
}
if r.GetCacheMisses() != 0 {
t.Errorf("expected 0 misses after reset, got %d", r.GetCacheMisses())
}
if r.GetResolveErrors() != 0 {
t.Errorf("expected 0 errors after reset, got %d", r.GetResolveErrors())
}
if r.GetAverageLatency() != 0 {
t.Errorf("expected 0 latency after reset, got %v", r.GetAverageLatency())
}
}
// TestStartStop 测试启动和停止。
func TestStartStop(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 启动
err := r.Start()
if err != nil {
t.Fatalf("Start failed: %v", err)
}
if !r.started.Load() {
t.Error("resolver should be started")
}
// 重复启动不应报错
err = r.Start()
if err != nil {
t.Errorf("Start() should not error when already started: %v", err)
}
// 停止
err = r.Stop()
if err != nil {
t.Fatalf("Stop failed: %v", err)
}
if r.started.Load() {
t.Error("resolver should be stopped")
}
}
// TestDeleteCacheEntry 测试删除缓存条目。
func TestDeleteCacheEntry(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加缓存
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 删除
r.DeleteCacheEntry("test.example.com")
// 验证删除
if r.IsCached("test.example.com") {
t.Error("cache entry should be deleted")
}
}
// TestClearCache 测试清空缓存。
func TestClearCache(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加多个缓存
for i := 0; i < 5; i++ {
host := fmt.Sprintf("test%d.example.com", i)
r.storeCache(host, &DNSCacheEntry{
IPs: []string{fmt.Sprintf("192.168.1.%d", i)},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts[host] = struct{}{}
r.mu.Unlock()
}
// 清空
r.ClearCache()
// 验证
stats := r.GetCacheStats()
if stats.Entries != 0 {
t.Errorf("expected 0 entries after clear, got %d", stats.Entries)
}
}
// TestConcurrentAccess 测试并发访问。
func TestConcurrentAccess(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加测试缓存
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 并发读取
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _ = r.LookupHostWithCache(context.Background(), "test.example.com")
}()
}
wg.Wait()
// 验证没有竞争条件导致的 panic
if r.GetCacheHits() != 100 {
t.Errorf("expected 100 cache hits, got %d", r.GetCacheHits())
}
}
// TestNoopResolver 测试空解析器。
func TestNoopResolver(t *testing.T) {
nr := &noopResolver{}
ctx := context.Background()
_, err := nr.LookupHost(ctx, "example.com")
if err == nil {
t.Error("noopResolver.LookupHost should return error")
}
_, err = nr.LookupHostWithCache(ctx, "example.com")
if err == nil {
t.Error("noopResolver.LookupHostWithCache should return error")
}
if err := nr.Refresh("example.com"); err != nil {
t.Error("noopResolver.Refresh should not return error")
}
if err := nr.Start(); err != nil {
t.Error("noopResolver.Start should not return error")
}
if err := nr.Stop(); err != nil {
t.Error("noopResolver.Stop should not return error")
}
stats := nr.Stats()
if stats.CacheHits != 0 || stats.CacheMisses != 0 {
t.Error("noopResolver.Stats should return empty stats")
}
}
// TestResolverConfigValidate 测试配置验证。
func TestResolverConfigValidate(t *testing.T) {
// 禁用状态不验证
cfg := &config.ResolverConfig{Enabled: false}
if err := cfg.Validate(); err != nil {
t.Errorf("disabled resolver should pass validation: %v", err)
}
// 启用但没有地址
cfg = &config.ResolverConfig{
Enabled: true,
}
if err := cfg.Validate(); err == nil {
t.Error("enabled resolver without addresses should fail")
}
// 有效配置
cfg = &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
IPv6: false,
}
if err := cfg.Validate(); err != nil {
t.Errorf("valid config should pass: %v", err)
}
// TTL 太短
cfg = &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 500 * time.Millisecond,
}
if err := cfg.Validate(); err == nil {
t.Error("valid < 1s should fail")
}
// IPv4 和 IPv6 都禁用
cfg = &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
IPv4: false,
IPv6: false,
}
if err := cfg.Validate(); err == nil {
t.Error("both IPv4 and IPv6 disabled should fail")
}
}
// TestResolverConfigTTL 测试 TTL 方法。
func TestResolverConfigTTL(t *testing.T) {
cfg := &config.ResolverConfig{
Valid: 60 * time.Second,
}
if cfg.TTL() != 60*time.Second {
t.Errorf("expected TTL=60s, got %v", cfg.TTL())
}
}
// TestRefresh 测试刷新方法。
func TestRefresh(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 测试 IP 地址直接返回(无 DNS 查询)
err := r.Refresh("127.0.0.1")
if err != nil {
t.Errorf("Refresh for IP should succeed: %v", err)
}
}
// TestCacheStats 测试缓存统计。
func TestCacheStats(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 1 * time.Second, // 短 TTL 用于测试过期
}
r := New(cfg).(*DNSResolver)
// 添加活跃缓存
r.storeCache("active.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 添加过期缓存
r.storeCache("expired.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(-1 * time.Second),
})
r.mu.Lock()
r.refreshHosts["active.example.com"] = struct{}{}
r.refreshHosts["expired.example.com"] = struct{}{}
r.mu.Unlock()
// 设置命中/未命中统计
r.hits.Store(10)
r.misses.Store(5)
stats := r.GetCacheStats()
if stats.Hits != 10 {
t.Errorf("expected 10 hits, got %d", stats.Hits)
}
if stats.Misses != 5 {
t.Errorf("expected 5 misses, got %d", stats.Misses)
}
if stats.Entries != 2 {
t.Errorf("expected 2 entries, got %d", stats.Entries)
}
if stats.Expired != 1 {
t.Errorf("expected 1 expired, got %d", stats.Expired)
}
}
// TestCacheSizeLimit 测试缓存大小限制。
func TestCacheSizeLimit(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3, // 限制 3 个条目
}
r := New(cfg).(*DNSResolver)
// 添加 5 个缓存条目,应淘汰 2 个
for i := 0; i < 5; i++ {
host := fmt.Sprintf("host%d.example.com", i)
r.storeCache(host, &DNSCacheEntry{
IPs: []string{fmt.Sprintf("192.168.1.%d", i)},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
}
// 验证缓存条目数不超过限制
stats := r.GetCacheStats()
if stats.Entries > 3 {
t.Errorf("expected at most 3 entries with CacheSize=3, got %d", stats.Entries)
}
// 验证最早添加的条目被淘汰LRU
if r.IsCached("host0.example.com") {
t.Error("host0.example.com should be evicted (oldest)")
}
if r.IsCached("host1.example.com") {
t.Error("host1.example.com should be evicted (second oldest)")
}
// 验证最新添加的条目存在
if !r.IsCached("host2.example.com") {
t.Error("host2.example.com should be cached")
}
if !r.IsCached("host3.example.com") {
t.Error("host3.example.com should be cached")
}
if !r.IsCached("host4.example.com") {
t.Error("host4.example.com should be cached")
}
}
// TestCacheSizeZero 测试 cache_size=0 时无限制。
func TestCacheSizeZero(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 0, // 无限制
}
r := New(cfg).(*DNSResolver)
// 添加大量缓存条目
for i := 0; i < 100; i++ {
host := fmt.Sprintf("host%d.example.com", i)
r.storeCache(host, &DNSCacheEntry{
IPs: []string{fmt.Sprintf("192.168.1.%d", i%256)},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
}
// 验证所有条目都存在
stats := r.GetCacheStats()
if stats.Entries != 100 {
t.Errorf("expected 100 entries with CacheSize=0, got %d", stats.Entries)
}
}
// TestLRUEvictionOrder 测试 LRU 淘汰顺序。
func TestLRUEvictionOrder(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3,
}
r := New(cfg).(*DNSResolver)
// 添加 3 个条目填满缓存
r.storeCache("a.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("b.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("c.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.3"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 访问 a.example.com 使其变为最新
r.storeCache("a.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 添加新条目,应淘汰 b.example.com最久未使用
r.storeCache("d.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.4"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 验证淘汰顺序
if r.IsCached("b.example.com") {
t.Error("b.example.com should be evicted (least recently used)")
}
if !r.IsCached("a.example.com") {
t.Error("a.example.com should be cached (recently accessed)")
}
if !r.IsCached("c.example.com") {
t.Error("c.example.com should be cached")
}
if !r.IsCached("d.example.com") {
t.Error("d.example.com should be cached (newly added)")
}
}
// TestCacheUpdatePreservesOrder 测试更新已存在条目不触发淘汰。
func TestCacheUpdatePreservesOrder(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3,
}
r := New(cfg).(*DNSResolver)
// 添加 3 个条目填满缓存
r.storeCache("a.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("b.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("c.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.3"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 更新已存在的条目(不应触发淘汰)
r.storeCache("b.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.20"}, // 新 IP
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 验证所有条目仍然存在
stats := r.GetCacheStats()
if stats.Entries != 3 {
t.Errorf("expected 3 entries after update, got %d", stats.Entries)
}
// 验证更新生效
entry, ok := r.GetCacheEntry("b.example.com")
if !ok {
t.Fatal("b.example.com should exist")
}
if len(entry.IPs) != 1 || entry.IPs[0] != "192.168.1.20" {
t.Errorf("expected IP 192.168.1.20, got %v", entry.IPs)
}
}