lolly/internal/resolver/mock_dns_test.go
xfy 12caed5d4e perf(resolver): replace slice-based LRU with container/list for O(1) operations
DNS resolver LRU cache used []string slice for access ordering:
- moveToFrontLocked: O(n) linear scan + slice重组 per cache update
- storeCache held exclusive write lock during entire O(n) operation,
  blocking all concurrent reads

Replace with container/list + map[string]*list.Element:
- storeCache: O(1) MoveToFront via list.Element pointer
- evictLRULocked: O(1) Back() + Remove()
- DeleteCacheEntry: O(1) instead of O(n) scan

This matches the LRU pattern already used by FileCache and
FileInfoCache. Write lock duration reduced from O(n) to O(1),
significantly reducing read contention under mixed workloads.
2026-06-04 00:13:10 +08:00

568 lines
13 KiB
Go
Raw Permalink 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 提供 Mock DNS 测试。
//
// 该文件测试 DNS 解析器的核心功能:
// - queryDNS 成功/超时/失败
// - queryWithResolver 自定义服务器
// - refreshLoop 启动/停止
// - 缓存和 LRU 淘汰
//
// 作者xfy
package resolver
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"rua.plus/lolly/internal/config"
)
// TestMockDNSResolverDisabled 测试禁用状态。
func TestMockDNSResolverDisabled(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: false,
}
resolver := New(cfg)
if resolver == nil {
t.Fatal("New() returned nil")
}
// 禁用状态下应返回错误
_, err := resolver.LookupHost(context.Background(), "example.com")
if err == nil {
t.Error("Expected error for disabled resolver")
}
}
// TestMockDNSResolverEnabled 测试启用状态。
func TestMockDNSResolverEnabled(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
CacheSize: 100,
}
resolver := New(cfg)
if resolver == nil {
t.Fatal("New() returned nil")
}
// 验证类型
if _, ok := resolver.(*DNSResolver); !ok {
t.Error("Expected DNSResolver type")
}
}
// TestMockDNSResolverDefaultValues 测试默认值设置。
func TestMockDNSResolverDefaultValues(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
// 其他字段为零值
}
resolver := New(cfg).(*DNSResolver)
// 验证默认值
if resolver.config.Valid != 30*time.Second {
t.Errorf("Expected default Valid 30s, got %v", resolver.config.Valid)
}
if resolver.config.Timeout != 5*time.Second {
t.Errorf("Expected default Timeout 5s, got %v", resolver.config.Timeout)
}
if !resolver.config.IPv4 {
t.Error("Expected IPv4 to be enabled by default")
}
}
// TestMockDNSLookupHostIPAddress 测试直接 IP 地址。
func TestMockDNSLookupHostIPAddress(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
}
resolver := New(cfg).(*DNSResolver)
// 直接 IP 地址应直接返回
ips, err := resolver.LookupHost(context.Background(), "127.0.0.1")
if err != nil {
t.Fatalf("LookupHost() error: %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 = resolver.LookupHost(context.Background(), "::1")
if err != nil {
t.Fatalf("LookupHost() error for IPv6: %v", err)
}
if len(ips) != 1 || ips[0] != "::1" {
t.Errorf("Expected ['::1'], got %v", ips)
}
}
// TestMockDNSLookupHostWithCache 测试带缓存的解析。
func TestMockDNSLookupHostWithCache(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 第一次查询(缓存未命中)
ips1, err := resolver.LookupHostWithCache(context.Background(), "localhost")
if err != nil {
t.Fatalf("First LookupHostWithCache() error: %v", err)
}
// 第二次查询(缓存命中)
ips2, err := resolver.LookupHostWithCache(context.Background(), "localhost")
if err != nil {
t.Fatalf("Second LookupHostWithCache() error: %v", err)
}
// 结果应一致
if len(ips1) != len(ips2) {
t.Errorf("Cache result mismatch: %v vs %v", ips1, ips2)
}
// 验证缓存命中
stats := resolver.Stats()
if stats.CacheHits < 1 {
t.Error("Expected at least one cache hit")
}
}
// TestMockDNSCacheExpiration 测试缓存过期。
func TestMockDNSCacheExpiration(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 100 * time.Millisecond, // 短 TTL
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 第一次查询
_, err := resolver.LookupHostWithCache(context.Background(), "localhost")
if err != nil {
t.Fatalf("First lookup error: %v", err)
}
// 等待缓存过期
time.Sleep(150 * time.Millisecond)
// 第二次查询应重新解析
_, err = resolver.LookupHostWithCache(context.Background(), "localhost")
if err != nil {
t.Fatalf("Second lookup error: %v", err)
}
// 验证有两次缓存未命中
stats := resolver.Stats()
if stats.CacheMisses < 2 {
t.Errorf("Expected at least 2 cache misses, got %d", stats.CacheMisses)
}
}
// TestMockDNSLRUEviction 测试 LRU 淘汰。
func TestMockDNSLRUEviction(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3, // 只缓存 3 个
}
resolver := New(cfg).(*DNSResolver)
// 添加 3 个条目
hosts := []string{"host1.local", "host2.local", "host3.local"}
for _, host := range hosts {
resolver.storeCache(host, &DNSCacheEntry{
IPs: []string{"127.0.0.1"},
ExpiresAt: time.Now().Add(time.Hour),
})
}
// 验证缓存大小
resolver.mu.RLock()
cacheLen := len(resolver.cache)
resolver.mu.RUnlock()
if cacheLen != 3 {
t.Errorf("Expected 3 cache entries, got %d", cacheLen)
}
// 添加第 4 个条目,应淘汰最旧的
resolver.storeCache("host4.local", &DNSCacheEntry{
IPs: []string{"127.0.0.1"},
ExpiresAt: time.Now().Add(time.Hour),
})
resolver.mu.RLock()
cacheLen = len(resolver.cache)
_, exists := resolver.cache["host1.local"]
resolver.mu.RUnlock()
if cacheLen > 3 {
t.Errorf("Cache should not exceed 3 entries, got %d", cacheLen)
}
if exists {
t.Error("Oldest entry should have been evicted")
}
}
// TestMockDNSRefreshLoop 测试后台刷新循环。
func TestMockDNSRefreshLoop(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 200 * time.Millisecond,
Timeout: 1 * time.Second,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 启动刷新循环
if err := resolver.Start(); err != nil {
t.Fatalf("Start() error: %v", err)
}
// 验证已启动
if !resolver.started.Load() {
t.Error("Resolver should be started")
}
// 添加一个需要刷新的主机
resolver.mu.Lock()
resolver.refreshHosts["localhost"] = struct{}{}
resolver.mu.Unlock()
// 等待刷新
time.Sleep(300 * time.Millisecond)
// 停止
if err := resolver.Stop(); err != nil {
t.Fatalf("Stop() error: %v", err)
}
// 验证已停止
if resolver.started.Load() {
t.Error("Resolver should be stopped")
}
}
// TestMockDNSStartIdempotent 测试重复启动。
func TestMockDNSStartIdempotent(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
}
resolver := New(cfg).(*DNSResolver)
// 第一次启动
if err := resolver.Start(); err != nil {
t.Fatalf("First Start() error: %v", err)
}
// 第二次启动应无操作
if err := resolver.Start(); err != nil {
t.Fatalf("Second Start() error: %v", err)
}
// 清理
_ = resolver.Stop()
}
// TestMockDNSStopIdempotent 测试重复停止。
func TestMockDNSStopIdempotent(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
}
resolver := New(cfg).(*DNSResolver)
// 未启动时停止
if err := resolver.Stop(); err != nil {
t.Fatalf("Stop() on non-started resolver error: %v", err)
}
// 启动
_ = resolver.Start()
// 第一次停止
if err := resolver.Stop(); err != nil {
t.Fatalf("First Stop() error: %v", err)
}
// 第二次停止应无操作
if err := resolver.Stop(); err != nil {
t.Fatalf("Second Stop() error: %v", err)
}
}
// TestMockDNSStatsFunc 测试统计信息。
func TestMockDNSStatsFunc(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 初始统计
stats := resolver.Stats()
if stats.CacheHits != 0 || stats.CacheMisses != 0 {
t.Error("Initial stats should be zero")
}
// 执行查询
_, _ = resolver.LookupHostWithCache(context.Background(), "localhost")
_, _ = resolver.LookupHostWithCache(context.Background(), "localhost")
stats = resolver.Stats()
if stats.CacheMisses < 1 {
t.Error("Expected at least one cache miss")
}
if stats.CacheHits < 1 {
t.Error("Expected at least one cache hit")
}
}
// TestMockDNSQueryDNSTimeout 测试 DNS 查询超时。
func TestMockDNSQueryDNSTimeout(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Timeout: 100 * time.Millisecond,
Addresses: []string{"127.0.0.1:53535"}, // 不存在的 DNS 服务器
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 使用短超时 context
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
start := time.Now()
_, err := resolver.LookupHost(ctx, "example.com")
elapsed := time.Since(start)
// 应该超时
if err == nil {
t.Error("Expected timeout error")
}
if elapsed > 200*time.Millisecond {
t.Errorf("Query took too long: %v", elapsed)
}
}
// TestMockDNSQueryWithResolverSystemDefault 测试使用系统默认 DNS。
func TestMockDNSQueryWithResolverSystemDefault(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
IPv4: true,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 不指定 DNS 服务器,使用系统默认
ips, err := resolver.queryWithResolver(context.Background(), "localhost", "")
if err != nil {
t.Fatalf("queryWithResolver() error: %v", err)
}
if len(ips) == 0 {
t.Error("Expected at least one IP for localhost")
}
}
// TestMockDNSConcurrentAccess 测试并发访问。
func TestMockDNSConcurrentAccess(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
CacheSize: 100,
}
resolver := New(cfg).(*DNSResolver)
var wg sync.WaitGroup
concurrency := 10
iterations := 100
for range concurrency {
wg.Go(func() {
for range iterations {
_, _ = resolver.LookupHostWithCache(context.Background(), "localhost")
}
})
}
wg.Wait()
stats := resolver.Stats()
totalOps := stats.CacheHits + stats.CacheMisses
if totalOps < int64(concurrency*iterations/2) {
t.Errorf("Expected more operations, got hits=%d, misses=%d", stats.CacheHits, stats.CacheMisses)
}
}
// TestMockDNSMoveToFrontLocked 测试 LRU 移动到前端。
func TestMockDNSMoveToFrontLocked(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
resolver.cache = map[string]*DNSCacheEntry{
"a": {},
"b": {},
"c": {},
}
elemA := resolver.lruList.PushFront("a")
elemB := resolver.lruList.PushFront("b")
elemC := resolver.lruList.PushFront("c")
resolver.lruIndex["a"] = elemA
resolver.lruIndex["b"] = elemB
resolver.lruIndex["c"] = elemC
resolver.lruList.MoveToFront(resolver.lruIndex["a"])
order := []string{}
for e := resolver.lruList.Back(); e != nil; e = e.Prev() {
order = append(order, e.Value.(string))
}
expected := []string{"b", "c", "a"}
for i, v := range expected {
if order[i] != v {
t.Errorf("LRU order[%d] = %s, want %s", i, order[i], v)
}
}
}
// TestMockDNSNoopResolver 测试空实现解析器。
func TestMockDNSNoopResolver(t *testing.T) {
var r Resolver = &noopResolver{}
// LookupHost 应返回错误
_, err := r.LookupHost(context.Background(), "example.com")
if err == nil {
t.Error("Expected error from noopResolver.LookupHost")
}
// LookupHostWithCache 应返回错误
_, err = r.LookupHostWithCache(context.Background(), "example.com")
if err == nil {
t.Error("Expected error from noopResolver.LookupHostWithCache")
}
// Refresh 应返回 nil
if err := r.Refresh("example.com"); err != nil {
t.Errorf("noopResolver.Refresh() error: %v", err)
}
// Start 应返回 nil
if err := r.Start(); err != nil {
t.Errorf("noopResolver.Start() error: %v", err)
}
// Stop 应返回 nil
if err := r.Stop(); err != nil {
t.Errorf("noopResolver.Stop() error: %v", err)
}
// Stats 应返回空
stats := r.Stats()
if stats.CacheHits != 0 || stats.CacheMisses != 0 {
t.Error("noopResolver.Stats() should return zero values")
}
}
// TestMockDNSCacheEntryConcurrency 测试缓存条目并发访问。
func TestMockDNSCacheEntryConcurrency(t *testing.T) {
entry := &DNSCacheEntry{
IPs: []string{"127.0.0.1", "127.0.0.2"},
ExpiresAt: time.Now().Add(time.Hour),
}
var wg sync.WaitGroup
var readCount atomic.Int64
// 并发读取
for range 100 {
wg.Go(func() {
entry.mu.RLock()
_ = entry.IPs
_ = entry.ExpiresAt
entry.mu.RUnlock()
readCount.Add(1)
})
}
wg.Wait()
if readCount.Load() != 100 {
t.Errorf("Expected 100 reads, got %d", readCount.Load())
}
}
// TestMockDNSIPv6QueryFallback 测试 IPv6 查询失败降级。
func TestMockDNSIPv6QueryFallback(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
IPv4: true,
IPv6: true,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 查询 localhost通常有 IPv4
ips, err := resolver.LookupHost(context.Background(), "localhost")
if err != nil {
t.Fatalf("LookupHost() error: %v", err)
}
// 应该至少有 IPv4 地址
if len(ips) == 0 {
t.Error("Expected at least one IP address")
}
}
// TestMockDNSDoRefresh 测试刷新操作。
func TestMockDNSDoRefresh(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
Timeout: 1 * time.Second,
CacheSize: 10,
}
resolver := New(cfg).(*DNSResolver)
// 添加需要刷新的主机
resolver.mu.Lock()
resolver.refreshHosts["localhost"] = struct{}{}
resolver.refreshHosts["127.0.0.1"] = struct{}{} // IP 地址会被跳过
resolver.mu.Unlock()
// 执行刷新
resolver.doRefresh()
// 验证刷新列表仍然存在
resolver.mu.RLock()
_, exists := resolver.refreshHosts["localhost"]
resolver.mu.RUnlock()
if !exists {
t.Error("Refresh hosts should still contain localhost")
}
}