Files
huya-go/internal/gui/app_test.go
T
yml2213 6e5beac151 refactor(sniper): 对齐 dygou_go 调度架构,增加双时间日志并解耦预热
- 抢购调度重构:参考 dygou_go 将 sniper 拆分为独立子模块(runner/run_control/retry/exchange/classify/config/summary/category/helpers);移除全局自适应降速与轮次上限,采用账号独立循环、统一账号级退避、单请求并发闸门与首发错峰
- 预热与建连优化:默认关闭预热(避免单账号超时连坐整机开抢),开启时转为后台非阻塞协程;WebSocket 握手超时从 10s 降至 1500ms
- 双时间日志:logger 增加 SetServerOffset,校准后日志前缀自动渲染「本地时间(H虎牙估算时间)」,实现跨机器绝对毫秒对齐
- 默认配置调优:版本提升至 v0.5.0,默认线程调为 2(缓解多实例并发限流),前端自动平滑迁移
- 验证方式:go test ./... -count=1 全通,npm run build 构建正常,单测覆盖双时间与预热配置
2026-10-17 18:29:17 +08:00

196 lines
5.5 KiB
Go

package gui
import (
"testing"
"time"
"huya-go/internal/logger"
)
func TestSplitCookieLinesNormalizesLineBreaks(t *testing.T) {
tests := []struct {
name string
input string
want []string
}{
{
name: "crlf_and_cr",
input: "ck-a\r\nck-b\rck-c",
want: []string{"ck-a", "ck-b", "ck-c"},
},
{
name: "escaped_line_breaks",
input: `ck-a\r\nck-b\rck-c\nck-d`,
want: []string{"ck-a", "ck-b", "ck-c", "ck-d"},
},
{
name: "slash_r_line_breaks",
input: "ck-a/r/nck-b/rck-c/nck-d",
want: []string{"ck-a", "ck-b", "ck-c", "ck-d"},
},
{
name: "slash_n_only_is_preserved",
input: "ck-a/nck-b",
want: []string{"ck-a/nck-b"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lines := splitCookieLines(tt.input)
if len(lines) != len(tt.want) {
t.Fatalf("len(splitCookieLines()) = %d, want %d", len(lines), len(tt.want))
}
for i, line := range lines {
if line.text != tt.want[i] {
t.Fatalf("line[%d] = %q, want %q", i, line.text, tt.want[i])
}
}
})
}
}
func TestParseAccountsRequiresExplicitSelection(t *testing.T) {
cfg := Config{
CookieText: "udb_uid=1; yyuid=1\nudb_uid=2; yyuid=2",
}
if _, err := parseAccounts(cfg); err == nil {
t.Fatal("空选择不应回退为全部账号")
}
cfg.SelectedAccountLines = []int{2}
accounts, err := parseAccounts(cfg)
if err != nil {
t.Fatalf("parseAccounts() error = %v", err)
}
if len(accounts) != 1 || accounts[0].UID != 2 {
t.Fatalf("accounts=%+v, want 仅第 2 行账号", accounts)
}
}
func TestDefaultConfigUsesWindowMode(t *testing.T) {
cfg := (&App{}).DefaultConfig()
if cfg.LeadMS != 0 {
t.Fatalf("LeadMS=%d, want 0(动态提前量)", cfg.LeadMS)
}
if cfg.Concurrency != 2 {
t.Fatalf("Concurrency=%d, want 2(降低多开时的请求洪峰)", cfg.Concurrency)
}
if cfg.Prewarm {
t.Fatal("Prewarm 应默认关闭,避免开抢前因单账号建连超时阻塞")
}
}
func TestParseStartTimeAcceptsClockOnly(t *testing.T) {
t.Run("future_today", func(t *testing.T) {
now := time.Date(2026, 7, 6, 10, 20, 30, 0, time.Local)
got := nextClockTime(now, 12, 5, 6)
want := time.Date(2026, 7, 6, 12, 5, 6, 0, time.Local)
if !got.Equal(want) {
t.Fatalf("nextClockTime() = %s, want %s", got, want)
}
})
t.Run("past_tomorrow", func(t *testing.T) {
now := time.Date(2026, 7, 6, 10, 20, 30, 0, time.Local)
got := nextClockTime(now, 9, 5, 6)
want := time.Date(2026, 7, 7, 9, 5, 6, 0, time.Local)
if !got.Equal(want) {
t.Fatalf("nextClockTime() = %s, want %s", got, want)
}
})
t.Run("parse_hms", func(t *testing.T) {
got, err := parseStartTime("23:59:58")
if err != nil {
t.Fatalf("parseStartTime() error = %v", err)
}
if got.Hour() != 23 || got.Minute() != 59 || got.Second() != 58 {
t.Fatalf("parseStartTime() = %s, want 23:59:58", got)
}
})
}
func TestSetLogLevelNormalizesAndFilters(t *testing.T) {
dir := t.TempDir()
store := logger.NewStore(20)
closeLog, err := logger.Init(store, dir, "测试实例", "test", dir)
if err != nil {
t.Fatalf("logger.Init() error = %v", err)
}
defer closeLog()
defer logger.SetMinLevel(logger.LevelInfo)
app := &App{logStore: store}
if got := app.SetLogLevel("这不是合法等级"); got != logger.LevelInfo {
t.Fatalf("非法等级应回退为 info, got %q", got)
}
if got := app.SetLogLevel("debug"); got != logger.LevelDebug {
t.Fatalf("SetLogLevel(debug) = %q", got)
}
logger.LogEvent("网络", logger.LevelDebug, "debug 应被保留")
app.SetLogLevel("error")
logger.LogEvent("网络", logger.LevelDebug, "debug 应被过滤")
for _, entry := range store.Since(0) {
if entry.Message == "debug 应被过滤" {
t.Fatal("低于最低等级的日志不应进入内存缓冲")
}
}
}
func TestParseTargetsAppliesPriceAndInventoryFilters(t *testing.T) {
targets := parseTargets(Config{
PID: 12889,
Name: "小黄鸭",
TargetText: "13001\nauto",
AutoSelect: true,
MaxPrice: 1000,
MinLeft: 2,
})
if len(targets) != 4 {
t.Fatalf("targets=%d, want 4", len(targets))
}
for _, target := range targets {
if target.MaxPrice != 1000 {
t.Fatalf("target %+v maxPrice=%d, want 1000", target, target.MaxPrice)
}
if target.MinLeft != 2 {
t.Fatalf("target %+v minLeft=%d, want 2", target, target.MinLeft)
}
}
}
func TestParseTargetsWithoutFiltersKeepsZeroValues(t *testing.T) {
targets := parseTargets(Config{PID: 12889})
if len(targets) != 1 {
t.Fatalf("targets=%d, want 1", len(targets))
}
if targets[0].MaxPrice != 0 || targets[0].MinLeft != 0 {
t.Fatalf("unexpected filters: %+v", targets[0])
}
}
// TestRunnerConfigMapsDirectExchangeToExchangeMode 验证界面「直接兑换」开关能真正
// 切换到「先查列表」模式:引擎在 ListBeforeExchange=false 时会强制回落到直兑,
// 所以关闭直接兑换必须同时置 ListBeforeExchange,否则开关形同失效。
func TestRunnerConfigMapsDirectExchangeToExchangeMode(t *testing.T) {
direct := runnerConfig(Config{
SID: 2203,
DelayMS: 120,
Concurrency: 5,
DirectExchange: true,
})
if !direct.DirectExchange || direct.ListBeforeExchange {
t.Fatalf("直接兑换模式映射错误: %+v", direct)
}
if direct.SID != 2203 || direct.Delay != 120*time.Millisecond || direct.Concurrency != 5 {
t.Fatalf("基础字段未透传: %+v", direct)
}
list := runnerConfig(Config{SID: 2203, DirectExchange: false})
if list.DirectExchange || !list.ListBeforeExchange {
t.Fatalf("关闭直接兑换后应改为先查列表: %+v", list)
}
}