187 lines
6.2 KiB
Go
187 lines
6.2 KiB
Go
package seckill
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"douyu_go/internal/types"
|
|
)
|
|
|
|
func makeAttempts(names ...string) []types.ExchangeAttempt {
|
|
out := make([]types.ExchangeAttempt, 0, len(names))
|
|
for i, n := range names {
|
|
out = append(out, types.ExchangeAttempt{
|
|
Account: types.Account{Name: n},
|
|
Index: i + 1,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestRetryIntervalFor(t *testing.T) {
|
|
stages := []types.RetryStage{
|
|
{MaxElapsed: 4, Interval: 120},
|
|
{MaxElapsed: 12, Interval: 200},
|
|
{MaxElapsed: 30, Interval: 500},
|
|
{MaxElapsed: 75, Interval: 1000},
|
|
}
|
|
cases := []struct {
|
|
elapsed time.Duration
|
|
want time.Duration
|
|
}{
|
|
{1 * time.Second, 120 * time.Millisecond},
|
|
{5 * time.Second, 200 * time.Millisecond},
|
|
{13 * time.Second, 500 * time.Millisecond},
|
|
{31 * time.Second, 1000 * time.Millisecond},
|
|
{100 * time.Second, 900 * time.Millisecond}, // 超出所有阶段,用默认值
|
|
}
|
|
for _, c := range cases {
|
|
if got := retryIntervalFor(stages, c.elapsed); got != c.want {
|
|
t.Errorf("retryIntervalFor(%v) = %v, want %v", c.elapsed, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRetryIntervalForEmpty(t *testing.T) {
|
|
// 空阶段或无效阶段都回退到默认 900ms
|
|
if got := retryIntervalFor(nil, 1*time.Second); got != 900*time.Millisecond {
|
|
t.Errorf("retryIntervalFor(nil) = %v, want 900ms", got)
|
|
}
|
|
if got := retryIntervalFor([]types.RetryStage{{MaxElapsed: 0, Interval: 0}}, 1*time.Second); got != 900*time.Millisecond {
|
|
t.Errorf("retryIntervalFor(invalid) = %v, want 900ms", got)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeXiaodianRun(t *testing.T) {
|
|
attempts, delay := normalizeXiaodianRun(0, 0, 4)
|
|
if attempts != 4 || delay != xiaodianDefaultDelay {
|
|
t.Fatalf("normalizeXiaodianRun fallback = (%d,%v), want (4,%v)", attempts, delay, xiaodianDefaultDelay)
|
|
}
|
|
attempts, delay = normalizeXiaodianRun(100001, 70000, 0)
|
|
if attempts != 100000 || delay != 60*time.Second {
|
|
t.Fatalf("normalizeXiaodianRun clamp = (%d,%v), want (100000,60s)", attempts, delay)
|
|
}
|
|
attempts, delay = normalizeXiaodianRun(2, 150, 0)
|
|
if attempts != 2 || delay != 150*time.Millisecond {
|
|
t.Fatalf("normalizeXiaodianRun explicit = (%d,%v), want (2,150ms)", attempts, delay)
|
|
}
|
|
}
|
|
|
|
func TestRetryCooldownDuration(t *testing.T) {
|
|
cases := []struct {
|
|
msg string
|
|
want time.Duration
|
|
}{
|
|
{"用户行为异常,请稍后再试", behaviorCooldown},
|
|
{"点击太快了", clickFastCooldown},
|
|
{"操作频繁请稍后", behaviorCooldown},
|
|
{"请求过多", behaviorCooldown},
|
|
{"请求太频繁", behaviorCooldown},
|
|
{"兑换成功", 0},
|
|
{"库存不足", 0},
|
|
}
|
|
for _, c := range cases {
|
|
if got := retryCooldownDuration(c.msg); got != c.want {
|
|
t.Errorf("retryCooldownDuration(%q) = %v, want %v", c.msg, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClassifyRetryStop(t *testing.T) {
|
|
acc, prod, reason := classifyRetryStop("您已达本次活动兑换上限")
|
|
if !acc || prod || reason != "已达兑换上限" {
|
|
t.Errorf("classifyRetryStop(兑换上限) = (%v,%v,%q), want (true,false,'已达兑换上限')", acc, prod, reason)
|
|
}
|
|
acc, prod, reason = classifyRetryStop("未绑定账号")
|
|
if !acc || prod || reason != "账号未绑定角色" {
|
|
t.Errorf("classifyRetryStop(未绑定) = (%v,%v,%q), want (true,false,'账号未绑定角色')", acc, prod, reason)
|
|
}
|
|
acc, prod, reason = classifyRetryStop("需要开通精英手册")
|
|
if !acc || prod || reason != "账号不满足精英手册条件" {
|
|
t.Errorf("classifyRetryStop(精英手册) = (%v,%v,%q), want (true,false,'账号不满足精英手册条件')", acc, prod, reason)
|
|
}
|
|
// 库存耗尽不停止
|
|
acc, prod, reason = classifyRetryStop("商品本次活动库存已耗完")
|
|
if acc || prod || reason != "" {
|
|
t.Errorf("classifyRetryStop(库存耗完) = (%v,%v,%q), want (false,false,'')", acc, prod, reason)
|
|
}
|
|
// 普通错误不停止
|
|
acc, prod, reason = classifyRetryStop("网络超时")
|
|
if acc || prod || reason != "" {
|
|
t.Errorf("classifyRetryStop(普通错误) = (%v,%v,%q), want (false,false,'')", acc, prod, reason)
|
|
}
|
|
}
|
|
|
|
func TestClassifyXiaodianRetryStopOwnedItem(t *testing.T) {
|
|
for _, message := range []string{"您已拥有该道具!", "已经拥有该道具", "账号已拥有该商品"} {
|
|
acc, prod, reason := classifyXiaodianRetryStop(message)
|
|
if !acc || prod || reason != "账号已拥有该道具" {
|
|
t.Errorf("classifyXiaodianRetryStop(%q) = (%v,%v,%q), want (true,false,'账号已拥有该道具')", message, acc, prod, reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFilterCooldownAttempts(t *testing.T) {
|
|
attempts := makeAttempts("a", "b", "c")
|
|
now := time.Now()
|
|
cooldown := map[string]time.Time{
|
|
"a": now.Add(5 * time.Second), // 仍在冷却
|
|
"b": now.Add(-5 * time.Second), // 已过期
|
|
}
|
|
out, nextReady := filterCooldownAttempts(attempts, cooldown, now)
|
|
if len(out) != 2 {
|
|
t.Fatalf("filterCooldownAttempts out length = %d, want 2", len(out))
|
|
}
|
|
for _, a := range out {
|
|
if a.Account.Name == "a" {
|
|
t.Errorf("account 'a' should be filtered out")
|
|
}
|
|
}
|
|
if nextReady.IsZero() {
|
|
t.Errorf("nextReady should not be zero")
|
|
}
|
|
if !nextReady.After(now) {
|
|
t.Errorf("nextReady should be after now")
|
|
}
|
|
}
|
|
|
|
func TestPickRoundAttempts(t *testing.T) {
|
|
// 账号 A 有 3 个包,账号 B 有 3 个包
|
|
attempts := []types.ExchangeAttempt{
|
|
{Account: types.Account{Name: "A"}, Index: 1},
|
|
{Account: types.Account{Name: "A"}, Index: 2},
|
|
{Account: types.Account{Name: "A"}, Index: 3},
|
|
{Account: types.Account{Name: "B"}, Index: 1},
|
|
{Account: types.Account{Name: "B"}, Index: 2},
|
|
{Account: types.Account{Name: "B"}, Index: 3},
|
|
}
|
|
// 第 1 轮每账号选第 1 包
|
|
out := pickRoundAttempts(attempts, 1, 3)
|
|
if len(out) != 2 {
|
|
t.Fatalf("pickRoundAttempts round1 length = %d, want 2", len(out))
|
|
}
|
|
for _, a := range out {
|
|
if a.Index != 1 {
|
|
t.Errorf("round1 should pick index 1, got %d for %s", a.Index, a.Account.Name)
|
|
}
|
|
}
|
|
// 第 2 轮每账号选第 2 包
|
|
out = pickRoundAttempts(attempts, 2, 3)
|
|
for _, a := range out {
|
|
if a.Index != 2 {
|
|
t.Errorf("round2 should pick index 2, got %d for %s", a.Index, a.Account.Name)
|
|
}
|
|
}
|
|
// attemptsPerAccount=0 时回退取各自首个
|
|
out = pickRoundAttempts(attempts, 1, 0)
|
|
for _, a := range out {
|
|
if a.Index != 1 {
|
|
t.Errorf("round with 0 perAccount should pick index 1, got %d", a.Index)
|
|
}
|
|
}
|
|
// 空输入
|
|
if out := pickRoundAttempts(nil, 1, 3); out != nil {
|
|
t.Errorf("pickRoundAttempts(nil) = %v, want nil", out)
|
|
}
|
|
}
|