Files
DevPack/internal/logging/logger_test.go
2026-03-04 14:19:31 +08:00

492 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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())
}
})
}
}