From 8f3f1527bcf11646deba366deecfb88c46de3d6a Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 16 Apr 2026 18:12:28 +0800 Subject: [PATCH] =?UTF-8?q?test(loadbalance):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=B4=9F=E8=BD=BD=E5=9D=87=E8=A1=A1=E5=99=A8=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 测试 RoundRobin/WRR/LeastConn/IPHash 算法 - 测试后端选择和权重分布 - 测试边界条件和并发安全 Co-Authored-By: Claude Opus 4.6 --- internal/loadbalance/balancer_test.go | 802 ++++++++++++++++++++++++++ 1 file changed, 802 insertions(+) diff --git a/internal/loadbalance/balancer_test.go b/internal/loadbalance/balancer_test.go index 3158dc1..ff6ba3f 100644 --- a/internal/loadbalance/balancer_test.go +++ b/internal/loadbalance/balancer_test.go @@ -11,8 +11,10 @@ package loadbalance import ( + "net" "sync" "testing" + "time" ) // createHealthyTarget 创建一个带有健康状态的目标(辅助函数) @@ -934,3 +936,803 @@ func TestIsValidAlgorithm(t *testing.T) { }) } } + +// TestRoundRobin_SelectExcluding 测试轮询排除选择功能。 +func TestRoundRobin_SelectExcluding(t *testing.T) { + t.Run("空排除列表", func(_ *testing.T) { + rr := NewRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + + got := rr.SelectExcluding(targets, nil) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + }) + + t.Run("排除部分目标", func(_ *testing.T) { + rr := NewRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + excluded := []*Target{targets[0], targets[1]} + + got := rr.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL != "http://backend3:8080" { + t.Errorf("SelectExcluding() = %q, want %q", got.URL, "http://backend3:8080") + } + }) + + t.Run("排除所有目标", func(_ *testing.T) { + rr := NewRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{targets[0], targets[1]} + + got := rr.SelectExcluding(targets, excluded) + if got != nil { + t.Errorf("SelectExcluding() = %v, want nil", got) + } + }) + + t.Run("排除列表含nil", func(_ *testing.T) { + rr := NewRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{nil, targets[0]} + + got := rr.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL == targets[0].URL { + t.Errorf("选中了被排除的目标: %q", got.URL) + } + }) + + t.Run("并发安全", func(_ *testing.T) { + rr := NewRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + excluded := []*Target{targets[0]} + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + got := rr.SelectExcluding(targets, excluded) + if got != nil && got.URL == targets[0].URL { + t.Errorf("选中了被排除的目标: %q", got.URL) + } + }() + } + wg.Wait() + }) + + t.Run("排除不健康目标外再排除一个", func(_ *testing.T) { + rr := NewRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", false), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + excluded := []*Target{targets[1]} + + got := rr.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL != "http://backend3:8080" { + t.Errorf("SelectExcluding() = %q, want %q", got.URL, "http://backend3:8080") + } + }) +} + +// TestWeightedRoundRobin_SelectExcluding 测试加权轮询排除选择功能。 +func TestWeightedRoundRobin_SelectExcluding(t *testing.T) { + t.Run("排除高权重目标", func(_ *testing.T) { + wrr := NewWeightedRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + targets[0].Weight = 1 + targets[1].Weight = 5 + excluded := []*Target{targets[1]} + + // 排除高权重目标后,只应选低权重目标 + for i := 0; i < 20; i++ { + got := wrr.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL != "http://backend1:8080" { + t.Errorf("SelectExcluding() = %q, want %q", got.URL, "http://backend1:8080") + } + } + }) + + t.Run("排除所有健康目标", func(_ *testing.T) { + wrr := NewWeightedRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{targets[0], targets[1]} + + got := wrr.SelectExcluding(targets, excluded) + if got != nil { + t.Errorf("SelectExcluding() = %v, want nil", got) + } + }) + + t.Run("排除列表含nil", func(_ *testing.T) { + wrr := NewWeightedRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + targets[0].Weight = 1 + targets[1].Weight = 1 + excluded := []*Target{nil, targets[0]} + + got := wrr.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL == targets[0].URL { + t.Errorf("选中了被排除的目标: %q", got.URL) + } + }) +} + +// TestLeastConnections_SelectExcluding 测试最少连接排除选择功能。 +func TestLeastConnections_SelectExcluding(t *testing.T) { + t.Run("排除连接最少的目标", func(_ *testing.T) { + lc := NewLeastConnections() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + targets[0].Connections = 1 + targets[1].Connections = 10 + targets[2].Connections = 5 + excluded := []*Target{targets[0]} + + got := lc.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + // 排除最少连接的后,应选连接数次少的 + if got.URL != "http://backend3:8080" { + t.Errorf("SelectExcluding() = %q, want %q", got.URL, "http://backend3:8080") + } + }) + + t.Run("排除列表含nil", func(_ *testing.T) { + lc := NewLeastConnections() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + targets[0].Connections = 5 + targets[1].Connections = 3 + excluded := []*Target{nil, targets[0]} + + got := lc.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL == targets[0].URL { + t.Errorf("选中了被排除的目标: %q", got.URL) + } + }) + + t.Run("全部排除", func(_ *testing.T) { + lc := NewLeastConnections() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{targets[0], targets[1]} + + got := lc.SelectExcluding(targets, excluded) + if got != nil { + t.Errorf("SelectExcluding() = %v, want nil", got) + } + }) +} + +// TestIPHash_SelectExcluding 测试IP哈希排除选择功能。 +func TestIPHash_SelectExcluding(t *testing.T) { + t.Run("排除命中目标", func(_ *testing.T) { + ih := NewIPHash() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + + // 找到该IP会命中的目标 + clientIP := "192.168.1.100" + first := ih.SelectByIP(targets, clientIP) + if first == nil { + t.Fatal("SelectByIP() = nil, want non-nil") + } + + // 排除该目标 + excluded := []*Target{first} + got := ih.SelectExcludingByIP(targets, excluded, clientIP) + if got == nil { + t.Fatal("SelectExcludingByIP() = nil, want non-nil") + } + if got.URL == first.URL { + t.Errorf("SelectExcludingByIP() 选中了被排除的目标: %q", got.URL) + } + }) + + t.Run("排除所有目标", func(_ *testing.T) { + ih := NewIPHash() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{targets[0], targets[1]} + + got := ih.SelectExcludingByIP(targets, excluded, "10.0.0.1") + if got != nil { + t.Errorf("SelectExcludingByIP() = %v, want nil", got) + } + }) + + t.Run("SelectExcluding使用空IP", func(_ *testing.T) { + ih := NewIPHash() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{targets[0]} + + got := ih.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + }) + + t.Run("排除列表含nil", func(_ *testing.T) { + ih := NewIPHash() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + excluded := []*Target{nil, targets[0]} + + got := ih.SelectExcludingByIP(targets, excluded, "192.168.1.1") + if got == nil { + t.Fatal("SelectExcludingByIP() = nil, want non-nil") + } + if got.URL == targets[0].URL { + t.Errorf("选中了被排除的目标: %q", got.URL) + } + }) +} + +// TestFilterHealthyAndExclude 测试filterHealthyAndExclude辅助函数。 +func TestFilterHealthyAndExclude(t *testing.T) { + t.Run("基本过滤和排除", func(_ *testing.T) { + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", false), + createHealthyTarget("http://backend3:8080", true), + } + excluded := []*Target{targets[0]} + + got := filterHealthyAndExclude(targets, excluded) + if len(got) != 1 { + t.Fatalf("len = %d, want 1", len(got)) + } + if got[0].URL != "http://backend3:8080" { + t.Errorf("got = %q, want %q", got[0].URL, "http://backend3:8080") + } + }) + + t.Run("空排除列表", func(_ *testing.T) { + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + + got := filterHealthyAndExclude(targets, nil) + if len(got) != 2 { + t.Fatalf("len = %d, want 2", len(got)) + } + }) + + t.Run("空目标列表", func(_ *testing.T) { + got := filterHealthyAndExclude(nil, []*Target{}) + if len(got) != 0 { + t.Errorf("len = %d, want 0", len(got)) + } + }) + + t.Run("排除列表含nil", func(_ *testing.T) { + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + } + excluded := []*Target{nil} + + got := filterHealthyAndExclude(targets, excluded) + if len(got) != 1 { + t.Fatalf("len = %d, want 1", len(got)) + } + }) +} + +// TestTarget_Hostname 测试Target.Hostname方法。 +func TestTarget_Hostname(t *testing.T) { + t.Run("从URL提取主机名", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com:8080/api", 1) + got := target.Hostname() + if got != "example.com" { + t.Errorf("Hostname() = %q, want %q", got, "example.com") + } + }) + + t.Run("无端口URL", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com/api", 1) + got := target.Hostname() + if got != "example.com" { + t.Errorf("Hostname() = %q, want %q", got, "example.com") + } + }) + + t.Run("无效URL主机名为空", func(_ *testing.T) { + target := &Target{URL: "not-a-valid-url"} + target.initHostname() + got := target.Hostname() + // url.Parse("not-a-valid-url") 解析为纯路径URL,Host为空 + if got != "" { + t.Errorf("Hostname() = %q, want empty string", got) + } + }) + + t.Run("缓存行为", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com:8080", 1) + _ = target.Hostname() + _ = target.Hostname() // 第二次应使用缓存 + got := target.Hostname() + if got != "example.com" { + t.Errorf("Hostname() = %q, want %q", got, "example.com") + } + }) +} + +// TestTarget_ResolvedIPs 测试Target.ResolvedIPs和SetResolvedIPs方法。 +func TestTarget_ResolvedIPs(t *testing.T) { + t.Run("未设置时返回nil", func(_ *testing.T) { + target := &Target{URL: "http://example.com"} + got := target.ResolvedIPs() + if got != nil { + t.Errorf("ResolvedIPs() = %v, want nil", got) + } + }) + + t.Run("设置后返回副本", func(_ *testing.T) { + target := &Target{URL: "http://example.com"} + ips := []string{"192.168.1.1", "192.168.1.2"} + target.SetResolvedIPs(ips) + + got := target.ResolvedIPs() + if len(got) != 2 { + t.Fatalf("len = %d, want 2", len(got)) + } + if got[0] != "192.168.1.1" || got[1] != "192.168.1.2" { + t.Errorf("ResolvedIPs() = %v, want %v", got, ips) + } + + // 修改原切片不影响内部存储 + ips[0] = "10.0.0.1" + got2 := target.ResolvedIPs() + if got2[0] != "192.168.1.1" { + t.Errorf("ResolvedIPs() 受外部修改影响 = %q, want %q", got2[0], "192.168.1.1") + } + }) + + t.Run("设置空列表", func(_ *testing.T) { + target := &Target{URL: "http://example.com"} + target.SetResolvedIPs([]string{}) + got := target.ResolvedIPs() + if len(got) != 0 { + t.Errorf("ResolvedIPs() = %v, want empty", got) + } + }) +} + +// TestTarget_NeedsResolve 测试Target.NeedsResolve方法。 +func TestTarget_NeedsResolve(t *testing.T) { + t.Run("IP地址不需要解析", func(_ *testing.T) { + target := NewTargetFromConfig("http://192.168.1.1:8080", 1) + if target.NeedsResolve(time.Minute) { + t.Error("IP地址URL不需要解析") + } + }) + + t.Run("首次解析需要", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com:8080", 1) + if !target.NeedsResolve(time.Minute) { + t.Error("首次解析应该需要") + } + }) + + t.Run("TTL未过期不需要", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com:8080", 1) + target.SetResolvedIPs([]string{"192.168.1.1"}) + // TTL为1小时,刚设置过,不应过期 + if target.NeedsResolve(time.Hour) { + t.Error("TTL未过期不应该需要解析") + } + }) + + t.Run("TTL过期需要", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com:8080", 1) + target.SetResolvedIPs([]string{"192.168.1.1"}) + // 使用极短的TTL模拟过期 + if !target.NeedsResolve(time.Nanosecond) { + t.Error("TTL过期应该需要解析") + } + }) +} + +// TestTarget_LastResolved 测试Target.LastResolved方法。 +func TestTarget_LastResolved(t *testing.T) { + t.Run("未设置时返回零值", func(_ *testing.T) { + target := &Target{URL: "http://example.com"} + got := target.LastResolved() + if !got.IsZero() { + t.Errorf("LastResolved() = %v, want zero", got) + } + }) + + t.Run("设置后返回时间", func(_ *testing.T) { + target := &Target{URL: "http://example.com"} + before := time.Now() + target.SetResolvedIPs([]string{"192.168.1.1"}) + after := time.Now() + + got := target.LastResolved() + if got.Before(before) || got.After(after) { + t.Errorf("LastResolved() = %v, 应该在 %v 和 %v 之间", got, before, after) + } + }) +} + +// TestNewTargetFromConfig 测试NewTargetFromConfig函数。 +func TestNewTargetFromConfig(t *testing.T) { + t.Run("创建健康目标", func(_ *testing.T) { + target := NewTargetFromConfig("http://backend:8080", 5) + if target.URL != "http://backend:8080" { + t.Errorf("URL = %q, want %q", target.URL, "http://backend:8080") + } + if target.Weight != 5 { + t.Errorf("Weight = %d, want 5", target.Weight) + } + if !target.Healthy.Load() { + t.Error("Healthy 应为 true") + } + }) + + t.Run("主机名自动初始化", func(_ *testing.T) { + target := NewTargetFromConfig("http://example.com:9090", 1) + got := target.Hostname() + if got != "example.com" { + t.Errorf("Hostname() = %q, want %q", got, "example.com") + } + }) +} + +// TestConsistentHash_PrecomputeHashes 测试预计算哈希功能。 +func TestConsistentHash_PrecomputeHashes(t *testing.T) { + t.Run("预计算哈希值", func(_ *testing.T) { + ch := NewConsistentHash(50, "ip") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + + ch.PrecomputeHashes(targets, 50) + + for _, target := range targets { + if len(target.VirtualHashes) != 50 { + t.Errorf("VirtualHashes len = %d, want 50", len(target.VirtualHashes)) + } + } + }) + + t.Run("跳过已预计算的目标", func(_ *testing.T) { + ch := NewConsistentHash(10, "ip") + target := createHealthyTarget("http://backend1:8080", true) + // 预计算一次 + ch.PrecomputeHashes([]*Target{target}, 10) + firstHashes := make([]uint64, len(target.VirtualHashes)) + copy(firstHashes, target.VirtualHashes) + + // 再次预计算相同数量 + ch.PrecomputeHashes([]*Target{target}, 10) + + // 哈希值应保持不变 + for i, h := range target.VirtualHashes { + if h != firstHashes[i] { + t.Errorf("预计算改变了已有哈希值") + break + } + } + }) + + t.Run("默认虚拟节点数", func(_ *testing.T) { + ch := NewConsistentHash(0, "ip") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + } + + ch.PrecomputeHashes(targets, 0) + + if len(targets[0].VirtualHashes) != 150 { + t.Errorf("VirtualHashes len = %d, want 150 (default)", len(targets[0].VirtualHashes)) + } + }) +} + +// TestConsistentHash_SelectExcluding 测试一致性哈希SelectExcluding方法。 +func TestConsistentHash_SelectExcluding(t *testing.T) { + t.Run("委托给SelectExcludingByKey", func(_ *testing.T) { + ch := NewConsistentHash(100, "ip") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + ch.Rebuild(targets) + + excluded := []*Target{targets[0]} + got := ch.SelectExcluding(targets, excluded) + if got == nil { + t.Fatal("SelectExcluding() = nil, want non-nil") + } + if got.URL == targets[0].URL { + t.Errorf("选中了被排除的目标: %q", got.URL) + } + }) +} + +// TestLeastConnections_ConcurrentSelection 测试最少连接并发选择。 +func TestLeastConnections_ConcurrentSelection(t *testing.T) { + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + lc := NewLeastConnections() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + got := lc.Select(targets) + if got == nil { + t.Error("并发Select() = nil, want non-nil") + } + }() + } + wg.Wait() +} + +// TestWeightedRoundRobin_ConcurrentSelection 测试加权轮询并发选择。 +func TestWeightedRoundRobin_ConcurrentSelection(t *testing.T) { + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + targets[0].Weight = 3 + targets[1].Weight = 1 + wrr := NewWeightedRoundRobin() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + got := wrr.Select(targets) + if got == nil { + t.Error("并发Select() = nil, want non-nil") + } + }() + } + wg.Wait() +} + +// TestIPHash_ConcurrentSelection 测试IP哈希并发选择。 +func TestIPHash_ConcurrentSelection(t *testing.T) { + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + ih := NewIPHash() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(ip string) { + defer wg.Done() + got := ih.SelectByIP(targets, ip) + if got == nil { + t.Error("并发SelectByIP() = nil, want non-nil") + } + }(net.IP{192, 168, 1, byte(i)}.String()) + } + wg.Wait() +} + +// TestTarget_Hostname_IPURL 测试纯IP地址URL的主机名提取。 +func TestTarget_Hostname_IPURL(t *testing.T) { + target := NewTargetFromConfig("http://10.0.0.1:8080", 1) + got := target.Hostname() + if got != "10.0.0.1" { + t.Errorf("Hostname() = %q, want %q", got, "10.0.0.1") + } +} + +// TestConsistentHash_SelectByKey_EmptyKey 测试空键选择行为。 +func TestConsistentHash_SelectByKey_EmptyKey(t *testing.T) { + ch := NewConsistentHash(100, "ip") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + } + + // 空键也应该能选择(不会panic) + got := ch.SelectByKey(targets, "") + if got == nil { + t.Fatal("SelectByKey(\"\") = nil, want non-nil") + } +} + +// TestConsistentHash_RebuildWithAllUnhealthy 测试所有目标不健康时重建。 +func TestConsistentHash_RebuildWithAllUnhealthy(t *testing.T) { + ch := NewConsistentHash(10, "ip") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", false), + createHealthyTarget("http://backend2:8080", false), + } + + ch.Rebuild(targets) + stats := ch.GetStats() + + if stats.CircleSize != 0 { + t.Errorf("CircleSize = %d, want 0", stats.CircleSize) + } + if stats.SortedHashes != 0 { + t.Errorf("SortedHashes = %d, want 0", stats.SortedHashes) + } +} + +// TestConsistentHash_GetStats 测试统计信息完整性。 +func TestConsistentHash_GetStats(t *testing.T) { + ch := NewConsistentHash(50, "uri") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + createHealthyTarget("http://backend3:8080", true), + } + + ch.Rebuild(targets) + stats := ch.GetStats() + + if stats.VirtualNodes != 50 { + t.Errorf("VirtualNodes = %d, want 50", stats.VirtualNodes) + } + if stats.CircleSize != 150 { // 3 targets * 50 nodes + t.Errorf("CircleSize = %d, want 150", stats.CircleSize) + } + if stats.SortedHashes != 150 { + t.Errorf("SortedHashes = %d, want 150", stats.SortedHashes) + } +} + +// TestConsistentHash_GetHashKey 测试哈希键配置获取。 +func TestConsistentHash_GetHashKey(t *testing.T) { + tests := []struct { + name string + hashKey string + want string + }{ + {"ip", "ip", "ip"}, + {"uri", "uri", "uri"}, + {"header", "header:X-Forwarded-For", "header:X-Forwarded-For"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + ch := NewConsistentHash(100, tt.hashKey) + if ch.GetHashKey() != tt.want { + t.Errorf("GetHashKey() = %q, want %q", ch.GetHashKey(), tt.want) + } + }) + } +} + +// TestSelectExcluding_DynamicRebuild 测试SelectByKey触发的动态重建。 +func TestSelectExcluding_DynamicRebuild(t *testing.T) { + ch := NewConsistentHash(10, "ip") + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + + // 不手动Rebuild,SelectByKey应自动触发 + got := ch.SelectByKey(targets, "10.0.0.1") + if got == nil { + t.Fatal("SelectByKey() = nil after auto-rebuild, want non-nil") + } +} + +// TestWeightedRoundRobin_NegativeWeight 测试负权重行为。 +func TestWeightedRoundRobin_NegativeWeight(t *testing.T) { + wrr := NewWeightedRoundRobin() + targets := []*Target{ + createHealthyTarget("http://backend1:8080", true), + createHealthyTarget("http://backend2:8080", true), + } + targets[0].Weight = -5 + targets[1].Weight = 3 + + // 负权重要被当作1处理 + counts := make(map[string]int) + for i := 0; i < 100; i++ { + got := wrr.Select(targets) + if got == nil { + t.Fatal("Select() = nil, want non-nil") + } + counts[got.URL]++ + } + + // 两个目标都应该被选中 + if counts["http://backend1:8080"] == 0 { + t.Error("负权重目标从未被选中") + } + if counts["http://backend2:8080"] == 0 { + t.Error("正权重目标从未被选中") + } +} + +// TestRoundRobin_NilTargets 测试nil切片输入。 +func TestRoundRobin_NilTargets(t *testing.T) { + rr := NewRoundRobin() + got := rr.Select(nil) + if got != nil { + t.Errorf("Select(nil) = %v, want nil", got) + } +} + +// TestLeastConnections_NilTargets 测试nil切片输入。 +func TestLeastConnections_NilTargets(t *testing.T) { + lc := NewLeastConnections() + got := lc.Select(nil) + if got != nil { + t.Errorf("Select(nil) = %v, want nil", got) + } +}