package logs_test import ( "bytes" "context" "errors" "strings" "testing" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "github.com/stretchr/testify/assert" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // captureOutput 是一个辅助函数,用于捕获 logger 的输出到内存缓冲区 func captureOutput(cfg config.LogConfig) (*logs.Logger, *bytes.Buffer) { var buf bytes.Buffer // 使用一个简单的 Console Encoder 进行测试,方便断言字符串 encoderConfig := zap.NewDevelopmentEncoderConfig() encoderConfig.EncodeTime = nil // 忽略时间,避免测试结果不一致 encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder encoderConfig.EncodeCaller = nil // 忽略调用者信息 encoder := zapcore.NewConsoleEncoder(encoderConfig) writer := zapcore.AddSync(&buf) level := zap.NewAtomicLevel() _ = level.UnmarshalText([]byte(cfg.Level)) core := zapcore.NewCore(encoder, writer, level) // 在测试中我们直接操作 zap Logger,而不是通过封装的 NewLogger,以注入内存 writer zapLogger := zap.New(core) logger := &logs.Logger{SugaredLogger: zapLogger.Sugar()} return logger, &buf } func TestNewLogger(t *testing.T) { t.Run("构造函数不会 panic", func(t *testing.T) { // 测试 Console 格式 cfgConsole := config.LogConfig{Level: "info", Format: "console"} assert.NotPanics(t, func() { logs.NewLogger(cfgConsole) }) // 测试 JSON 格式 cfgJSON := config.LogConfig{Level: "info", Format: "json"} assert.NotPanics(t, func() { logs.NewLogger(cfgJSON) }) // 测试文件日志启用 // 不实际写入文件,只确保构造函数能正常运行 cfgFile := config.LogConfig{ Level: "info", EnableFile: true, FilePath: "test.log", } assert.NotPanics(t, func() { logs.NewLogger(cfgFile) }) }) } func TestLogger_Write_ForGin(t *testing.T) { logger, buf := captureOutput(config.LogConfig{Level: "info"}) ginLog := "[GIN-debug] Listening and serving HTTP on :8080\n" _, err := logger.Write([]byte(ginLog)) assert.NoError(t, err) output := buf.String() // logger.Write 会将 gin 的日志转为 info 级别 assert.Contains(t, output, "INFO") assert.Contains(t, output, strings.TrimSpace(ginLog)) } func TestGormLogger(t *testing.T) { logger, buf := captureOutput(config.LogConfig{Level: "debug"}) // 设置为 debug 以捕获所有级别 gormLogger := logs.NewGormLogger(logger) // 模拟 GORM 的 Trace 调用参数 ctx := context.Background() sql := "SELECT * FROM users WHERE id = 1" rows := int64(1) fc := func() (string, int64) { return sql, rows } t.Run("Slow Query", func(t *testing.T) { buf.Reset() // 模拟一个耗时超过 200ms 的查询 begin := time.Now().Add(-300 * time.Millisecond) gormLogger.Trace(ctx, begin, fc, nil) output := buf.String() assert.Contains(t, output, "WARN", "应包含 WARN 级别") assert.Contains(t, output, "[GORM] slow query", "应包含慢查询信息") assert.Contains(t, output, "SELECT * FROM users WHERE id = 1", "应包含 SQL 语句") }) t.Run("Error Query", func(t *testing.T) { buf.Reset() queryError := errors.New("syntax error") gormLogger.Trace(ctx, time.Now(), fc, queryError) output := buf.String() assert.Contains(t, output, "ERROR") assert.Contains(t, output, "[GORM] error: syntax error") }) t.Run("Record Not Found Error is Skipped", func(t *testing.T) { buf.Reset() // 错误必须包含 "record not found" 字符串以匹配 logs.go 中的判断逻辑 queryError := errors.New("record not found") gormLogger.Trace(ctx, time.Now(), fc, queryError) assert.Empty(t, buf.String(), "开启 SkipErrRecordNotFound 后,record not found 错误不应产生任何日志") }) t.Run("Normal Query", func(t *testing.T) { buf.Reset() // 模拟一个快速查询 gormLogger.Trace(ctx, time.Now(), fc, nil) output := buf.String() assert.Contains(t, output, "DEBUG") // 正常查询是 Debug 级别 assert.Contains(t, output, "[GORM] trace") }) }