Files
dygou_go/internal/seckill/scheduler_test.go
T

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)
}
}