260 lines
7.8 KiB
Go
260 lines
7.8 KiB
Go
package logger
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||
|
||
"go.uber.org/zap"
|
||
"go.uber.org/zap/zapcore"
|
||
)
|
||
|
||
// tmpCfg 构造一个指向临时目录的日志配置,用于 Init。
|
||
func tmpCfg(dir string) *config.Config {
|
||
return &config.Config{
|
||
Server: config.ServerConfig{Mode: "production"},
|
||
Log: config.LogConfig{
|
||
Dir: dir,
|
||
MaxSize: 1,
|
||
MaxBackups: 1,
|
||
MaxAge: 1,
|
||
Compress: false,
|
||
},
|
||
}
|
||
}
|
||
|
||
// TestH7ConcurrentInitCloseAndRead 验证请求 goroutine 的包级日志读路径
|
||
// 与 Init/Close 重新装配全局 logger 无数据竞争(-race)。
|
||
//
|
||
// H7 根因:原实现 Info/Error/APILog/DBLog/Sync 裸读包级 Logger/sugar/apiLog/dbLog,
|
||
// 而 Init/Close 在 m.mu 下写这些变量——实例锁保护全局变量、读侧无锁 → re-Init/Close
|
||
// 与请求日志竞争。修复后读路径经 atomic.Pointer Load。
|
||
//
|
||
// 红/绿验证:将 currentLogger/currentSugar/currentAPILog/currentDBLog 临时改为
|
||
// 裸读 Logger/sugar/apiLog/dbLog(或等价地恢复包级裸读),-race 必复现 DATA RACE;
|
||
// 恢复 atomic 后绿。
|
||
func TestH7ConcurrentInitCloseAndRead(t *testing.T) {
|
||
dir := t.TempDir()
|
||
cfg := tmpCfg(dir)
|
||
|
||
// 确保 DefaultLogger 起点干净。
|
||
_ = DefaultLogger.Close()
|
||
t.Cleanup(func() { _ = DefaultLogger.Close() })
|
||
|
||
stop := make(chan struct{})
|
||
var wg sync.WaitGroup
|
||
|
||
// 写者:循环 Init/Close(重新装配全局 logger)。
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
for {
|
||
select {
|
||
case <-stop:
|
||
return
|
||
default:
|
||
}
|
||
_ = DefaultLogger.Init(cfg)
|
||
_ = DefaultLogger.Close()
|
||
}
|
||
}()
|
||
|
||
// 读者:并发调用读路径(包级函数 + atomic 快照读取)。
|
||
// 仅读取 logger 指针——这正是被竞态的访问;不调用 .Info/.Errorf 等写方法,
|
||
// 避免对已被 re-Init 关闭的旧 writer 触发 lumberjack 重新打开同一文件,
|
||
// 导致测试 TempDir 清理时句柄仍占用(框架不支持 re-Init 与在途写入并发,
|
||
// 该约束非 H7 范围)。
|
||
for i := 0; i < 4; i++ {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
for {
|
||
select {
|
||
case <-stop:
|
||
return
|
||
default:
|
||
}
|
||
// 这些调用内部经 atomic Load 读取包级 logger 指针。
|
||
_ = currentLogger()
|
||
_ = currentSugar()
|
||
_ = currentAPILog()
|
||
_ = currentDBLog()
|
||
_ = APILog()
|
||
_ = DBLog()
|
||
}
|
||
}()
|
||
}
|
||
|
||
// 运行窗口足以让 race detector 采到任何竞争。
|
||
time.Sleep(120 * time.Millisecond)
|
||
close(stop)
|
||
wg.Wait()
|
||
|
||
// 收尾到 Nop,避免后续测试持有临时目录句柄。
|
||
_ = DefaultLogger.Close()
|
||
}
|
||
|
||
// TestH7CurrentLoggerReflectsInitAndClose 验证 atomic 快照随 Init/Close 正确切换:
|
||
// Init 后 currentLogger 写入文件;Close 后回到 Nop(不写文件)。
|
||
func TestH7CurrentLoggerReflectsInitAndClose(t *testing.T) {
|
||
dir := t.TempDir()
|
||
cfg := tmpCfg(dir)
|
||
|
||
_ = DefaultLogger.Close()
|
||
t.Cleanup(func() { _ = DefaultLogger.Close() })
|
||
|
||
// Init 前:Nop,调用安全且不写文件。
|
||
before := currentLogger()
|
||
if before == nil {
|
||
t.Fatal("currentLogger nil before Init")
|
||
}
|
||
|
||
if err := DefaultLogger.Init(cfg); err != nil {
|
||
t.Fatalf("Init: %v", err)
|
||
}
|
||
|
||
// Init 后:currentLogger 与 Logger 兼容别名一致,且为非 Nop 实例。
|
||
after := currentLogger()
|
||
if after == nil {
|
||
t.Fatal("currentLogger nil after Init")
|
||
}
|
||
if after != Logger {
|
||
t.Error("after Init, currentLogger() != Logger (compat alias out of sync)")
|
||
}
|
||
|
||
// 写一条日志并 flush,验证落到文件(说明拿到的是真实 logger,非 Nop)。
|
||
const mark = "H7_INIT_REFLECT_xyz"
|
||
after.Info(mark)
|
||
_ = DefaultLogger.Sync()
|
||
|
||
data, err := readFile(t, filepath.Join(dir, "app.log"))
|
||
if err != nil {
|
||
t.Fatalf("read app.log: %v", err)
|
||
}
|
||
if !strings.Contains(data, mark) {
|
||
t.Errorf("app.log missing mark %q after Init (got Nop?)", mark)
|
||
}
|
||
|
||
// Close 后:currentLogger 回到 Nop(与 Init 前实例不同,但同为 Nop 行为)。
|
||
if err := DefaultLogger.Close(); err != nil {
|
||
t.Fatalf("Close: %v", err)
|
||
}
|
||
closed := currentLogger()
|
||
if closed == nil {
|
||
t.Fatal("currentLogger nil after Close")
|
||
}
|
||
// Nop logger 写不报错也不落盘;验证 Close 后再写不新增 mark。
|
||
closed.Info("H7_SHOULD_NOT_APPEAR_xyz")
|
||
_ = DefaultLogger.Sync()
|
||
data2, _ := readFile(t, filepath.Join(dir, "app.log"))
|
||
if strings.Contains(data2, "H7_SHOULD_NOT_APPEAR_xyz") {
|
||
t.Error("wrote to file after Close (expected Nop)")
|
||
}
|
||
}
|
||
|
||
// TestH7AtomicPointersNonNil 验证 init 后四个 atomic 快照永不 nil,
|
||
// 且 APILog/DBLog 返回与内部 atomic 一致的实例。
|
||
func TestH7AtomicPointersNonNil(t *testing.T) {
|
||
if loggerPtr.Load() == nil {
|
||
t.Error("loggerPtr nil after init")
|
||
}
|
||
if sugarPtr.Load() == nil {
|
||
t.Error("sugarPtr nil after init")
|
||
}
|
||
if apiLogPtr.Load() == nil {
|
||
t.Error("apiLogPtr nil after init")
|
||
}
|
||
if dbLogPtr.Load() == nil {
|
||
t.Error("dbLogPtr nil after init")
|
||
}
|
||
if APILog() != apiLogPtr.Load() {
|
||
t.Error("APILog() != apiLogPtr")
|
||
}
|
||
if DBLog() != dbLogPtr.Load() {
|
||
t.Error("DBLog() != dbLogPtr")
|
||
}
|
||
if currentLogger() != loggerPtr.Load() {
|
||
t.Error("currentLogger() != loggerPtr")
|
||
}
|
||
}
|
||
|
||
// TestH7DurationFieldFix 验证 H7b:Field.Duration 签名改为
|
||
// func(key string, value time.Duration) zap.Field,且 key 不再被丢弃。
|
||
//
|
||
// 旧实现 func(key string, value interface{}) 在 case zap.Field 分支 return v
|
||
// 丢弃 key,签名与实现矛盾。修复后直接委托 zap.Duration(key, value)。
|
||
func TestH7DurationFieldFix(t *testing.T) {
|
||
f := Field.Duration("elapsed", 5*time.Second)
|
||
if f.Key != "elapsed" {
|
||
t.Errorf("Duration key lost: got %q, want %q (H7b regression)", f.Key, "elapsed")
|
||
}
|
||
// zap.Duration 用 zapcore.DurationType 编码,Integer 字段承载纳秒。
|
||
if f.Integer != int64(5*time.Second) {
|
||
t.Errorf("Duration value mismatch: got %d, want %d", f.Integer, int64(5*time.Second))
|
||
}
|
||
|
||
// 零值与其他字段不受影响。
|
||
f2 := Field.Duration("d0", 0)
|
||
if f2.Key != "d0" {
|
||
t.Errorf("Duration zero key lost: %q", f2.Key)
|
||
}
|
||
}
|
||
|
||
// TestH7DurationFieldSignature 编译期锁定 Field.Duration 的签名为
|
||
// func(string, time.Duration) zap.Field(H7b 修复的核心:类型安全签名)。
|
||
//
|
||
// 旧签名为 func(string, interface{}) zap.Field,其 case zap.Field 分支 return v
|
||
// 丢弃传入的 key。若回退旧签名,下方赋值将因参数类型不匹配(interface{} ≠ time.Duration)
|
||
// 编译失败——即"修复前红、修复后绿"由编译器强制保证。
|
||
func TestH7DurationFieldSignature(t *testing.T) {
|
||
var _ func(string, time.Duration) zap.Field = Field.Duration
|
||
}
|
||
|
||
// readFile 读取文件内容,文件不存在时返回空串(不报错)。
|
||
func readFile(t *testing.T, path string) (string, error) {
|
||
t.Helper()
|
||
b, err := os.ReadFile(path)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return "", nil
|
||
}
|
||
return "", err
|
||
}
|
||
return string(b), nil
|
||
}
|
||
|
||
// TestSetLevelHotSwap_M19:Init 后可运行期热切换日志级别(M19)。
|
||
func TestSetLevelHotSwap_M19(t *testing.T) {
|
||
dir := t.TempDir()
|
||
cfg := tmpCfg(dir)
|
||
_ = DefaultLogger.Close()
|
||
t.Cleanup(func() { _ = DefaultLogger.Close() })
|
||
|
||
if err := DefaultLogger.Init(cfg); err != nil {
|
||
t.Fatalf("Init: %v", err)
|
||
}
|
||
|
||
// Init 后(tmpCfg 为 production 模式,默认 InfoLevel)可热切换到 ErrorLevel。
|
||
before := DefaultLogger.GetLevel()
|
||
if before != zapcore.InfoLevel {
|
||
t.Errorf("default level = %v, want InfoLevel (production)", before)
|
||
}
|
||
if ok := DefaultLogger.SetLevel(zapcore.ErrorLevel); !ok {
|
||
t.Fatal("SetLevel after Init should return true")
|
||
}
|
||
if got := DefaultLogger.GetLevel(); got != zapcore.ErrorLevel {
|
||
t.Errorf("after SetLevel, level = %v, want ErrorLevel", got)
|
||
}
|
||
|
||
// 包级 facade 同步。
|
||
SetLevel(zapcore.WarnLevel)
|
||
if got := GetLevel(); got != zapcore.WarnLevel {
|
||
t.Errorf("package GetLevel = %v, want WarnLevel", got)
|
||
}
|
||
}
|