package logging import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "github.com/rs/zerolog" ) // ==================== New / Init 构造测试 ==================== func TestNew_DefaultOptions(t *testing.T) { logger, err := New(Options{}) if err != nil { t.Fatalf("New() 默认选项不应报错, got: %v", err) } if logger == nil { t.Fatal("New() 返回的 Logger 不应为 nil") } // 默认级别应为 Info if logger.level != zerolog.InfoLevel { t.Errorf("默认级别应为 Info, got: %s", logger.level) } if logger.quiet { t.Error("默认不应为静默模式") } if logger.verbose { t.Error("默认不应为详细模式") } if logger.logFile != nil { t.Error("默认不应有日志文件") } } func TestNew_WithLevel(t *testing.T) { tests := []struct { level string expected zerolog.Level }{ {"trace", zerolog.TraceLevel}, {"debug", zerolog.DebugLevel}, {"info", zerolog.InfoLevel}, {"warn", zerolog.WarnLevel}, {"error", zerolog.ErrorLevel}, {"fatal", zerolog.FatalLevel}, } for _, tt := range tests { t.Run(tt.level, func(t *testing.T) { logger, err := New(Options{Level: tt.level, Quiet: true}) if err != nil { t.Fatalf("New(Level=%s) 不应报错, got: %v", tt.level, err) } if logger.level != tt.expected { t.Errorf("级别应为 %s, got: %s", tt.expected, logger.level) } }) } } func TestNew_InvalidLevel(t *testing.T) { _, err := New(Options{Level: "invalid_level"}) if err == nil { t.Fatal("无效日志级别应返回错误") } if !strings.Contains(err.Error(), "无效的日志级别") { t.Errorf("错误消息应包含 '无效的日志级别', got: %s", err.Error()) } } func TestNew_VerboseOverridesLevel(t *testing.T) { logger, err := New(Options{Level: "error", Verbose: true, Quiet: true}) if err != nil { t.Fatalf("不应报错, got: %v", err) } // Verbose 应覆盖 Level,强制设为 Debug if logger.level != zerolog.DebugLevel { t.Errorf("Verbose=true 时级别应为 Debug, got: %s", logger.level) } if !logger.verbose { t.Error("verbose 标志应为 true") } } func TestNew_QuietMode(t *testing.T) { logger, err := New(Options{Quiet: true}) if err != nil { t.Fatalf("不应报错, got: %v", err) } if !logger.quiet { t.Error("quiet 标志应为 true") } } func TestNew_WithLogFile(t *testing.T) { tmpDir := t.TempDir() logPath := filepath.Join(tmpDir, "test.log") logger, err := New(Options{LogFile: logPath, Quiet: true}) if err != nil { t.Fatalf("不应报错, got: %v", err) } defer logger.Close() if logger.logFile == nil { t.Fatal("logFile 不应为 nil") } // 验证文件已创建 if _, err := os.Stat(logPath); os.IsNotExist(err) { t.Error("日志文件应已被创建") } } func TestNew_LogFileCreatesDir(t *testing.T) { tmpDir := t.TempDir() logPath := filepath.Join(tmpDir, "subdir", "nested", "test.log") logger, err := New(Options{LogFile: logPath, Quiet: true}) if err != nil { t.Fatalf("不应报错, got: %v", err) } defer logger.Close() // 验证嵌套目录已创建 dirPath := filepath.Dir(logPath) if _, err := os.Stat(dirPath); os.IsNotExist(err) { t.Error("日志目录应已被自动创建") } } // ==================== Init / G 全局实例测试 ==================== func TestInit_SetsGlobal(t *testing.T) { // 保存并恢复原始全局实例 original := global defer func() { global = original }() logger, err := Init(Options{Quiet: true}) if err != nil { t.Fatalf("Init() 不应报错, got: %v", err) } if G() != logger { t.Error("G() 应返回 Init() 设置的全局实例") } } func TestG_DefaultSafe(t *testing.T) { // 即使未初始化,G() 也应返回安全的 nop logger original := global defer func() { global = original }() global = &Logger{zl: zerolog.Nop()} log := G() if log == nil { t.Fatal("G() 不应返回 nil") } // 调用方法不应 panic log.Info("安全调用测试") log.Debugf("格式化 %s", "测试") } // ==================== 日志输出测试 ==================== // newBufferLogger 创建一个写入 buffer 的 Logger,用于验证输出 func newBufferLogger(level zerolog.Level) (*Logger, *bytes.Buffer) { buf := &bytes.Buffer{} zl := zerolog.New(buf).Level(level).With().Timestamp().Logger() return &Logger{zl: zl, level: level}, buf } func TestLogger_InfoOutput(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Info("测试消息") output := buf.String() if !strings.Contains(output, "测试消息") { t.Errorf("输出应包含日志消息, got: %s", output) } if !strings.Contains(output, `"level":"info"`) { t.Errorf("输出应包含 info 级别, got: %s", output) } } func TestLogger_Infof(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Infof("你好 %s, 编号 %d", "世界", 42) output := buf.String() if !strings.Contains(output, "你好 世界, 编号 42") { t.Errorf("格式化输出不正确, got: %s", output) } } func TestLogger_DebugFilteredByLevel(t *testing.T) { // Info 级别的 Logger 应过滤 Debug 消息 logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Debug("这条不应出现") if buf.Len() > 0 { t.Errorf("Info 级别不应输出 Debug 消息, got: %s", buf.String()) } } func TestLogger_DebugVisibleAtDebugLevel(t *testing.T) { logger, buf := newBufferLogger(zerolog.DebugLevel) logger.Debug("调试信息") if !strings.Contains(buf.String(), "调试信息") { t.Errorf("Debug 级别应输出 Debug 消息, got: %s", buf.String()) } } func TestLogger_TraceFilteredByInfo(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Trace("跟踪信息") if buf.Len() > 0 { t.Errorf("Info 级别不应输出 Trace 消息, got: %s", buf.String()) } } func TestLogger_WarnOutput(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Warn("警告信息") output := buf.String() if !strings.Contains(output, "警告信息") { t.Errorf("输出应包含警告消息, got: %s", output) } if !strings.Contains(output, `"level":"warn"`) { t.Errorf("输出应包含 warn 级别, got: %s", output) } } func TestLogger_ErrorOutput(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Error("错误信息") output := buf.String() if !strings.Contains(output, "错误信息") { t.Errorf("输出应包含错误消息, got: %s", output) } if !strings.Contains(output, `"level":"error"`) { t.Errorf("输出应包含 error 级别, got: %s", output) } } func TestLogger_Errorf(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Errorf("文件 %s 失败: %d", "test.txt", 404) if !strings.Contains(buf.String(), "文件 test.txt 失败: 404") { t.Errorf("格式化错误输出不正确, got: %s", buf.String()) } } func TestLogger_Warnf(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.Warnf("版本 %s 已废弃", "v0.1.0") if !strings.Contains(buf.String(), "版本 v0.1.0 已废弃") { t.Errorf("格式化警告输出不正确, got: %s", buf.String()) } } func TestLogger_Tracef(t *testing.T) { logger, buf := newBufferLogger(zerolog.TraceLevel) logger.Tracef("进入函数 %s", "Scan") if !strings.Contains(buf.String(), "进入函数 Scan") { t.Errorf("格式化跟踪输出不正确, got: %s", buf.String()) } } func TestLogger_Debugf(t *testing.T) { logger, buf := newBufferLogger(zerolog.DebugLevel) logger.Debugf("变量值: %v", map[string]int{"a": 1}) if !strings.Contains(buf.String(), "变量值:") { t.Errorf("格式化调试输出不正确, got: %s", buf.String()) } } // ==================== 结构化字段测试 ==================== func TestLogger_WithField(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.WithField("collector", "go").Msg("扫描完成") output := buf.String() if !strings.Contains(output, "collector") { t.Errorf("输出应包含字段名 collector, got: %s", output) } if !strings.Contains(output, "扫描完成") { t.Errorf("输出应包含消息, got: %s", output) } } func TestLogger_WithError(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) testErr := os.ErrNotExist logger.WithError(testErr).Msg("操作失败") output := buf.String() if !strings.Contains(output, "error") { t.Errorf("输出应包含 error 字段, got: %s", output) } if !strings.Contains(output, "操作失败") { t.Errorf("输出应包含消息, got: %s", output) } } func TestLogger_WithDuration(t *testing.T) { logger, buf := newBufferLogger(zerolog.InfoLevel) logger.WithDuration(3 * time.Second).Msg("处理完成") output := buf.String() if !strings.Contains(output, "duration") { t.Errorf("输出应包含 duration 字段, got: %s", output) } if !strings.Contains(output, "处理完成") { t.Errorf("输出应包含消息, got: %s", output) } } // ==================== 工具方法测试 ==================== func TestLogger_IsDebug(t *testing.T) { tests := []struct { name string level zerolog.Level expected bool }{ {"TraceLevel", zerolog.TraceLevel, true}, {"DebugLevel", zerolog.DebugLevel, true}, {"InfoLevel", zerolog.InfoLevel, false}, {"WarnLevel", zerolog.WarnLevel, false}, {"ErrorLevel", zerolog.ErrorLevel, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger := &Logger{level: tt.level, zl: zerolog.Nop()} if logger.IsDebug() != tt.expected { t.Errorf("IsDebug() 应为 %v (level=%s)", tt.expected, tt.level) } }) } } func TestLogger_IsQuiet(t *testing.T) { logger := &Logger{quiet: true, zl: zerolog.Nop()} if !logger.IsQuiet() { t.Error("IsQuiet() 应返回 true") } logger2 := &Logger{quiet: false, zl: zerolog.Nop()} if logger2.IsQuiet() { t.Error("IsQuiet() 应返回 false") } } func TestLogger_IsVerbose(t *testing.T) { logger := &Logger{verbose: true, zl: zerolog.Nop()} if !logger.IsVerbose() { t.Error("IsVerbose() 应返回 true") } logger2 := &Logger{verbose: false, zl: zerolog.Nop()} if logger2.IsVerbose() { t.Error("IsVerbose() 应返回 false") } } // ==================== Close 测试 ==================== func TestLogger_Close_WithFile(t *testing.T) { tmpDir := t.TempDir() logPath := filepath.Join(tmpDir, "close_test.log") logger, err := New(Options{LogFile: logPath, Quiet: true}) if err != nil { t.Fatalf("不应报错, got: %v", err) } // 写一条日志确保文件可用 logger.Info("关闭前的日志") // Close 不应 panic logger.Close() // 关闭后文件不可写(已关闭) _, writeErr := logger.logFile.WriteString("test") if writeErr == nil { t.Error("文件关闭后写入应报错") } } func TestLogger_Close_WithoutFile(t *testing.T) { logger, _ := New(Options{Quiet: true}) // 没有日志文件时 Close 不应 panic logger.Close() } // ==================== 日志文件写入测试 ==================== func TestLogger_FileOutput(t *testing.T) { tmpDir := t.TempDir() logPath := filepath.Join(tmpDir, "output_test.log") logger, err := New(Options{LogFile: logPath, Quiet: true}) if err != nil { t.Fatalf("不应报错, got: %v", err) } logger.Info("写入文件的消息") logger.Warn("警告消息") logger.Close() // 读取文件内容 content, err := os.ReadFile(logPath) if err != nil { t.Fatalf("读取日志文件失败: %v", err) } lines := strings.Split(strings.TrimSpace(string(content)), "\n") if len(lines) < 2 { t.Fatalf("日志文件应至少有 2 行, got %d 行: %s", len(lines), string(content)) } // 验证是合法的 JSON 格式(文件输出为 JSON) for i, line := range lines { var entry map[string]any if err := json.Unmarshal([]byte(line), &entry); err != nil { t.Errorf("第 %d 行不是合法 JSON: %s", i+1, line) } } } // ==================== 级别过滤综合测试 ==================== func TestLogger_LevelFiltering(t *testing.T) { tests := []struct { name string logLevel zerolog.Level writeFunc func(l *Logger) shouldLog bool }{ {"Error级别写Info", zerolog.ErrorLevel, func(l *Logger) { l.Info("msg") }, false}, {"Error级别写Error", zerolog.ErrorLevel, func(l *Logger) { l.Error("msg") }, true}, {"Error级别写Warn", zerolog.ErrorLevel, func(l *Logger) { l.Warn("msg") }, false}, {"Warn级别写Warn", zerolog.WarnLevel, func(l *Logger) { l.Warn("msg") }, true}, {"Warn级别写Info", zerolog.WarnLevel, func(l *Logger) { l.Info("msg") }, false}, {"Debug级别写Trace", zerolog.DebugLevel, func(l *Logger) { l.Trace("msg") }, false}, {"Trace级别写Trace", zerolog.TraceLevel, func(l *Logger) { l.Trace("msg") }, true}, {"Info级别写Info", zerolog.InfoLevel, func(l *Logger) { l.Info("msg") }, true}, {"Info级别写Error", zerolog.InfoLevel, func(l *Logger) { l.Error("msg") }, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger, buf := newBufferLogger(tt.logLevel) tt.writeFunc(logger) hasOutput := buf.Len() > 0 if hasOutput != tt.shouldLog { t.Errorf("shouldLog=%v, but hasOutput=%v (output: %s)", tt.shouldLog, hasOutput, buf.String()) } }) } }