// selector_test.go:P2C + sticky LRU 单测(无外网,固定随机源)。 package proxymanager import ( "testing" "time" ) // fixedRnd 固定序列随机源(可复现)。 func fixedRnd(seq []int) func(int) int { i := 0 return func(n int) int { v := seq[i%len(seq)] % n i++ return v } } func TestPickLowestDelay(t *testing.T) { cands := []string{"A", "B", "C"} delays := map[string]int{"A": 200, "B": 100, "C": 300} picked := Pick(cands, func(n string) int { return delays[n] }, fixedRnd([]int{0, 1})) if picked != "B" { t.Errorf("P2C picked = %s, want B(最低延迟)", picked) } } func TestPickUnknownDelayNotPreferred(t *testing.T) { // 未探测节点(<=0)视为最差:A 已探测 500ms,B 未探测 → 应选 A cands := []string{"A", "B"} picked := Pick(cands, func(n string) int { if n == "A" { return 500 } return 0 }, fixedRnd([]int{0, 1})) if picked != "A" { t.Errorf("picked = %s, want A(未探测节点不应霸占选择)", picked) } } func TestPickSingleCandidate(t *testing.T) { if got := Pick([]string{"only"}, func(string) int { return 1 }, fixedRnd([]int{0})); got != "only" { t.Errorf("单候选 picked = %s", got) } if got := Pick(nil, func(string) int { return 0 }, fixedRnd([]int{0})); got != "" { t.Errorf("空候选 picked = %q, want 空串", got) } } func TestPickNeverSameIndexTwice(t *testing.T) { // rnd 恒返同值时,两样本必须不同(j=(i+1)%n 兜底) cands := []string{"A", "B", "C"} delays := map[string]int{"A": 100, "B": 200, "C": 300} picked := Pick(cands, func(n string) int { return delays[n] }, func(int) int { return 0 }) if picked != "A" { t.Errorf("picked = %s, want A(0 与 1 比较取低)", picked) } } func TestStickyTTLExpiry(t *testing.T) { s := NewStickyTable(time.Minute, 10) s.Set("k", "node1") if n, ok := s.Get("k"); !ok || n != "node1" { t.Fatalf("Get = %q,%v", n, ok) } // TTL 过期(直接改 ExpireAt 模拟时钟前进) s.mu.Lock() e := s.m["k"] e.ExpireAt = time.Now().Add(-time.Second) s.m["k"] = e s.mu.Unlock() if _, ok := s.Get("k"); ok { t.Fatal("过期条目应失效") } } func TestStickyLRUEviction(t *testing.T) { s := NewStickyTable(time.Hour, 3) s.Set("a", "1") s.Set("b", "2") s.Set("c", "3") s.Get("a") // touch a → b 变最旧 s.Set("d", "4") if _, ok := s.Get("b"); ok { t.Fatal("LRU 应淘汰 b(容量 3)") } if _, ok := s.Get("a"); !ok { t.Fatal("a 被访问过不应被淘汰") } if s.Len() != 3 { t.Errorf("Len = %d, want 3", s.Len()) } } func TestStickyReset(t *testing.T) { s := NewStickyTable(time.Hour, 10) s.Set("k", "v") s.Reset() if s.Len() != 0 { t.Fatal("Reset 后应为空") } } func TestStickyKey(t *testing.T) { if StickyKey("sess1", "d.com") != "s:sess1" { t.Error("session 优先") } if StickyKey("", "d.com") != "d:d.com" { t.Error("无 session 用 domain") } } func TestStickyDefaultCapacity(t *testing.T) { s := NewStickyTable(0, 0) // 走默认值:TTL 10min / 容量 1000 for i := 0; i < 1200; i++ { s.Set(string(rune('a'+i%26))+itoa(i), "n") } if s.Len() > 1000 { t.Errorf("容量上限失效: Len=%d", s.Len()) } } func itoa(n int) string { if n == 0 { return "0" } var b []byte for n > 0 { b = append([]byte{byte('0' + n%10)}, b...) n /= 10 } return string(b) }