Files
2026-08-15 00:04:01 +08:00

270 lines
5.5 KiB
Go

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")
}