package logger import ( "log" "os" "path/filepath" "strings" "testing" ) // resetLogger restores package-level state between tests. func resetLogger() { currentLevel = LevelInfo loggerMu.Lock() if logFile != nil { _ = logFile.Close() } logFile = nil logPath = "" logger = log.New(os.Stdout, "", 0) loggerMu.Unlock() } func TestSetLevel(t *testing.T) { tests := []struct { input string want Level }{ {"debug", LevelDebug}, {"info", LevelInfo}, {"warn", LevelWarn}, {"error", LevelError}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { resetLogger() SetLevel(tt.input) if currentLevel != tt.want { t.Errorf("SetLevel(%q): got %d, want %d", tt.input, currentLevel, tt.want) } }) } } func TestSetLevelInvalid(t *testing.T) { resetLogger() SetLevel("info") SetLevel("bogus") if currentLevel != LevelInfo { t.Errorf("invalid level changed current level: got %d, want %d", currentLevel, LevelInfo) } } func TestSetLevelCaseSensitive(t *testing.T) { resetLogger() SetLevel("INFO") // uppercase should not match if currentLevel != LevelInfo { t.Errorf("uppercase level should not change level: got %d, want %d", currentLevel, LevelInfo) } } func TestSetOutputToFile(t *testing.T) { resetLogger() dir := t.TempDir() path := filepath.Join(dir, "test.log") if err := SetOutput(path); err != nil { t.Fatalf("SetOutput(%q): %v", path, err) } if logPath != path { t.Errorf("logPath: got %q, want %q", logPath, path) } if logFile == nil { t.Error("logFile should not be nil after SetOutput") } // Write something and verify it appears in the file Info("test message") if err := logFile.Sync(); err != nil { t.Fatalf("sync: %v", err) } //nolint: gosec // test reads temp file in temp dir data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile: %v", err) } if !strings.Contains(string(data), "test message") { t.Errorf("log file should contain 'test message', got: %s", string(data)) } // Close file before TempDir cleanup resetLogger() } func TestSetOutputEmptyResetsToStdout(t *testing.T) { resetLogger() dir := t.TempDir() path := filepath.Join(dir, "test.log") if err := SetOutput(path); err != nil { t.Fatalf("SetOutput: %v", err) } if err := SetOutput(""); err != nil { t.Fatalf("SetOutput(\"\"): %v", err) } if logPath != "" { t.Errorf("logPath should be empty, got %q", logPath) } if logFile != nil { t.Error("logFile should be nil after reset to stdout") } } func TestSetOutputInvalidPath(t *testing.T) { resetLogger() err := SetOutput("/nonexistent/dir/file.log") if err == nil { t.Error("SetOutput with invalid path should return error") } } func TestReopenNoFile(t *testing.T) { resetLogger() // Reopen when logging to stdout should be a no-op if err := Reopen(); err != nil { t.Errorf("Reopen with no file should not error: %v", err) } } func TestReopen(t *testing.T) { resetLogger() dir := t.TempDir() path := filepath.Join(dir, "test.log") if err := SetOutput(path); err != nil { t.Fatalf("SetOutput: %v", err) } // Write initial message Info("before reopen") _ = logFile.Sync() // Close and remove the file to simulate log rotation _ = logFile.Close() _ = os.Remove(path) // Reopen should recreate the file if err := Reopen(); err != nil { t.Fatalf("Reopen: %v", err) } if logFile == nil { t.Error("logFile should not be nil after reopen") } // Write after reopen Info("after reopen") _ = logFile.Sync() //nolint: gosec // test reads temp file in temp dir data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile: %v", err) } content := string(data) if !strings.Contains(content, "after reopen") { t.Errorf("log should contain 'after reopen', got: %s", content) } if strings.Contains(content, "before reopen") { t.Error("log should not contain 'before reopen' after file was removed") } // Close file before TempDir cleanup resetLogger() } func TestFormatLog(t *testing.T) { // Force PID init mu.Do(initPID) msg := formatLog("INFO", "test key=value") // Verify format: timestamp level pekoe[pid]: message if !strings.Contains(msg, "INFO pekoe[") { t.Errorf("expected 'INFO pekoe[', got: %s", msg) } if !strings.Contains(msg, "]: test key=value") { t.Errorf("expected message content, got: %s", msg) } } func TestFormatLogLevels(t *testing.T) { mu.Do(initPID) levels := []string{"DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"} for _, level := range levels { msg := formatLog(level, "x") if !strings.Contains(msg, level+" pekoe[") { t.Errorf("level %s: expected '%s pekoe[', got: %s", level, level, msg) } } } func TestLevelFiltering(t *testing.T) { tests := []struct { level string logFunc func(string, ...interface{}) name string want bool // whether message should appear }{ // At debug level, all messages should appear {"debug", Debug, "debug_at_debug", true}, {"debug", Info, "info_at_debug", true}, {"debug", Warn, "warn_at_debug", true}, {"debug", Error, "error_at_debug", true}, // At info level, debug should be filtered {"info", Debug, "debug_at_info", false}, {"info", Info, "info_at_info", true}, {"info", Warn, "warn_at_info", true}, {"info", Error, "error_at_info", true}, // At warn level, debug and info should be filtered {"warn", Debug, "debug_at_warn", false}, {"warn", Info, "info_at_warn", false}, {"warn", Warn, "warn_at_warn", true}, {"warn", Error, "error_at_warn", true}, // At error level, only error should appear {"error", Debug, "debug_at_error", false}, {"error", Info, "info_at_error", false}, {"error", Warn, "warn_at_error", false}, {"error", Error, "error_at_error", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resetLogger() dir := t.TempDir() path := filepath.Join(dir, "filter.log") if err := SetOutput(path); err != nil { t.Fatalf("SetOutput: %v", err) } SetLevel(tt.level) tt.logFunc(tt.name) _ = logFile.Sync() //nolint: gosec // test reads temp file in temp dir data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile: %v", err) } content := string(data) contains := strings.Contains(content, tt.name) if tt.want && !contains { t.Errorf("level=%s: expected %s to appear in log, got: %s", tt.level, tt.name, content) } if !tt.want && contains { t.Errorf("level=%s: expected %s to be filtered, but it appeared in log", tt.level, tt.name) } // Close file before TempDir cleanup resetLogger() }) } } func TestLogFormatIncludesKeyValues(t *testing.T) { resetLogger() dir := t.TempDir() path := filepath.Join(dir, "format.log") if err := SetOutput(path); err != nil { t.Fatalf("SetOutput: %v", err) } SetLevel("debug") Info("event=test key=value count=%d", 42) _ = logFile.Sync() //nolint: gosec // test reads temp file in temp dir data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile: %v", err) } content := string(data) // Should contain the formatted message if !strings.Contains(content, "event=test") { t.Errorf("expected 'event=test' in log, got: %s", content) } if !strings.Contains(content, "key=value") { t.Errorf("expected 'key=value' in log, got: %s", content) } if !strings.Contains(content, "count=42") { t.Errorf("expected 'count=42' in log, got: %s", content) } // Close file before TempDir cleanup resetLogger() } func TestMultipleSetOutputSwitches(t *testing.T) { resetLogger() dir := t.TempDir() path1 := filepath.Join(dir, "first.log") path2 := filepath.Join(dir, "second.log") // Write to first file if err := SetOutput(path1); err != nil { t.Fatalf("SetOutput path1: %v", err) } SetLevel("info") Info("first file") _ = logFile.Sync() // Switch to second file (SetOutput closes the first file) if err := SetOutput(path2); err != nil { t.Fatalf("SetOutput path2: %v", err) } Info("second file") _ = logFile.Sync() // Verify first file has only its message //nolint: gosec // test reads temp files in temp dir data1, err := os.ReadFile(path1) if err != nil { t.Fatalf("ReadFile path1: %v", err) } if !strings.Contains(string(data1), "first file") { t.Error("first log should contain 'first file'") } if strings.Contains(string(data1), "second file") { t.Error("first log should not contain 'second file'") } // Verify second file has only its message //nolint: gosec // test reads temp files in temp dir data2, err := os.ReadFile(path2) if err != nil { t.Fatalf("ReadFile path2: %v", err) } if !strings.Contains(string(data2), "second file") { t.Error("second log should contain 'second file'") } if strings.Contains(string(data2), "first file") { t.Error("second log should not contain 'first file'") } // Close file before TempDir cleanup resetLogger() }