875093dff2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
81 lines
3.2 KiB
Go
81 lines
3.2 KiB
Go
package accounts_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/wangjia/pay/config"
|
|
"github.com/wangjia/pay/internal/accounts"
|
|
)
|
|
|
|
func TestWeightedDeterministic(t *testing.T) {
|
|
w := accounts.NewWeighted()
|
|
cs := cands("a1", "a2", "a3")
|
|
// 同一 OutTradeNo 必落同一账户(确定性,可复现)。
|
|
first, _ := w.Pick("fake|global", cs, accounts.PickHint{OutTradeNo: "PAY-XYZ"})
|
|
for i := 0; i < 5; i++ {
|
|
got, _ := w.Pick("fake|global", cs, accounts.PickHint{OutTradeNo: "PAY-XYZ"})
|
|
if got.AccountID != first.AccountID {
|
|
t.Fatalf("同单号应恒定命中 %s, 第 %d 次得 %s", first.AccountID, i, got.AccountID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWeightedDistributesAndRespectsWeight(t *testing.T) {
|
|
w := accounts.NewWeighted()
|
|
// a1 权重 3,a2 权重 1 → 长跑大致 3:1。
|
|
cs := []config.AccountConfig{
|
|
{AccountID: "a1", Weight: 3},
|
|
{AccountID: "a2", Weight: 1},
|
|
}
|
|
counts := map[string]int{}
|
|
for i := 0; i < 4000; i++ {
|
|
no := "PAY-" + string(rune('A'+i%26)) + string(rune('0'+i/26%10)) + string(rune('a'+i/260%26)) + string(rune('0'+i%7))
|
|
got, _ := w.Pick("fake|global", cs, accounts.PickHint{OutTradeNo: no})
|
|
counts[got.AccountID]++
|
|
}
|
|
if counts["a1"] == 0 || counts["a2"] == 0 {
|
|
t.Fatalf("两账户都应被选中, got %+v", counts)
|
|
}
|
|
// a1 应显著多于 a2(3:1 容差:a1 至少是 a2 的 2 倍)。
|
|
if counts["a1"] < counts["a2"]*2 {
|
|
t.Fatalf("权重 3:1 未体现, got %+v", counts)
|
|
}
|
|
}
|
|
|
|
// TestWeightedZeroWeightTreatedAsOne 用确定性断言(非抽样分布)验证 Weight<=0 记 1。
|
|
//
|
|
// 此前版本靠 400 个 "N<digit><letter>"(i%10, i%26 联动生成)key 抽样统计两账户
|
|
// 是否都出现过 —— 但 fnv32a 对这批同构 key 恰好全部落在偶数哈希(见 check.go 复现),
|
|
// 导致抽样必然只命中一侧、测试恒红,与实现无关(已用随机 key / 完整 10x26 网格核实
|
|
// 实现对普通 key 分布均匀,证据见修复报告)。改为:
|
|
// 1. 单候选 weight=0 必须可选中(不因权重被当成 0 而排除)。
|
|
// 2. 两候选 weight=0+0 → 各按文档规则记 1、总权重 2,target=fnv32a(key)%2,
|
|
// 按候选顺序累加桶(a1 占 [0,1)、a2 占 [1,2))。用两个已知奇偶性的字面量 key
|
|
// (提前用同一 fnv32a 规则手算)分别断言恰好落在 a1 / a2,不依赖分布采样。
|
|
func TestWeightedZeroWeightTreatedAsOne(t *testing.T) {
|
|
w := accounts.NewWeighted()
|
|
|
|
single := []config.AccountConfig{{AccountID: "solo", Weight: 0}}
|
|
got, err := w.Pick("fake|global", single, accounts.PickHint{OutTradeNo: "ANY-KEY"})
|
|
if err != nil || got.AccountID != "solo" {
|
|
t.Fatalf("单候选 weight=0 应可选中, got %v err %v", got, err)
|
|
}
|
|
|
|
cs := []config.AccountConfig{{AccountID: "a1", Weight: 0}, {AccountID: "a2", Weight: 0}}
|
|
// fnv32a("PAY-0002") = 3179067314,偶数 → target=0,落 a1 的 [0,1) 桶。
|
|
// fnv32a("PAY-0001") = 3162289695,奇数 → target=1,落 a2 的 [1,2) 桶。
|
|
cases := []struct {
|
|
key string
|
|
want string
|
|
}{
|
|
{"PAY-0002", "a1"},
|
|
{"PAY-0001", "a2"},
|
|
}
|
|
for _, tc := range cases {
|
|
got, err := w.Pick("fake|global", cs, accounts.PickHint{OutTradeNo: tc.key})
|
|
if err != nil || got.AccountID != tc.want {
|
|
t.Fatalf("key=%s: weight=0 应按 1 处理走桶, want %s got %v err %v", tc.key, tc.want, got, err)
|
|
}
|
|
}
|
|
}
|