diff --git a/internal/logging/logger_test.go b/internal/logging/logger_test.go index 2b43acc..5b5e1b9 100644 --- a/internal/logging/logger_test.go +++ b/internal/logging/logger_test.go @@ -1 +1,491 @@ 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()) + } + }) + } +}