- 锁单后首次支付的等待从 0.3-0.7 秒改回 2.3-3.0 秒。斗鱼对"锁单→支付"存在最短间隔: 2026-10-03 19:38 的模拟里,+0.50s/+0.60s/+1.37s/+1.68s 发起的支付一律被 "您点击太快,请稍等再试"拒绝,而 +2.56s/+3.30s 发起的都成功;19:20 那次在 +2.1s~+3.9s 发起的 6 笔全部一次通过,阈值落在 (1.68s, 2.1s] 之间。 0.3.1 把等待压到 0.3-0.7 秒踩进该区间,6 个号各白挨两次拒绝,反而多花 2.7 秒、 多打 12 个请求——上一版"2-4 秒纯属浪费"的判断是错的,那段等待本就是躲这条服务端规则 - 日志文件表头补充版本号与构建时间:此前无法从日志判断用户跑的是哪个包 - 新增测试断言表头包含版本号
139 lines
4.2 KiB
Go
139 lines
4.2 KiB
Go
package logger
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestStructuredLogging 验证 LogEvent→sink→MemoryLogStore 结构化链路:
|
|
// LogEntry 的 category/level/account 应直接由调用方填入,而非字符串反解析。
|
|
func TestStructuredLogging(t *testing.T) {
|
|
store := NewMemoryLogStore(100)
|
|
cleanup := InitLogging(store, t.TempDir(), "测试实例", "/tmp/douyu-go-test")
|
|
defer func() {
|
|
cleanup()
|
|
globalSink = nil
|
|
}()
|
|
|
|
LogEvent("抢购", "success", "兑换成功: %s", "商品A")
|
|
LogAccountEvent("抢购", "warn", "testuser", "冷却: %ds", 5)
|
|
|
|
entries := store.Since(0)
|
|
// InitLogging 自身产生 1 条"日志文件已启用",加上上述 2 条,共 3 条
|
|
if len(entries) != 3 {
|
|
t.Fatalf("Since(0) = %d entries, want 3", len(entries))
|
|
}
|
|
if entries[0].Category != "日志" {
|
|
t.Errorf("entry0 Category = %q, want '日志' (InitLogging 启动日志)", entries[0].Category)
|
|
}
|
|
|
|
e1 := entries[1]
|
|
if e1.ID != 2 {
|
|
t.Errorf("entry1 ID = %d, want 2", e1.ID)
|
|
}
|
|
if e1.Category != "抢购" {
|
|
t.Errorf("entry1 Category = %q, want '抢购'", e1.Category)
|
|
}
|
|
if e1.Level != "success" {
|
|
t.Errorf("entry1 Level = %q, want 'success'", e1.Level)
|
|
}
|
|
if e1.Account != "" {
|
|
t.Errorf("entry1 Account = %q, want empty", e1.Account)
|
|
}
|
|
if e1.Time == "" {
|
|
t.Error("entry1 Time is empty")
|
|
}
|
|
if !strings.Contains(e1.Message, "[抢购][success]") {
|
|
t.Errorf("entry1 Message = %q, want contains '[抢购][success]'", e1.Message)
|
|
}
|
|
if !strings.Contains(e1.Message, "兑换成功: 商品A") {
|
|
t.Errorf("entry1 Message = %q, want contains '兑换成功: 商品A'", e1.Message)
|
|
}
|
|
|
|
e2 := entries[2]
|
|
if e2.ID != 3 {
|
|
t.Errorf("entry2 ID = %d, want 3", e2.ID)
|
|
}
|
|
if e2.Account != "testuser" {
|
|
t.Errorf("entry2 Account = %q, want 'testuser'", e2.Account)
|
|
}
|
|
if !strings.Contains(e2.Message, "[账号:testuser]") {
|
|
t.Errorf("entry2 Message = %q, want contains '[账号:testuser]'", e2.Message)
|
|
}
|
|
if !strings.Contains(e2.Message, "冷却: 5s") {
|
|
t.Errorf("entry2 Message = %q, want contains '冷却: 5s'", e2.Message)
|
|
}
|
|
|
|
// 验证 minLevel 过滤:设为 error 后 info 日志不应进入内存
|
|
SetMinLevel("error")
|
|
LogEvent("测试", "info", "这条应被过滤")
|
|
if got := store.Since(e2.ID); len(got) != 0 {
|
|
t.Errorf("after SetMinLevel(error), Since(%d) = %d entries, want 0", e2.ID, len(got))
|
|
}
|
|
SetMinLevel("info") // 恢复,避免影响后续测试
|
|
}
|
|
|
|
// TestCreateUniqueLogFileAvoidsCollision 同一目录同秒启动的两个进程不能写同一个文件,
|
|
// 否则日志会交错、分不清实例。
|
|
func TestCreateUniqueLogFileAvoidsCollision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
base := filepath.Join(dir, "douyu_20261003_160856")
|
|
|
|
first, firstPath, err := createUniqueLogFile(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = first.Close()
|
|
if got := filepath.Base(firstPath); got != "douyu_20261003_160856.log" {
|
|
t.Fatalf("first = %q", got)
|
|
}
|
|
|
|
second, secondPath, err := createUniqueLogFile(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = second.Close()
|
|
if got := filepath.Base(secondPath); got != "douyu_20261003_160856-2.log" {
|
|
t.Fatalf("second = %q, want 同目录 -2 后缀", got)
|
|
}
|
|
}
|
|
|
|
// TestLogFileHeaderDocumentsFormat 日志文件开头必须说明双时间含义、实例名与数据目录,
|
|
// 否则把日志复制或汇总后看不出两个时间分别是什么、来自哪台机器的哪个实例。
|
|
func TestLogFileHeaderDocumentsFormat(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := NewMemoryLogStore(20)
|
|
cleanup := InitLogging(store, dir, "实例 2", `C:\Users\Administrator\AppData\Roaming\douyu-go\instances\instance-2`)
|
|
LogEvent("系统", "success", "已启动实例 2")
|
|
cleanup()
|
|
globalSink = nil
|
|
|
|
path := store.FilePath()
|
|
if path == "" {
|
|
t.Fatal("日志文件路径为空")
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := string(data)
|
|
for _, want := range []string{
|
|
"实例: 实例 2",
|
|
"版本: ",
|
|
"数据目录: ",
|
|
"时间格式: 本地时间(D斗鱼估算时间)",
|
|
"校准完成前偏移为 0",
|
|
"logLevel 改成 debug",
|
|
"已启动实例 2",
|
|
} {
|
|
if !strings.Contains(content, want) {
|
|
t.Errorf("日志文件缺少 %q\n实际内容:\n%s", want, content)
|
|
}
|
|
}
|
|
if !strings.HasPrefix(content, "====") {
|
|
t.Errorf("表头应在文件最开始:\n%s", content)
|
|
}
|
|
}
|