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