Initial commit

This commit is contained in:
eleksir
2026-04-02 23:56:28 +03:00
commit 9e30243533
34 changed files with 8413 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
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()
}