package logger import ( "os" "path/filepath" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func resetLogger() { closeState(globalLogger.Swap(nil)) } func TestParseLevel_AllLevels(t *testing.T) { tests := []struct { input string expected string // string form for comparison }{ {"debug", "debug"}, {"info", "info"}, {"warn", "warn"}, {"error", "error"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { level, err := parseLevel(tt.input) assert.NoError(t, err) assert.Equal(t, tt.expected, level.String()) }) } } func TestParseLevel_UnknownDefaultsToInfo(t *testing.T) { level, err := parseLevel("nonexistent") assert.NoError(t, err) assert.Equal(t, "info", level.String()) } func TestParseLevel_EmptyString(t *testing.T) { level, err := parseLevel("") assert.NoError(t, err) assert.Equal(t, "info", level.String()) } func TestGetOutput_Stdout(t *testing.T) { f, err := getOutput("stdout") assert.NoError(t, err) assert.Equal(t, os.Stdout, f) } func TestGetOutput_Stderr(t *testing.T) { f, err := getOutput("stderr") assert.NoError(t, err) assert.Equal(t, os.Stderr, f) } func TestGetOutput_File(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.log") f, err := getOutput(path) assert.NoError(t, err) require.NotNil(t, f) defer f.Close() // File should have been created _, statErr := os.Stat(path) assert.NoError(t, statErr) } func TestGetOutput_InvalidPath(t *testing.T) { // A path inside a nonexistent directory should fail _, err := getOutput("/nonexistent/dir/that/does/not/exist/log.txt") assert.Error(t, err) } func TestInit_JSONFormat(t *testing.T) { cfg := Config{ Level: "info", Format: "json", Output: "stdout", ErrorOutput: "stderr", } err := Init(cfg) assert.NoError(t, err) defer resetLogger() // globalLogger should be set require.NotNil(t, globalLogger.Load()) } func TestInit_ConsoleFormat(t *testing.T) { cfg := Config{ Level: "debug", Format: "console", Output: "stdout", ErrorOutput: "stderr", } err := Init(cfg) assert.NoError(t, err) defer resetLogger() require.NotNil(t, globalLogger.Load()) } func TestInit_FileOutput(t *testing.T) { dir := t.TempDir() logPath := filepath.Join(dir, "app.log") errPath := filepath.Join(dir, "err.log") cfg := Config{ Level: "warn", Format: "json", Output: logPath, ErrorOutput: errPath, } err := Init(cfg) assert.NoError(t, err) defer resetLogger() require.NotNil(t, globalLogger.Load()) // Write a log and sync L().Info("test message from file output") Sync() // The log file should exist _, statErr := os.Stat(logPath) assert.NoError(t, statErr) } func TestInit_ReplacesFileOutputsWithoutLeakingDescriptors(t *testing.T) { const fdDir = "/proc/self/fd" resetLogger() defer resetLogger() before, err := os.ReadDir(fdDir) if err != nil { t.Skip("open file descriptor inspection is unavailable") } dir := t.TempDir() cfg := Config{ Level: "info", Format: "json", Output: filepath.Join(dir, "app.log"), ErrorOutput: filepath.Join(dir, "err.log"), } var wg sync.WaitGroup for range 20 { wg.Add(1) go func() { defer wg.Done() _ = L() Sync() }() require.NoError(t, Init(cfg)) } wg.Wait() after, err := os.ReadDir(fdDir) require.NoError(t, err) require.LessOrEqual(t, len(after), len(before)+2) } func TestInit_InvalidLevel(t *testing.T) { // parseLevel never returns an error, so Init should succeed even with an // unknown level (defaults to info). This test verifies that behavior. cfg := Config{ Level: "totally-invalid-level", Format: "json", Output: "stdout", ErrorOutput: "stderr", } err := Init(cfg) assert.NoError(t, err) defer resetLogger() } func TestInit_InvalidOutput(t *testing.T) { cfg := Config{ Level: "info", Format: "json", Output: "/nonexistent/dir/that/does/not/exist/log.txt", ErrorOutput: "stderr", } err := Init(cfg) assert.Error(t, err) } func TestInit_InvalidErrorOutput(t *testing.T) { cfg := Config{ Level: "info", Format: "json", Output: "stdout", ErrorOutput: "/nonexistent/dir/that/does/not/exist/err.txt", } err := Init(cfg) assert.Error(t, err) } func TestL_WithoutInit(t *testing.T) { // Reset globalLogger to nil to test the fallback path resetLogger() defer resetLogger() l := L() assert.NotNil(t, l) // Should be a usable sugared logger l.Info("fallback logger works") } func TestL_AfterInit(t *testing.T) { cfg := Config{ Level: "info", Format: "json", Output: "stdout", ErrorOutput: "stderr", } err := Init(cfg) require.NoError(t, err) defer resetLogger() l := L() assert.NotNil(t, l) } func TestSync_WithLogger(t *testing.T) { cfg := Config{ Level: "info", Format: "json", Output: "stdout", ErrorOutput: "stderr", } err := Init(cfg) require.NoError(t, err) defer resetLogger() L().Info("message before sync") Sync() } func TestSync_WithoutLogger(t *testing.T) { resetLogger() // Should not panic Sync() } func TestL_LogsAtVariousLevels(t *testing.T) { cfg := Config{ Level: "debug", Format: "json", Output: "stdout", ErrorOutput: "stderr", } err := Init(cfg) require.NoError(t, err) defer resetLogger() l := L() l.Debug("debug message") l.Info("info message") l.Warn("warn message") l.Error("error message") l.Infof("formatted %s", "info") l.Errorf("formatted %s", "error") }