Files
huya-go/internal/logger/logger_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

237 lines
6.3 KiB
Go

package logger
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func initForTest(t *testing.T) (*Store, string, func()) {
t.Helper()
dir := t.TempDir()
store := NewStore(200)
closeLog, err := Init(store, dir, "测试实例", "test", dir)
if err != nil {
t.Fatalf("Init() error = %v", err)
}
t.Cleanup(func() {
SetMinLevel(LevelInfo)
SetEmitter(nil)
})
return store, FilePath(), closeLog
}
// TestInitWritesHeaderAndStructuredLines 验证日志文件带实例表头,
// 且每一行包含分类、级别与账号标签。
func TestInitWritesHeaderAndStructuredLines(t *testing.T) {
store, path, closeLog := initForTest(t)
if path == "" {
t.Fatal("日志文件路径为空")
}
LogEvent("配置", LevelInfo, "活动 SID=%d", 2203)
LogAccountEvent("监控", LevelSuccess, "88231", "兑换成功 pid=%d", 12889)
closeLog()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("读取日志文件失败: %v", err)
}
text := string(data)
for _, want := range []string{
"实例: 测试实例 (test)",
"数据目录:",
"[配置][info] 活动 SID=2203",
"[监控][success][账号:88231] 兑换成功 pid=12889",
} {
if !strings.Contains(text, want) {
t.Fatalf("日志文件缺少 %q:\n%s", want, text)
}
}
if store.Count() < 3 {
t.Fatalf("内存日志条数 = %d, want >= 3", store.Count())
}
}
// TestUniqueLogFileAvoidsSameSecondCollision 同一秒内两次创建也不能拿到同一个文件:
// 多开或快速重启时,两个进程写同一个文件会让日志互相污染。
func TestUniqueLogFileAvoidsSameSecondCollision(t *testing.T) {
dir := t.TempDir()
first, err := newFileWriter(dir, "test")
if err != nil {
t.Fatal(err)
}
defer first.Close()
second, err := newFileWriter(dir, "test")
if err != nil {
t.Fatal(err)
}
defer second.Close()
if first.Path() == second.Path() {
t.Fatalf("两个进程拿到了同一个日志文件: %s", first.Path())
}
if filepath.Dir(first.Path()) != dir {
t.Fatalf("日志目录 = %q, want %q", filepath.Dir(first.Path()), dir)
}
}
// TestFileWriterRotatesAndDropsOldest 验证按大小滚动并丢弃最旧的备份。
func TestFileWriterRotatesAndDropsOldest(t *testing.T) {
dir := t.TempDir()
writer, err := newFileWriter(dir, "test")
if err != nil {
t.Fatal(err)
}
defer writer.Close()
writer.maxBytes = 200
writer.backups = 2
for i := 0; i < 60; i++ {
if _, err := writer.Write([]byte(fmt.Sprintf("第 %d 条日志,用于触发滚动\n", i))); err != nil {
t.Fatal(err)
}
}
if _, err := os.Stat(writer.Path() + ".1"); err != nil {
t.Fatalf("期望存在滚动文件 .1: %v", err)
}
if _, err := os.Stat(writer.Path() + ".2"); err != nil {
t.Fatalf("期望存在滚动文件 .2: %v", err)
}
if _, err := os.Stat(writer.Path() + ".3"); err == nil {
t.Fatal("backups=2 时不应出现 .3")
}
}
type blockingWriter struct {
release chan struct{}
}
func (w blockingWriter) Write(p []byte) (int, error) {
<-w.release
return len(p), nil
}
// TestAsyncWriterDropsInsteadOfBlocking 队列满时必须丢弃日志而不是阻塞调用方,
// 否则抢购主链路会被磁盘 I/O 拖住。
func TestAsyncWriterDropsInsteadOfBlocking(t *testing.T) {
release := make(chan struct{})
writer := NewAsyncWriter(blockingWriter{release: release}, 1)
start := time.Now()
for i := 0; i < 100; i++ {
if _, err := writer.Write([]byte("x\n")); err != nil {
t.Fatalf("Write() error = %v", err)
}
}
if elapsed := time.Since(start); elapsed > time.Second {
t.Fatalf("写入被阻塞了 %s", elapsed)
}
if writer.Dropped() == 0 {
t.Fatal("队列满时应记录丢弃条数")
}
close(release)
if err := writer.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
}
// TestStoreSinceIsIncremental 验证界面增量拉取语义:只返回 ID 更大的日志。
func TestStoreSinceIsIncremental(t *testing.T) {
store := NewStore(3)
first := store.Append(Entry{Message: "第一条"})
if first.ID == 0 {
t.Fatal("Append 应分配自增 ID")
}
store.Append(Entry{Message: "第二条"})
if got := store.Since(0); len(got) != 2 {
t.Fatalf("Since(0) 条数 = %d, want 2", len(got))
}
if got := store.Since(first.ID); len(got) != 1 || got[0].Message != "第二条" {
t.Fatalf("Since(first) = %+v", got)
}
// 超出容量时丢弃最旧的条目。
store.Append(Entry{Message: "第三条"})
store.Append(Entry{Message: "第四条"})
if got := store.Since(0); len(got) != 3 || got[0].Message != "第二条" {
t.Fatalf("环形缓冲 = %+v", got)
}
if store.Count() != 3 {
t.Fatalf("Count() = %d, want 3", store.Count())
}
}
// TestMinLevelFiltersDebug 低等级日志不进入内存缓冲,也不落盘。
func TestMinLevelFiltersDebug(t *testing.T) {
store, _, closeLog := initForTest(t)
defer func() {
SetMinLevel(LevelInfo)
closeLog()
}()
SetMinLevel(LevelDebug)
LogEvent("网络", LevelDebug, "debug 应保留")
if MinLevel() != LevelDebug {
t.Fatalf("MinLevel() = %q", MinLevel())
}
SetMinLevel(LevelError)
LogEvent("网络", LevelDebug, "debug 应被过滤")
if MinLevel() != LevelError {
t.Fatalf("MinLevel() = %q", MinLevel())
}
for _, entry := range store.Since(0) {
if entry.Message == "debug 应被过滤" {
t.Fatal("低于最低等级的日志不应进入内存缓冲")
}
}
if got := LevelFromString("不是等级"); got != LevelInfo {
t.Fatalf("LevelFromString(非法) = %q, want info", got)
}
}
// TestDoubleTimeWithServerOffset 验证设置服务端时间偏移后,日志带双时间前缀。
func TestDoubleTimeWithServerOffset(t *testing.T) {
store, path, closeLog := initForTest(t)
defer func() {
SetServerOffset(0)
closeLog()
}()
// 模拟服务端比本地快 500ms
SetServerOffset(500 * time.Millisecond)
if ServerOffset() != 500*time.Millisecond {
t.Fatalf("ServerOffset() = %v, want 500ms", ServerOffset())
}
LogEvent("监控", LevelInfo, "双时间测试消息")
closeLog()
entries := store.Since(0)
if len(entries) == 0 {
t.Fatal("未写入日志条目")
}
last := entries[len(entries)-1]
if !strings.Contains(last.Time, "(H") {
t.Fatalf("条目 Time 未包含虎牙估算时间前缀: %s", last.Time)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("读取日志文件失败: %v", err)
}
if !strings.Contains(string(data), "(H") {
t.Fatalf("文件未包含虎牙估算时间前缀:\n%s", string(data))
}
}