Files
huya-go/internal/sniper/sniper_test.go
T
yml2213 0294606807 参考 dygou_go 优化抢购调度:请求级并发闸门、售罄继续探测与任务汇总
- 并发额度下沉到单次请求:runState.sem 只覆盖一次在途请求(列表/兑换/刷新),账号在退避等待期间释放额度;旧实现把额度给整个账号循环,退避中的账号一直霸占额度,账号多时互相饿死
- 售罄不再默认终止:normalizeConfig 去掉 StopOnSoldOut 默认 true,售罄按退避继续探测到轮次结束(别人取消或锁单过期会让库存回流);CategoryOffShelf 拆成独立分支,下架/不存在仍终止该奖品;删除已无意义的 ContinueOnSoldOut
- 退避统一加 0-25% 抖动(jitterDuration,只往上抖,配置值仍是下限),避免多账号在同一时刻整批重试形成周期尖峰
- 新增 Runner.OnSummary/RunSummary:轮次、账号、目标、耗时、列表/兑换/积分/记录请求次数、成功账号数与结局分布(按次数排序),在 refresh.Wait() 之后回调;CLI 与 GUI 各输出一行汇总加逐类结局
- 取消清理:等待额度的账号返回 action=exchange_canceled 且调用方直接返回,停止监控时不再为每个排队账号刷错误结果
- http 传输补连接池参数(MaxConnsPerHost=64、MaxIdleConns=128、MaxIdleConnsPerHost=64、IdleConnTimeout=30s),多账号不再反复重建连接
- CLI/GUI 的 waitUntil 从 200ms 轮询改为一次性定时器,消除 0-200ms 随机延迟(正好错过开抢窗口)
- GUI 抽出 runnerConfig(),用 ListBeforeExchange=!DirectExchange 表达界面语义,修复「直接兑换」开关失效(旧实现被引擎强制回落直兑,价格/库存过滤不生效)
- 测试:sniper 新增售罄继续探测、StopOnSoldOut 即停、下架终止、在途请求数等于并发数、汇总含刷新请求、抖动区间等用例;GUI 新增 runnerConfig 用例;README 补「抢购调度与退避(2026-10-16 调整)」
2026-10-16 16:06:49 +08:00

664 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sniper
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"huya-go/internal/huya"
)
type fakeActivityClient struct {
mu sync.Mutex
listCount int
exchangeCount int
prewarmCount int
scoreCount int
recordCount int
listResp *huya.GetActPrizeListResp
exchangeFn func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error)
}
func (f *fakeActivityClient) GetActPrizeList(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetActPrizeListResp, error) {
f.mu.Lock()
f.listCount++
f.mu.Unlock()
if f.listResp != nil {
return f.listResp, nil
}
return &huya.GetActPrizeListResp{}, nil
}
func (f *fakeActivityClient) QueryUserScore(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetUserScoreResp, error) {
f.mu.Lock()
f.scoreCount++
f.mu.Unlock()
return &huya.GetUserScoreResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) GetUserPrizeRecords(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetUserPrizeRecordsResp, error) {
f.mu.Lock()
f.recordCount++
f.mu.Unlock()
return &huya.GetUserPrizeRecordsResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) ScoreExchangePrize(ctx context.Context, uid int64, cookie string, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
f.mu.Lock()
f.exchangeCount++
f.mu.Unlock()
if f.exchangeFn != nil {
return f.exchangeFn(uid, sid, pid)
}
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) PrewarmActivity(ctx context.Context, uid int64, cookie string, sid int32) error {
f.mu.Lock()
f.prewarmCount++
f.mu.Unlock()
return nil
}
func (f *fakeActivityClient) counts() (int, int, int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.listCount, f.exchangeCount, f.prewarmCount
}
func TestClassifyExchange(t *testing.T) {
tests := []struct {
name string
resp *huya.ScoreExchangePrizeResp
err error
want string
}{
{name: "success", resp: &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, want: CategorySuccess},
{name: "frequent", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, want: CategoryTooFrequent},
{name: "not open", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品暂未开放兑换"}, want: CategoryNotOpen},
{name: "not enough", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "兑换货币不足"}, want: CategoryNotEnough},
{name: "already", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "已兑换该奖品"}, want: CategoryAlreadyDone},
{name: "off shelf", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品已下架"}, want: CategoryOffShelf},
{name: "sold out", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "库存不足"}, want: CategorySoldOut},
{name: "error", err: errors.New("timeout"), want: CategoryError},
{
name: "success with order id",
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "sOrderId_2203_12889_1_1783328404_957"},
want: CategorySuccess,
},
{
name: "not enough in precondition tip",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
Msg: "兑换失败",
ActPreCondition: huya.ActPreCondition{Tip: "积分不足,无法兑换"},
},
want: CategoryNotEnough,
},
{
name: "not enough in condition params",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
Msg: "兑换失败",
ActPreCondition: huya.ActPreCondition{ConditionParams: map[string]string{"tip": "余额不足"}},
},
want: CategoryNotEnough,
},
{
name: "condition gate without text",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
ActPreCondition: huya.ActPreCondition{ConditionType: 2},
},
want: CategoryCondition,
},
{name: "empty response", resp: &huya.ScoreExchangePrizeResp{Status: 500}, want: CategoryFailed},
{name: "nil response", resp: nil, want: CategoryFailed},
{
name: "text wins over order id",
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "order", Msg: "库存不足"},
want: CategorySoldOut,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ClassifyExchange(tt.resp, tt.err); got != tt.want {
t.Fatalf("ClassifyExchange()=%s, want %s", got, tt.want)
}
})
}
}
func TestRunTargetsDirectExchangeSkipsListAndStopsPerAccount(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 5,
Delay: time.Millisecond,
Concurrency: 2,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
listCount, exchangeCount, _ := client.counts()
if listCount != 0 {
t.Fatalf("listCount=%d, want 0", listCount)
}
if exchangeCount != len(accounts) {
t.Fatalf("exchangeCount=%d, want %d", exchangeCount, len(accounts))
}
}
func TestRunTargetsTerminalNotEnoughStopsAccountTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "兑换货币不足"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 4,
Delay: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1", exchangeCount)
}
}
func TestRunTargetsTooFrequentBackoffSkipsNextLoop(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 2,
Delay: time.Millisecond,
TooFrequentBackoff: 50 * time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1", exchangeCount)
}
}
func TestRunTargetsSharedListForNameTargets(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Prizes: []huya.PrizeInfo{{ID: 12889, SID: 2203, Name: "套装-潮萌小黄鸭", NewScore: 988, LeftNum: 10}},
},
}
runner := Runner{Client: client}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
err := runner.RunTargets(context.Background(), accounts, []Target{{Name: "小黄鸭"}}, Config{
SID: 2203,
Loops: 1,
Concurrency: 2,
StopAccountAfterSuccess: true,
ListBeforeExchange: true,
SharePrizeList: true,
}, nil)
if err != nil {
t.Fatal(err)
}
listCount, exchangeCount, _ := client.counts()
if listCount != 1 {
t.Fatalf("listCount=%d, want 1", listCount)
}
if exchangeCount != len(accounts) {
t.Fatalf("exchangeCount=%d, want %d", exchangeCount, len(accounts))
}
}
func TestRunTargetsAutoSelectPrefersPrizeWithLeft(t *testing.T) {
var gotPID int32
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Prizes: []huya.PrizeInfo{
{ID: 1, SID: 2203, Name: "无剩余", NewScore: 10, LeftNum: 0},
{ID: 2, SID: 2203, Name: "有剩余", NewScore: 20, LeftNum: 5},
},
},
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
gotPID = pid
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Auto: true}}, Config{
SID: 2203,
Loops: 1,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
if gotPID != 2 {
t.Fatalf("gotPID=%d, want 2", gotPID)
}
}
// TestRunTargetsCategoryFilter 验证按类目限定后只会在该类的商品里成交。
func TestRunTargetsCategoryFilter(t *testing.T) {
var gotPID int32
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Tags: []huya.PrizeTag{
{ID: 101, Name: "精英专享", Weight: 2},
{ID: 99, Name: "限量返场", Weight: 1},
},
Prizes: []huya.PrizeInfo{
{ID: 13415, Name: "套装-雪山精英", NewScore: 600, LeftNum: 100, Tags: "101"},
{ID: 13433, Name: "怪诞乔克服装礼包", NewScore: 8000, LeftNum: 50, Tags: "99"},
},
},
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
gotPID = pid
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Auto: true}}, Config{
SID: 2277,
Category: "限量返场",
Loops: 1,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
if gotPID != 13433 {
t.Fatalf("gotPID=%d, want 13433(应只在限量返场里选品)", gotPID)
}
}
// TestRunTargetsCategoryFilterMisses 验证类目里没有目标时给出带类目的未命中提示。
func TestRunTargetsCategoryFilterMisses(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Tags: []huya.PrizeTag{{ID: 99, Name: "限量返场", Weight: 1}},
Prizes: []huya.PrizeInfo{{ID: 13415, Name: "套装-雪山精英", NewScore: 600, Tags: "101"}},
},
}
results := make([]Result, 0, 4)
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Name: "雪山"}}, Config{
SID: 2277,
Category: "限量返场",
Loops: 1,
ListBeforeExchange: true,
}, func(result Result) {
results = append(results, result)
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Category != CategoryNotFound {
t.Fatalf("results=%+v, want 单条 not_found", results)
}
if results[0].Message != "类目[限量返场]内未查询到所需物品,继续监控" {
t.Fatalf("Message=%q", results[0].Message)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 0 {
t.Fatalf("exchangeCount=%d, want 0", exchangeCount)
}
}
func TestRunTargetsRefreshAfterSuccessEmitsRefreshResults(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
results := make(chan Result, 8)
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 1,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(result Result) {
results <- result
})
if err != nil {
t.Fatal(err)
}
seenScore := false
seenRecords := false
deadline := time.After(time.Second)
for !seenScore || !seenRecords {
select {
case result := <-results:
seenScore = seenScore || result.Action == "refresh_score"
seenRecords = seenRecords || result.Action == "refresh_records"
case <-deadline:
t.Fatalf("未收到刷新结果: score=%v records=%v", seenScore, seenRecords)
}
}
}
func TestRunTargetsConditionGateKeepsRetrying(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{
Status: 500,
ActPreCondition: huya.ActPreCondition{ConditionType: 3},
}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 3,
Delay: 10 * time.Millisecond,
ErrorBackoff: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 3 {
t.Fatalf("exchangeCount=%d, want 3", exchangeCount)
}
}
func TestNormalizeTargetsDropsDuplicatePrizeTargets(t *testing.T) {
targets := normalizeTargets([]Target{
{PID: 13409, Name: "幸运币礼包(小)"},
{PID: 13409},
{Name: "幸运币礼包(小)"},
{PID: 12889, Name: "套装-潮萌小黄鸭"},
})
if len(targets) != 2 {
t.Fatalf("targets=%d, want 2: %+v", len(targets), targets)
}
if targets[0].PID != 13409 || targets[1].PID != 12889 {
t.Fatalf("unexpected targets: %+v", targets)
}
}
func TestRunTargetsExchangesPrizeOnceWhenTargetsDuplicated(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{
{PID: 13409, Name: "幸运币礼包(小)"},
{PID: 13409},
}, Config{
SID: 2277,
Loops: 3,
Delay: time.Millisecond,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(同一奖品不应重复兑换)", exchangeCount)
}
}
func TestRunTargetsWaitsForRefreshBeforeReturn(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
var mu sync.Mutex
seen := map[string]bool{}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 13409}}, Config{
SID: 2277,
Loops: 1,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(result Result) {
mu.Lock()
seen[result.Action] = true
mu.Unlock()
})
if err != nil {
t.Fatal(err)
}
mu.Lock()
defer mu.Unlock()
if !seen["refresh_score"] || !seen["refresh_records"] {
t.Fatalf("RunTargets 返回时异步刷新还没结束: %v", seen)
}
}
func TestRunTargetsDoesNotExchangeSamePrizeTwiceViaNameMatch(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Prizes: []huya.PrizeInfo{{ID: 13409, Name: "幸运币礼包(小)", NewScore: 20}},
},
}
runner := Runner{Client: client}
// 两个不同名称的 target 都会匹配到同一个奖品。
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{
{Name: "幸运币"},
{Name: "礼包"},
}, Config{
SID: 2277,
Loops: 2,
Delay: time.Millisecond,
DirectExchange: false,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(同一奖品不应重复兑换)", exchangeCount)
}
}
// TestRunTargetsSoldOutKeepsProbingUntilSuccess 验证「已抢完」不再默认终止:
// 库存可能因为别人取消 / 锁单过期回流,窗口内要继续探测到成功。
func TestRunTargetsSoldOutKeepsProbingUntilSuccess(t *testing.T) {
var mu sync.Mutex
calls := 0
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
mu.Lock()
calls++
current := calls
mu.Unlock()
if current <= 2 {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "该奖品已抢完"}, nil
}
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 6,
Delay: time.Millisecond,
ErrorBackoff: time.Millisecond,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 3 {
t.Fatalf("exchangeCount=%d, want 3(售罄后应继续探测直到成功)", exchangeCount)
}
}
// TestRunTargetsStopOnSoldOutTerminatesTarget 验证显式开启 StopOnSoldOut 时售罄即停。
func TestRunTargetsStopOnSoldOutTerminatesTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "该奖品已抢完"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 5,
Delay: time.Millisecond,
ErrorBackoff: time.Millisecond,
StopOnSoldOut: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(StopOnSoldOut 时售罄即停)", exchangeCount)
}
}
// TestRunTargetsOffShelfTerminatesTarget 验证下架/不存在仍然直接终止该 target。
func TestRunTargetsOffShelfTerminatesTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品已下架"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 4,
Delay: time.Millisecond,
ErrorBackoff: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(下架应终止该 target)", exchangeCount)
}
}
// TestRunTargetsConcurrencyLimitsInFlightRequests 验证并发额度作用在单次请求上:
// 在途请求数不会超过 Concurrency,但多个账号确实能同时在途。
func TestRunTargetsConcurrencyLimitsInFlightRequests(t *testing.T) {
var inFlight, peak int32
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
current := atomic.AddInt32(&inFlight, 1)
for {
observed := atomic.LoadInt32(&peak)
if current <= observed || atomic.CompareAndSwapInt32(&peak, observed, current) {
break
}
}
time.Sleep(10 * time.Millisecond)
atomic.AddInt32(&inFlight, -1)
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, nil
},
}
runner := Runner{Client: client}
accounts := make([]Account, 0, 6)
for uid := int64(1); uid <= 6; uid++ {
accounts = append(accounts, Account{UID: uid, Cookie: "yyuid=1"})
}
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 1,
Delay: time.Millisecond,
Concurrency: 2,
TooFrequentBackoff: 10 * time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
if got := atomic.LoadInt32(&peak); got != 2 {
t.Fatalf("peak in-flight=%d, want 2(并发额度应精确限制在途请求数)", got)
}
}
// TestRunTargetsEmitsSummary 验证任务结束时汇总请求次数与结局分布。
func TestRunTargetsEmitsSummary(t *testing.T) {
client := &fakeActivityClient{}
var summary RunSummary
runner := Runner{
Client: client,
OnSummary: func(item RunSummary) { summary = item },
}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
// 异步刷新只在有 emit 回调时才跑(没地方上报就不发请求),这里给个空回调。
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
Loops: 3,
Delay: time.Millisecond,
Concurrency: 2,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(Result) {})
if err != nil {
t.Fatal(err)
}
if summary.Accounts != len(accounts) || summary.Targets != 1 {
t.Fatalf("summary=%+v", summary)
}
if summary.ExchangeReqs != len(accounts) {
t.Fatalf("ExchangeReqs=%d, want %d", summary.ExchangeReqs, len(accounts))
}
if summary.ScoreReqs != len(accounts) || summary.RecordReqs != len(accounts) {
t.Fatalf("刷新请求未计入汇总: score=%d records=%d", summary.ScoreReqs, summary.RecordReqs)
}
if summary.SuccessUIDs != len(accounts) || summary.Outcomes[CategorySuccess] != len(accounts) {
t.Fatalf("成功统计不正确: %+v", summary)
}
if summary.Duration <= 0 {
t.Fatalf("Duration=%v, want > 0", summary.Duration)
}
}
// TestJitterDurationAddsUpToQuarter 验证抖动只往上加、最多 25%,且不退化为 0。
func TestJitterDurationAddsUpToQuarter(t *testing.T) {
base := 400 * time.Millisecond
for i := 0; i < 200; i++ {
got := jitterDuration(base)
if got < base || got > base+base/4 {
t.Fatalf("jitterDuration(%v)=%v, want [%v,%v]", base, got, base, base+base/4)
}
}
if got := jitterDuration(0); got != 0 {
t.Fatalf("jitterDuration(0)=%v, want 0", got)
}
if got := jitterDuration(time.Nanosecond); got != time.Nanosecond {
t.Fatalf("jitterDuration(1ns)=%v, want 1ns", got)
}
}