v0.1.8 tests converted to standard library from testify
This commit is contained in:
+162
-229
@@ -7,313 +7,246 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createTestLogger creates logger in temp directory
|
||||
func createTestLogger(t *testing.T) (*Logger, string) {
|
||||
tmpDir := t.TempDir()
|
||||
logger := NewLogger()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.EnableConsole = false
|
||||
cfg.EnableFile = true
|
||||
cfg.Directory = tmpDir
|
||||
cfg.BufferSize = 1000
|
||||
cfg.FlushIntervalMs = 10
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start the logger
|
||||
err = logger.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
return logger, tmpDir
|
||||
}
|
||||
|
||||
// TestNewLogger verifies that a new logger is created with the correct initial state
|
||||
// TestNewLogger verifies initial state of an unconfigured logger.
|
||||
func TestNewLogger(t *testing.T) {
|
||||
logger := NewLogger()
|
||||
|
||||
assert.NotNil(t, logger)
|
||||
assert.False(t, logger.state.IsInitialized.Load())
|
||||
assert.False(t, logger.state.LoggerDisabled.Load())
|
||||
isFalse(t, logger.state.IsInitialized.Load(), "IsInitialized")
|
||||
isFalse(t, logger.state.LoggerDisabled.Load(), "LoggerDisabled")
|
||||
isFalse(t, logger.state.Started.Load(), "Started")
|
||||
isTrue(t, logger.state.ProcessorExited.Load(), "ProcessorExited")
|
||||
|
||||
// A default formatter must exist to avoid nil dereference before ApplyConfig
|
||||
if logger.formatter.Load() == nil {
|
||||
t.Error("formatter not pre-initialized")
|
||||
}
|
||||
// Start before ApplyConfig must fail
|
||||
errContains(t, logger.Start(), "logger not initialized", "Start")
|
||||
}
|
||||
|
||||
// TestApplyConfig verifies that applying a valid configuration initializes the logger correctly
|
||||
// TestApplyConfig verifies initialization and log file creation.
|
||||
func TestApplyConfig(t *testing.T) {
|
||||
logger, tmpDir := createTestLogger(t)
|
||||
defer logger.Shutdown()
|
||||
logger, tmpDir := newTestLogger(t)
|
||||
|
||||
// Verify initialization
|
||||
assert.True(t, logger.state.IsInitialized.Load())
|
||||
|
||||
// Verify log file creation
|
||||
// The file now contains "Logger started"
|
||||
logPath := filepath.Join(tmpDir, "log.log")
|
||||
_, err := os.Stat(logPath)
|
||||
assert.NoError(t, err)
|
||||
isTrue(t, logger.state.IsInitialized.Load(), "IsInitialized")
|
||||
if _, err := os.Stat(filepath.Join(tmpDir, "log.log")); err != nil {
|
||||
t.Errorf("active log file missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyConfigString tests applying configuration overrides from key-value strings
|
||||
// TestApplyConfigRejection verifies invalid configs are rejected without mutating state.
|
||||
func TestApplyConfigRejection(t *testing.T) {
|
||||
logger, _ := newTestLogger(t)
|
||||
before := *logger.GetConfig()
|
||||
|
||||
errContains(t, logger.ApplyConfig(nil), "cannot be nil", "nil config")
|
||||
|
||||
bad := logger.GetConfig()
|
||||
bad.Format = "yaml"
|
||||
errContains(t, logger.ApplyConfig(bad), "invalid format", "invalid format")
|
||||
|
||||
mustEqual(t, *logger.GetConfig(), before, "config after rejected applies")
|
||||
}
|
||||
|
||||
// TestApplyConfigString covers key-value overrides, error paths, and rollback.
|
||||
func TestApplyConfigString(t *testing.T) {
|
||||
logger, _ := createTestLogger(t)
|
||||
defer logger.Shutdown()
|
||||
logger, _ := newTestLogger(t)
|
||||
// Dedicated directory target; never point a file-enabled logger at a shared path
|
||||
movedDir := filepath.Join(t.TempDir(), "moved")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configString []string
|
||||
verify func(t *testing.T, cfg *Config)
|
||||
wantError bool
|
||||
name string
|
||||
overrides []string
|
||||
wantErr string
|
||||
verify func(t *testing.T, cfg *Config)
|
||||
}{
|
||||
{
|
||||
name: "basic config string",
|
||||
configString: []string{
|
||||
"level=-4",
|
||||
"directory=/tmp/log",
|
||||
"format=json",
|
||||
},
|
||||
name: "numeric level and directory",
|
||||
overrides: []string{"level=-4", "directory=" + movedDir, "format=json"},
|
||||
verify: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, LevelDebug, cfg.Level)
|
||||
assert.Equal(t, "/tmp/log", cfg.Directory)
|
||||
assert.Equal(t, "json", cfg.Format)
|
||||
equal(t, cfg.Level, LevelDebug, "Level")
|
||||
equal(t, cfg.Directory, movedDir, "Directory")
|
||||
equal(t, cfg.Format, "json", "Format")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "level by name",
|
||||
configString: []string{"level=debug"},
|
||||
name: "named level",
|
||||
overrides: []string{"level=warn"},
|
||||
verify: func(t *testing.T, cfg *Config) { equal(t, cfg.Level, LevelWarn, "Level") },
|
||||
},
|
||||
{
|
||||
name: "boolean values",
|
||||
overrides: []string{"enable_console=true", "enable_file=true", "show_timestamp=false"},
|
||||
verify: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, LevelDebug, cfg.Level)
|
||||
isTrue(t, cfg.EnableConsole, "EnableConsole")
|
||||
isTrue(t, cfg.EnableFile, "EnableFile")
|
||||
isFalse(t, cfg.ShowTimestamp, "ShowTimestamp")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "boolean values",
|
||||
configString: []string{
|
||||
"enable_console=true",
|
||||
"enable_file=true",
|
||||
"show_timestamp=false",
|
||||
},
|
||||
name: "float and policy values",
|
||||
overrides: []string{"retention_period_hrs=1.5", "sanitization=txt"},
|
||||
verify: func(t *testing.T, cfg *Config) {
|
||||
assert.True(t, cfg.EnableConsole)
|
||||
assert.True(t, cfg.EnableFile)
|
||||
assert.False(t, cfg.ShowTimestamp)
|
||||
equal(t, cfg.RetentionPeriodHrs, 1.5, "RetentionPeriodHrs")
|
||||
equal(t, cfg.Sanitization, PolicyTxt, "Sanitization")
|
||||
},
|
||||
},
|
||||
{name: "missing separator", overrides: []string{"invalid"}, wantErr: "expected key=value"},
|
||||
{name: "empty key", overrides: []string{"=value"}, wantErr: "key cannot be empty"},
|
||||
{name: "unknown key", overrides: []string{"unknown_key=value"}, wantErr: "unknown configuration key"},
|
||||
{name: "bad integer", overrides: []string{"buffer_size=not_a_number"}, wantErr: "invalid integer value"},
|
||||
{name: "bad boolean", overrides: []string{"enable_file=yes-please"}, wantErr: "invalid boolean value"},
|
||||
{name: "bad level name", overrides: []string{"level=verbose"}, wantErr: "invalid level value"},
|
||||
// Field parse succeeds; rejection happens in Validate
|
||||
{name: "unvalidated policy", overrides: []string{"sanitization=bogus"}, wantErr: "invalid sanitization policy"},
|
||||
{
|
||||
name: "invalid format",
|
||||
configString: []string{"invalid"},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "unknown key",
|
||||
configString: []string{"unknown_key=value"},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid value type",
|
||||
configString: []string{"buffer_size=not_a_number"},
|
||||
wantError: true,
|
||||
name: "multiple errors combined",
|
||||
overrides: []string{"unknown_key=1", "buffer_size=x"},
|
||||
wantErr: "multiple configuration errors",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := logger.ApplyConfigString(tt.configString...)
|
||||
before := *logger.GetConfig()
|
||||
err := logger.ApplyConfigString(tt.overrides...)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
cfg := logger.GetConfig()
|
||||
tt.verify(t, cfg)
|
||||
if tt.wantErr != "" {
|
||||
errContains(t, err, tt.wantErr, "ApplyConfigString")
|
||||
equal(t, *logger.GetConfig(), before, "config must be unchanged on error")
|
||||
return
|
||||
}
|
||||
mustNoErr(t, err, "ApplyConfigString")
|
||||
tt.verify(t, logger.GetConfig())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerLoggingLevels checks that messages are correctly filtered based on the configured log level
|
||||
// TestLoggerLoggingLevels checks level-based filtering of emitted records.
|
||||
func TestLoggerLoggingLevels(t *testing.T) {
|
||||
logger, tmpDir := createTestLogger(t)
|
||||
defer logger.Shutdown()
|
||||
logger, tmpDir := newTestLogger(t)
|
||||
|
||||
// Log at different levels
|
||||
logger.Debug("debug message")
|
||||
logger.Info("info message")
|
||||
logger.Warn("warn message")
|
||||
logger.Error("error message")
|
||||
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
||||
|
||||
// Flush and verify
|
||||
err := logger.Flush(time.Second)
|
||||
require.NoError(t, err)
|
||||
// Writes are asynchronous; poll until all expected records land
|
||||
mustEventually(t, time.Second, "log records written", func() bool {
|
||||
c := readLog(t, tmpDir)
|
||||
return strings.Contains(c, "info message") &&
|
||||
strings.Contains(c, "warn message") &&
|
||||
strings.Contains(c, "error message")
|
||||
})
|
||||
|
||||
// Read log file
|
||||
var content []byte
|
||||
var fileContent string
|
||||
// Poll for a short period to wait for all async writes to complete.
|
||||
// This makes the test robust against scheduling variations.
|
||||
success := false
|
||||
for i := 0; i < 20; i++ {
|
||||
content, err = os.ReadFile(filepath.Join(tmpDir, "log.log"))
|
||||
require.NoError(t, err)
|
||||
fileContent = string(content)
|
||||
if strings.Contains(fileContent, "info message") &&
|
||||
strings.Contains(fileContent, "warn message") &&
|
||||
strings.Contains(fileContent, "error message") {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
require.True(t, success, "timed out waiting for all log messages to be written")
|
||||
|
||||
// Default level is INFO, so debug shouldn't appear
|
||||
assert.NotContains(t, string(content), "debug message")
|
||||
assert.Contains(t, string(content), "info message")
|
||||
assert.Contains(t, string(content), "warn message")
|
||||
assert.Contains(t, string(content), "error message")
|
||||
content := readLog(t, tmpDir)
|
||||
notContains(t, content, "debug message", "debug below configured level")
|
||||
}
|
||||
|
||||
// TestLoggerWithTrace ensures that logging with a stack trace does not cause a panic
|
||||
func TestLoggerWithTrace(t *testing.T) {
|
||||
logger, _ := createTestLogger(t)
|
||||
defer logger.Shutdown()
|
||||
// TestLoggerTraceDepth verifies trace emission is gated by depth without panicking.
|
||||
func TestLoggerTraceDepth(t *testing.T) {
|
||||
logger, tmpDir := newTestLogger(t)
|
||||
|
||||
cfg := logger.GetConfig()
|
||||
cfg.Level = LevelDebug
|
||||
logger.ApplyConfig(cfg)
|
||||
cfg.Format = "txt"
|
||||
cfg.ShowTimestamp = false
|
||||
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
||||
|
||||
logger.DebugTrace(2, "trace test")
|
||||
logger.Flush(time.Second)
|
||||
logger.Info("no trace here") // TraceDepth 0 -> no trace field
|
||||
logger.DebugTrace(2, "traced") // explicit depth -> trace present
|
||||
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
||||
|
||||
// Just verify it doesn't panic - trace content varies by runtime
|
||||
}
|
||||
mustEventually(t, time.Second, "traced record written", func() bool {
|
||||
return strings.Contains(readLog(t, tmpDir), "traced")
|
||||
})
|
||||
|
||||
// TestLoggerFormats verifies that the logger produces the correct output for different formats
|
||||
func TestLoggerFormats(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
check func(t *testing.T, content string)
|
||||
}{
|
||||
{
|
||||
name: "txt format",
|
||||
format: "txt",
|
||||
check: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, `INFO "test message"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json format",
|
||||
format: "json",
|
||||
check: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, `"level":"INFO"`)
|
||||
assert.Contains(t, content, `"fields":["test message"]`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "raw format",
|
||||
format: "raw",
|
||||
check: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, "test message")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logger := NewLogger()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = tmpDir
|
||||
cfg.Format = tt.format
|
||||
cfg.EnableFile = true
|
||||
cfg.ShowTimestamp = false // As in the original test
|
||||
cfg.ShowLevel = true // As in the original test
|
||||
// Set a fast flush interval for test reliability
|
||||
cfg.FlushIntervalMs = 10
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start the logger after configuring it
|
||||
err = logger.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer logger.Shutdown()
|
||||
|
||||
logger.Info("test message")
|
||||
|
||||
err = logger.Flush(time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "log.log"))
|
||||
require.NoError(t, err)
|
||||
|
||||
tt.check(t, string(content))
|
||||
})
|
||||
for _, line := range strings.Split(readLog(t, tmpDir), "\n") {
|
||||
if strings.Contains(line, "no trace here") && strings.Contains(line, "->") {
|
||||
t.Errorf("unexpected trace on zero-depth record: %s", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerConcurrency ensures the logger is safe for concurrent use from multiple goroutines
|
||||
// TestLoggerConcurrency exercises concurrent producers against a single processor.
|
||||
func TestLoggerConcurrency(t *testing.T) {
|
||||
logger, _ := createTestLogger(t)
|
||||
defer logger.Shutdown()
|
||||
logger, _ := newTestLogger(t)
|
||||
|
||||
const goroutines, perGoroutine = 10, 100
|
||||
var wg sync.WaitGroup
|
||||
for i := range 10 {
|
||||
for i := range goroutines {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := range 100 {
|
||||
for j := range perGoroutine {
|
||||
logger.Info("goroutine", i, "log", j)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
err := logger.Flush(time.Second)
|
||||
assert.NoError(t, err)
|
||||
noErr(t, logger.Flush(time.Second), "Flush")
|
||||
|
||||
// Upper bound only: processor-side write/rotation failures increment
|
||||
// DroppedLogs without TotalDroppedLogs, so an exact identity is unsafe
|
||||
processed := logger.state.TotalLogsProcessed.Load()
|
||||
dropped := logger.state.TotalDroppedLogs.Load()
|
||||
if total := uint64(goroutines * perGoroutine); processed+dropped > total {
|
||||
t.Errorf("counters exceed submitted records: processed=%d dropped=%d total=%d",
|
||||
processed, dropped, total)
|
||||
}
|
||||
if processed == 0 {
|
||||
t.Error("no records processed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerStdoutMirroring confirms that console output can be enabled without causing panics
|
||||
func TestLoggerStdoutMirroring(t *testing.T) {
|
||||
logger := NewLogger()
|
||||
// TestLoggerConsoleTargets verifies console-only operation for each target.
|
||||
func TestLoggerConsoleTargets(t *testing.T) {
|
||||
for _, target := range []string{"stdout", "stderr", "split"} {
|
||||
t.Run(target, func(t *testing.T) {
|
||||
logger := NewLogger()
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = t.TempDir()
|
||||
cfg.EnableConsole = true
|
||||
cfg.EnableFile = false
|
||||
cfg.ConsoleTarget = target
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = t.TempDir()
|
||||
cfg.EnableConsole = true
|
||||
cfg.EnableFile = false
|
||||
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
||||
mustNoErr(t, logger.Start(), "Start")
|
||||
t.Cleanup(func() { _ = logger.Shutdown() })
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
err = logger.Start()
|
||||
require.NoError(t, err)
|
||||
defer logger.Shutdown()
|
||||
|
||||
// Just verify it doesn't panic - actual stdout capture is complex
|
||||
logger.Info("stdout test")
|
||||
// split routes >=WARN to stderr; exercise both branches
|
||||
logger.Info("console info")
|
||||
logger.Error("console error")
|
||||
noErr(t, logger.Flush(time.Second), "Flush")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerWrite verifies that the Write method outputs raw, unformatted data
|
||||
// TestLoggerWrite verifies Write emits raw bytes with no formatting, framing, or sanitization.
|
||||
func TestLoggerWrite(t *testing.T) {
|
||||
logger, tmpDir := createTestLogger(t)
|
||||
defer logger.Shutdown()
|
||||
logger, tmpDir := newTestLogger(t)
|
||||
|
||||
// PolicyTxt would hex-encode control bytes; FlagRaw must bypass it
|
||||
cfg := logger.GetConfig()
|
||||
cfg.Sanitization = PolicyTxt
|
||||
cfg.Format = "txt"
|
||||
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
||||
|
||||
logger.Write("raw", "output", 123)
|
||||
logger.Write("\x1b[31m")
|
||||
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
||||
|
||||
logger.Flush(time.Second)
|
||||
mustEventually(t, time.Second, "raw record written", func() bool {
|
||||
return strings.Contains(readLog(t, tmpDir), "\x1b[31m")
|
||||
})
|
||||
|
||||
// Small delay for flush
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "log.log"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(content), "raw output 123")
|
||||
assert.True(t, strings.HasSuffix(string(content), "raw output 123"))
|
||||
content := readLog(t, tmpDir)
|
||||
contains(t, content, "raw output 123", "space-joined raw args")
|
||||
notContains(t, content, "<1b>", "sanitizer must be bypassed under FlagRaw")
|
||||
if strings.HasSuffix(content, "\n") {
|
||||
t.Error("Write must not append a trailing newline")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user