Files
x/selector/strategy_test.go
T
ginuerzh aabebd047b fix(selector): safe type assertion, clean up debug logging, add doc comments and 64 tests
Replace bare type assertion in ParallelStrategy with comma-ok check to
prevent panic on non-Node inputs. Downgrade noisy Infof hash-selection
log to Tracef and remove Chinese-language debug output. Add doc comments
to all exported symbols in weighted.go.
2026-05-25 23:38:35 +08:00

272 lines
6.8 KiB
Go

package selector
import (
"context"
"hash/crc32"
"sync"
"testing"
"github.com/go-gost/core/metadata"
"github.com/go-gost/core/selector"
xctx "github.com/go-gost/x/ctx"
xmd "github.com/go-gost/x/metadata"
)
// --- RoundRobinStrategy ---
func TestRoundRobinStrategy_Empty(t *testing.T) {
s := RoundRobinStrategy[int]()
if v := s.Apply(context.Background()); v != 0 {
t.Fatalf("expected zero value, got %d", v)
}
}
func TestRoundRobinStrategy_Single(t *testing.T) {
s := RoundRobinStrategy[int]()
for i := 0; i < 5; i++ {
if v := s.Apply(context.Background(), 42); v != 42 {
t.Fatalf("iteration %d: expected 42, got %d", i, v)
}
}
}
func TestRoundRobinStrategy_Sequence(t *testing.T) {
s := RoundRobinStrategy[int]()
items := []int{10, 20, 30}
// Should cycle through 10, 20, 30, 10, 20, 30, ...
for i := 0; i < 9; i++ {
v := s.Apply(context.Background(), items...)
expected := items[i%3]
if v != expected {
t.Fatalf("iteration %d: expected %d, got %d", i, expected, v)
}
}
}
func TestRoundRobinStrategy_Concurrent(t *testing.T) {
s := RoundRobinStrategy[int]()
items := []int{0, 1, 2, 3, 4}
const goroutines = 100
var wg sync.WaitGroup
results := make(chan int, goroutines)
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results <- s.Apply(context.Background(), items...)
}()
}
wg.Wait()
close(results)
// Every result should be a valid item
for v := range results {
if v < 0 || v > 4 {
t.Fatalf("unexpected value %d", v)
}
}
}
// --- FIFOStrategy ---
func TestFIFOStrategy_Empty(t *testing.T) {
s := FIFOStrategy[string]()
if v := s.Apply(context.Background()); v != "" {
t.Fatalf("expected zero value, got %q", v)
}
}
func TestFIFOStrategy_AlwaysFirst(t *testing.T) {
s := FIFOStrategy[int]()
for i := 0; i < 10; i++ {
v := s.Apply(context.Background(), 100, 200, 300)
if v != 100 {
t.Fatalf("iteration %d: expected 100, got %d", i, v)
}
}
}
// --- RandomStrategy ---
func TestRandomStrategy_Empty(t *testing.T) {
s := RandomStrategy[int]()
if v := s.Apply(context.Background()); v != 0 {
t.Fatalf("expected zero value, got %d", v)
}
}
func TestRandomStrategy_Single(t *testing.T) {
s := RandomStrategy[int]()
for i := 0; i < 10; i++ {
if v := s.Apply(context.Background(), 7); v != 7 {
t.Fatalf("expected 7, got %d", v)
}
}
}
func TestRandomStrategy_Distribution(t *testing.T) {
s := RandomStrategy[int]()
items := []int{0, 1, 2}
counts := make(map[int]int)
const n = 3000
for i := 0; i < n; i++ {
v := s.Apply(context.Background(), items...)
counts[v]++
}
// With uniform weights, each should get roughly n/3
for _, item := range items {
if counts[item] < n/len(items)/2 {
t.Fatalf("item %d underrepresented: %d/%d", item, counts[item], n)
}
}
}
// weightedItem implements metadata.Metadatable for weight testing.
type weightedItem struct {
md metadata.Metadata
}
func (w *weightedItem) Metadata() metadata.Metadata { return w.md }
func TestRandomStrategy_Weighted(t *testing.T) {
s := RandomStrategy[*weightedItem]()
heavy := &weightedItem{md: xmd.NewMetadata(map[string]any{"weight": 100})}
light := &weightedItem{md: xmd.NewMetadata(map[string]any{"weight": 1})}
heavyCount := 0
const n = 2000
for i := 0; i < n; i++ {
v := s.Apply(context.Background(), heavy, light)
if v == heavy {
heavyCount++
}
}
// Heavy should win ~99% of the time with weight 100 vs 1
if heavyCount < int(float64(n)*0.9) {
t.Fatalf("heavy item underrepresented: %d/%d", heavyCount, n)
}
}
func TestRandomStrategy_ZeroWeight(t *testing.T) {
s := RandomStrategy[int]()
// Zero/negative weights default to 1, so all items should be selectable
v := s.Apply(context.Background(), 1, 2, 3)
if v < 1 || v > 3 {
t.Fatalf("expected 1-3, got %d", v)
}
}
// --- HashStrategy ---
func TestHashStrategy_Empty(t *testing.T) {
s := HashStrategy[int]()
if v := s.Apply(context.Background()); v != 0 {
t.Fatalf("expected zero value, got %d", v)
}
}
func TestHashStrategy_ClientID(t *testing.T) {
s := HashStrategy[int]()
items := []int{10, 20, 30, 40, 50}
ctx := xctx.ContextWithClientID(context.Background(), "test-client")
// Same client ID should always map to the same item
v1 := s.Apply(ctx, items...)
v2 := s.Apply(ctx, items...)
if v1 != v2 {
t.Fatalf("hash strategy should be deterministic for same client ID: %d != %d", v1, v2)
}
// Verify it matches manual CRC32 calculation
expectedIdx := uint64(crc32.ChecksumIEEE([]byte("test-client"))) % uint64(len(items))
if v1 != items[expectedIdx] {
t.Fatalf("expected items[%d]=%d, got %d", expectedIdx, items[expectedIdx], v1)
}
}
func TestHashStrategy_HashSource(t *testing.T) {
s := HashStrategy[int]()
items := []int{10, 20, 30}
ctx := xctx.ContextWithHash(context.Background(), &xctx.Hash{Source: "my-hash-key"})
v1 := s.Apply(ctx, items...)
v2 := s.Apply(ctx, items...)
if v1 != v2 {
t.Fatalf("hash strategy should be deterministic for same hash source: %d != %d", v1, v2)
}
}
func TestHashStrategy_ClientIDPriorityOverHash(t *testing.T) {
s := HashStrategy[int]()
items := []int{10, 20, 30, 40}
ctxClient := xctx.ContextWithClientID(context.Background(), "clientA")
ctxBoth := xctx.ContextWithHash(
xctx.ContextWithClientID(context.Background(), "clientA"),
&xctx.Hash{Source: "different-hash"},
)
vClient := s.Apply(ctxClient, items...)
vBoth := s.Apply(ctxBoth, items...)
// Client ID should take priority over hash source
if vClient != vBoth {
t.Fatalf("client ID should take priority: %d != %d", vClient, vBoth)
}
}
func TestHashStrategy_FallbackRandom(t *testing.T) {
s := HashStrategy[int]()
items := []int{0, 1, 2}
counts := make(map[int]int)
const n = 3000
for i := 0; i < n; i++ {
v := s.Apply(context.Background(), items...)
counts[v]++
}
// Without client ID or hash, falls back to random
for _, item := range items {
if counts[item] < n/len(items)/2 {
t.Fatalf("item %d underrepresented in random fallback: %d/%d", item, counts[item], n)
}
}
}
func TestHashStrategy_DifferentClientIDs(t *testing.T) {
s := HashStrategy[int]()
items := make([]int, 100)
for i := range items {
items[i] = i
}
selected := make(map[int]bool)
for i := 0; i < 100; i++ {
ctx := xctx.ContextWithClientID(context.Background(), xctx.ClientID(string(rune('A'+i))))
v := s.Apply(ctx, items...)
selected[v] = true
}
// Different client IDs should spread across multiple items
if len(selected) < 10 {
t.Fatalf("expected distribution across many items, got %d unique selections", len(selected))
}
}
// --- Interface compliance ---
func TestStrategyInterfaceCompliance(t *testing.T) {
// Verify all strategies implement selector.Strategy
_ = selector.Strategy[int](RoundRobinStrategy[int]())
_ = selector.Strategy[int](RandomStrategy[int]())
_ = selector.Strategy[int](FIFOStrategy[int]())
_ = selector.Strategy[int](HashStrategy[int]())
_ = selector.Strategy[int](ParallelStrategy[int]())
}