Pulse/internal/logging/logging_test.go
2026-03-18 16:06:30 +00:00

1183 lines
29 KiB
Go

package logging
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/term"
)
func resetLoggingState() {
mu.Lock()
defer mu.Unlock()
// Always use the real close function during test cleanup so active writers
// can be closed even when a test has stubbed closeFileFn.
closeFileFn = defaultCloseFileFn
if fileCloser != nil {
_ = fileCloser.Close()
fileCloser = nil
}
baseWriter = os.Stderr
baseComponent = ""
baseLogger = zerolog.New(baseWriter).With().Timestamp().Logger()
log.Logger = baseLogger
zerolog.TimeFieldFormat = defaultTimeFmt
zerolog.SetGlobalLevel(zerolog.InfoLevel)
nowFn = time.Now
isTerminalFn = term.IsTerminal
mkdirAllFn = os.MkdirAll
chmodFn = os.Chmod
openFileFn = os.OpenFile
openFn = os.Open
statFn = os.Stat
lstatFn = os.Lstat
readDirFn = os.ReadDir
renameFn = os.Rename
removeFn = os.Remove
copyFn = io.Copy
gzipNewWriterFn = gzip.NewWriter
statFileFn = defaultStatFileFn
chmodFileFn = func(file *os.File, mode os.FileMode) error { return file.Chmod(mode) }
closeFileFn = defaultCloseFileFn
compressFn = compressAndRemove
GetBroadcaster().Shutdown()
}
func baseWriterDebugString() string {
return fmt.Sprintf("%#v", baseWriter)
}
func captureStderr(t *testing.T, fn func()) string {
t.Helper()
originalStderr := os.Stderr
readPipe, writePipe, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create stderr pipe: %v", err)
}
os.Stderr = writePipe
defer func() {
os.Stderr = originalStderr
_ = readPipe.Close()
_ = writePipe.Close()
}()
fn()
if err := writePipe.Close(); err != nil {
t.Fatalf("failed to close write pipe: %v", err)
}
output, err := io.ReadAll(readPipe)
if err != nil {
t.Fatalf("failed to read stderr output: %v", err)
}
return string(output)
}
func TestInitJSONFormatSetsLevelAndComponent(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "debug",
Component: "apiserver",
})
mu.RLock()
defer mu.RUnlock()
repr := baseWriterDebugString()
if !strings.Contains(repr, fmt.Sprintf("(%p)", os.Stderr)) {
t.Fatalf("expected base writer to include os.Stderr, got %#v", baseWriter)
}
if !strings.Contains(repr, "LogBroadcaster") {
t.Fatalf("expected base writer to include broadcaster, got %#v", baseWriter)
}
if zerolog.GlobalLevel() != zerolog.DebugLevel {
t.Fatalf("expected global level debug, got %s", zerolog.GlobalLevel())
}
if baseComponent != "apiserver" {
t.Fatalf("expected base component apiserver, got %s", baseComponent)
}
if !reflect.DeepEqual(log.Logger, baseLogger) {
t.Fatal("expected global log.Logger to match baseLogger")
}
}
func TestInitConsoleFormatUsesConsoleWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "console",
Level: "info",
})
mu.RLock()
defer mu.RUnlock()
repr := baseWriterDebugString()
if !strings.Contains(repr, "zerolog.ConsoleWriter") {
t.Fatalf("expected console writer, got %#v", baseWriter)
}
if !strings.Contains(repr, "LogBroadcaster") {
t.Fatalf("expected base writer to include broadcaster, got %#v", baseWriter)
}
}
func TestInitAutoFormatWithPipe(t *testing.T) {
t.Cleanup(resetLoggingState)
origStderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
os.Stderr = w
defer func() {
os.Stderr = origStderr
_ = r.Close()
_ = w.Close()
}()
Init(Config{
Format: "auto",
Level: "info",
})
mu.RLock()
defer mu.RUnlock()
repr := baseWriterDebugString()
if !strings.Contains(repr, fmt.Sprintf("(%p)", w)) {
t.Fatalf("expected base writer to use provided pipe, got %#v", baseWriter)
}
if !strings.Contains(repr, "LogBroadcaster") {
t.Fatalf("expected base writer to include broadcaster, got %#v", baseWriter)
}
}
func TestWithRequestID(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
ctx, generated := WithRequestID(nil, "")
if generated == "" {
t.Fatal("expected generated request id")
}
if ctx == nil {
t.Fatal("expected non-nil context")
}
ctx2, id := WithRequestID(nil, "custom-123")
if id != "custom-123" {
t.Fatalf("expected custom-123, got %s", id)
}
if ctx2 == nil {
t.Fatal("expected non-nil context")
}
}
func TestWithRequestIDTrimsWhitespace(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{})
_, id := WithRequestID(nil, " ")
if id == "" {
t.Fatal("expected generated id for whitespace input")
}
}
func TestInitThreadSafety(t *testing.T) {
t.Cleanup(resetLoggingState)
var wg sync.WaitGroup
configs := []Config{
{Format: "json", Level: "debug", Component: "worker"},
{Format: "json", Level: "warn", Component: "api"},
}
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
Init(configs[idx%len(configs)])
}(i)
}
wg.Wait()
mu.RLock()
defer mu.RUnlock()
// Ensure baseLogger is valid and global logger matches it.
if reflect.DeepEqual(baseLogger, zerolog.Logger{}) {
t.Fatal("expected initialized base logger")
}
if !reflect.DeepEqual(log.Logger, baseLogger) {
t.Fatal("expected global log.Logger to match baseLogger after concurrent init")
}
}
func TestIsLevelEnabled(t *testing.T) {
t.Cleanup(resetLoggingState)
// Set global level to Info
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if !IsLevelEnabled(zerolog.InfoLevel) {
t.Fatal("expected info level to be enabled")
}
if !IsLevelEnabled(zerolog.WarnLevel) {
t.Fatal("expected warn level to be enabled")
}
if !IsLevelEnabled(zerolog.ErrorLevel) {
t.Fatal("expected error level to be enabled")
}
if IsLevelEnabled(zerolog.DebugLevel) {
t.Fatal("expected debug level to be disabled")
}
// Change to debug level
zerolog.SetGlobalLevel(zerolog.DebugLevel)
if !IsLevelEnabled(zerolog.DebugLevel) {
t.Fatal("expected debug level to be enabled after setting global level")
}
}
func TestRollingFileWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
logFile := dir + "/test.log"
cfg := Config{
Format: "json",
Level: "info",
FilePath: logFile,
MaxSizeMB: 1,
MaxAgeDays: 7,
Compress: false,
}
Init(cfg)
// Write some log output
log.Info().Msg("test message")
// Check file exists (this confirms rolling file writer was created)
if _, err := os.Stat(logFile); os.IsNotExist(err) {
t.Fatal("expected log file to be created")
}
// Check file has content
data, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
if len(data) == 0 {
t.Fatal("expected log file to have content")
}
}
func TestInitClosesPreviousRollingFileWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
logFile := filepath.Join(dir, "app.log")
closeCalls := 0
closeFileFn = func(file *os.File) error {
closeCalls++
return file.Close()
}
Init(Config{Format: "json", FilePath: logFile})
Init(Config{Format: "json", FilePath: logFile})
if closeCalls != 1 {
t.Fatalf("expected previous file writer to be closed once, got %d", closeCalls)
}
}
func TestShutdownClosesActiveRollingFileWriterAndSubscribers(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
logFile := filepath.Join(dir, "app.log")
closeCalls := 0
closeFileFn = func(file *os.File) error {
closeCalls++
return file.Close()
}
Init(Config{Format: "json", FilePath: logFile})
_, ch, _ := GetBroadcaster().Subscribe()
Shutdown()
Shutdown()
if closeCalls != 1 {
t.Fatalf("expected active file writer to be closed once, got %d", closeCalls)
}
if fileCloser != nil {
t.Fatal("expected fileCloser to be cleared after shutdown")
}
select {
case _, ok := <-ch:
if ok {
t.Fatal("expected subscriber channel to be closed on shutdown")
}
default:
t.Fatal("expected subscriber channel to be closed immediately")
}
}
func TestParseLevelDefaults(t *testing.T) {
tests := []struct {
input string
want zerolog.Level
}{
{"", zerolog.InfoLevel},
{"debug", zerolog.DebugLevel},
{"DEBUG", zerolog.DebugLevel},
{"trace", zerolog.TraceLevel},
{"info", zerolog.InfoLevel},
{"INFO", zerolog.InfoLevel},
{"warn", zerolog.WarnLevel},
{"warning", zerolog.WarnLevel},
{"WARN", zerolog.WarnLevel},
{"error", zerolog.ErrorLevel},
{"ERROR", zerolog.ErrorLevel},
{"fatal", zerolog.FatalLevel},
{"panic", zerolog.PanicLevel},
{"disabled", zerolog.Disabled},
{"unknown", zerolog.InfoLevel},
}
for _, tc := range tests {
got := parseLevel(tc.input)
if got != tc.want {
t.Errorf("parseLevel(%q) = %v, want %v", tc.input, got, tc.want)
}
}
}
func TestParseLevelWarnsOnInvalidValue(t *testing.T) {
t.Cleanup(resetLoggingState)
stderr := captureStderr(t, func() {
if got := parseLevel(" VERBOSE "); got != zerolog.InfoLevel {
t.Fatalf("expected info fallback for invalid level, got %s", got)
}
})
if !strings.Contains(stderr, `logging: invalid level "verbose"; using "info"`) {
t.Fatalf("expected invalid level warning, got %q", stderr)
}
}
func TestSelectWriter(t *testing.T) {
tests := []struct {
format string
isTTY bool
wantErr bool
}{
{"json", false, false},
{"console", false, false},
{"auto", false, false},
}
for _, tc := range tests {
w := selectWriter(tc.format)
if w == nil {
t.Errorf("selectWriter(%q) returned nil", tc.format)
}
}
}
func TestSelectWriterAutoTerminal(t *testing.T) {
t.Cleanup(resetLoggingState)
isTerminalFn = func(int) bool { return true }
w := selectWriter("auto")
if _, ok := w.(zerolog.ConsoleWriter); !ok {
t.Fatalf("expected console writer, got %#v", w)
}
}
func TestSelectWriterDefault(t *testing.T) {
t.Cleanup(resetLoggingState)
w := selectWriter("unknown")
if w != os.Stderr {
t.Fatalf("expected default writer to be os.Stderr, got %#v", w)
}
}
func TestSelectWriterWarnsOnInvalidFormat(t *testing.T) {
t.Cleanup(resetLoggingState)
stderr := captureStderr(t, func() {
w := selectWriter("pretty")
if w != os.Stderr {
t.Fatalf("expected invalid format fallback to os.Stderr, got %#v", w)
}
})
if !strings.Contains(stderr, `logging: invalid format "pretty"; using "json"`) {
t.Fatalf("expected invalid format warning, got %q", stderr)
}
}
func TestIsTerminalNil(t *testing.T) {
t.Cleanup(resetLoggingState)
if isTerminal(nil) {
t.Fatal("expected nil file to report false")
}
}
func TestNewRollingFileWriter_EmptyPath(t *testing.T) {
t.Cleanup(resetLoggingState)
writer, err := newRollingFileWriter(Config{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if writer != nil {
t.Fatalf("expected nil writer, got %#v", writer)
}
}
func TestNewRollingFileWriter_MkdirError(t *testing.T) {
t.Cleanup(resetLoggingState)
mkdirAllFn = func(string, os.FileMode) error {
return errors.New("mkdir failed")
}
_, err := newRollingFileWriter(Config{FilePath: "/tmp/logs/test.log"})
if err == nil {
t.Fatal("expected error from mkdir")
}
}
func TestNewRollingFileWriter_HardensDirectoryPermissions(t *testing.T) {
t.Cleanup(resetLoggingState)
root := t.TempDir()
logDir := filepath.Join(root, "logs")
if err := os.MkdirAll(logDir, 0o755); err != nil {
t.Fatalf("failed to create log dir: %v", err)
}
writer, err := newRollingFileWriter(Config{FilePath: filepath.Join(logDir, "app.log")})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
defer func() { _ = w.closeLocked() }()
info, err := os.Stat(logDir)
if err != nil {
t.Fatalf("failed to stat log dir: %v", err)
}
if got := info.Mode().Perm(); got != logDirPerm {
t.Fatalf("expected log dir mode %o, got %o", logDirPerm, got)
}
}
func TestNewRollingFileWriter_RejectsSymlinkFile(t *testing.T) {
t.Cleanup(resetLoggingState)
root := t.TempDir()
target := filepath.Join(root, "target.log")
if err := os.WriteFile(target, []byte("data"), 0o600); err != nil {
t.Fatalf("failed to create target file: %v", err)
}
link := filepath.Join(root, "symlink.log")
requireSymlinkOrSkip(t, target, link)
if _, err := newRollingFileWriter(Config{FilePath: link}); err == nil {
t.Fatal("expected error for symlink log file path")
}
}
func TestNewRollingFileWriter_RejectsSymlinkDir(t *testing.T) {
t.Cleanup(resetLoggingState)
root := t.TempDir()
realDir := filepath.Join(root, "real-logs")
if err := os.MkdirAll(realDir, 0o755); err != nil {
t.Fatalf("failed to create real log dir: %v", err)
}
linkDir := filepath.Join(root, "logs-link")
requireSymlinkOrSkip(t, realDir, linkDir)
if _, err := newRollingFileWriter(Config{FilePath: filepath.Join(linkDir, "app.log")}); err == nil {
t.Fatal("expected error for symlink log directory path")
}
}
func TestInitFileWriterError(t *testing.T) {
t.Cleanup(resetLoggingState)
mkdirAllFn = func(string, os.FileMode) error {
return errors.New("mkdir failed")
}
Init(Config{
Format: "json",
FilePath: "/tmp/logs/test.log",
})
}
func TestNewRollingFileWriter_DefaultMaxSize(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
writer, err := newRollingFileWriter(Config{
FilePath: filepath.Join(dir, "app.log"),
MaxSizeMB: 0,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
if w.maxBytes != int64(defaultMaxSizeMB)*bytesPerMB {
t.Fatalf("expected default max bytes, got %d", w.maxBytes)
}
_ = w.closeLocked()
}
func TestNewRollingFileWriter_SecureDirectoryPermissions(t *testing.T) {
t.Cleanup(resetLoggingState)
var (
called bool
mode os.FileMode
)
origMkdirAll := mkdirAllFn
mkdirAllFn = func(path string, perm os.FileMode) error {
called = true
mode = perm
return origMkdirAll(path, perm)
}
logFile := filepath.Join(t.TempDir(), "logs", "app.log")
writer, err := newRollingFileWriter(Config{FilePath: logFile})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !called {
t.Fatal("expected mkdirAllFn to be called")
}
if mode != 0o700 {
t.Fatalf("expected log directory mode 0700, got %#o", mode)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
if w.maxBytes != int64(defaultMaxSizeMB)*bytesPerMB {
t.Fatalf("expected default max bytes, got %d", w.maxBytes)
}
_ = w.closeLocked()
}
func TestNewRollingFileWriter_MaxSizeOverflowUsesDefault(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
writer, err := newRollingFileWriter(Config{
FilePath: filepath.Join(dir, "app.log"),
MaxSizeMB: int(maxSafeSizeMB + 1),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
if w.maxBytes != int64(defaultMaxSizeMB)*bytesPerMB {
t.Fatalf("expected default max bytes, got %d", w.maxBytes)
}
_ = w.closeLocked()
}
func TestNewRollingFileWriter_NegativeMaxAgeUsesDefault(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
writer, err := newRollingFileWriter(Config{
FilePath: filepath.Join(dir, "app.log"),
MaxSizeMB: 1,
MaxAgeDays: -1,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
if w.maxAge != time.Duration(defaultMaxAgeDays)*24*time.Hour {
t.Fatalf("expected default max age, got %s", w.maxAge)
}
_ = w.closeLocked()
}
func TestNewRollingFileWriter_ZeroMaxAgeDisablesCleanup(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
writer, err := newRollingFileWriter(Config{
FilePath: filepath.Join(dir, "app.log"),
MaxSizeMB: 1,
MaxAgeDays: 0,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
if w.maxAge != 0 {
t.Fatalf("expected max age 0, got %s", w.maxAge)
}
_ = w.closeLocked()
}
func TestNewRollingFileWriter_MaxAgeOverflowClamps(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
writer, err := newRollingFileWriter(Config{
FilePath: filepath.Join(dir, "app.log"),
MaxSizeMB: 1,
MaxAgeDays: maxDurationDays + 1,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, ok := writer.(*rollingFileWriter)
if !ok {
t.Fatalf("expected rollingFileWriter, got %#v", writer)
}
if w.maxAge != time.Duration(maxDurationDays)*24*time.Hour {
t.Fatalf("expected clamped max age, got %s", w.maxAge)
}
_ = w.closeLocked()
}
func TestNewRollingFileWriter_OpenError(t *testing.T) {
t.Cleanup(resetLoggingState)
openFileFn = func(string, int, os.FileMode) (*os.File, error) {
return nil, errors.New("open failed")
}
_, err := newRollingFileWriter(Config{FilePath: filepath.Join(t.TempDir(), "app.log")})
if err == nil {
t.Fatal("expected error from openOrCreateLocked")
}
}
func TestOpenOrCreateLocked_StatError(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
w := &rollingFileWriter{path: filepath.Join(dir, "app.log")}
statFileFn = func(*os.File) (os.FileInfo, error) {
return nil, errors.New("stat failed")
}
if err := w.openOrCreateLocked(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if w.currentSize != 0 {
t.Fatalf("expected current size 0, got %d", w.currentSize)
}
_ = w.closeLocked()
}
func TestOpenOrCreateLocked_AlreadyOpen(t *testing.T) {
t.Cleanup(resetLoggingState)
file, err := os.CreateTemp(t.TempDir(), "log")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
w := &rollingFileWriter{path: file.Name(), file: file}
if err := w.openOrCreateLocked(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
_ = w.closeLocked()
}
func TestOpenOrCreateLocked_HardensFilePermissions(t *testing.T) {
t.Cleanup(resetLoggingState)
path := filepath.Join(t.TempDir(), "app.log")
if err := os.WriteFile(path, []byte("existing"), 0o644); err != nil {
t.Fatalf("failed to create log file: %v", err)
}
w := &rollingFileWriter{path: path}
if err := w.openOrCreateLocked(); err != nil {
t.Fatalf("openOrCreateLocked error: %v", err)
}
defer func() { _ = w.closeLocked() }()
info, err := os.Stat(path)
if err != nil {
t.Fatalf("failed to stat log file: %v", err)
}
if got := info.Mode().Perm(); got != logFilePerm {
t.Fatalf("expected log file mode %o, got %o", logFilePerm, got)
}
}
func TestRollingFileWriter_WriteOpenError(t *testing.T) {
t.Cleanup(resetLoggingState)
openFileFn = func(string, int, os.FileMode) (*os.File, error) {
return nil, errors.New("open failed")
}
w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log")}
if _, err := w.Write([]byte("data")); err == nil {
t.Fatal("expected write error")
}
}
func TestRollingFileWriter_WriteRotateError(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
callCount := 0
openFileFn = func(name string, flag int, perm os.FileMode) (*os.File, error) {
callCount++
if callCount == 1 {
return os.OpenFile(name, flag, perm)
}
return nil, errors.New("open failed")
}
w := &rollingFileWriter{path: path, maxBytes: 1}
if _, err := w.Write([]byte("too big")); err == nil {
t.Fatal("expected rotate error")
}
}
func TestRollingFileWriter_RotateCompress(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0600); err != nil {
t.Fatalf("failed to write file: %v", err)
}
w := &rollingFileWriter{path: path, maxBytes: 1, compress: true}
if err := w.openOrCreateLocked(); err != nil {
t.Fatalf("openOrCreateLocked error: %v", err)
}
ch := make(chan string, 1)
compressFn = func(p string) { ch <- p }
if err := w.rotateLocked(); err != nil {
t.Fatalf("rotateLocked error: %v", err)
}
select {
case <-ch:
case <-time.After(2 * time.Second):
t.Fatal("expected compress to be triggered")
}
_ = w.closeLocked()
}
func TestRollingFileWriter_RotateRenameError(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0600); err != nil {
t.Fatalf("failed to write file: %v", err)
}
w := &rollingFileWriter{path: path, maxBytes: 1, compress: true}
if err := w.openOrCreateLocked(); err != nil {
t.Fatalf("openOrCreateLocked error: %v", err)
}
renameCalled := false
renameFn = func(oldpath, newpath string) error {
renameCalled = true
return errors.New("rename failed")
}
compressCalled := make(chan struct{}, 1)
compressFn = func(string) {
compressCalled <- struct{}{}
}
if err := w.rotateLocked(); err != nil {
t.Fatalf("rotateLocked error: %v", err)
}
if !renameCalled {
t.Fatal("expected rename to be attempted")
}
select {
case <-compressCalled:
t.Fatal("expected compression to be skipped on rename error")
default:
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected log file to exist after rename failure: %v", err)
}
_ = w.closeLocked()
}
func TestRotateLockedCloseError(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
t.Fatalf("failed to open file: %v", err)
}
w := &rollingFileWriter{path: path, file: file}
closeFileFn = func(*os.File) error {
return errors.New("close failed")
}
if err := w.rotateLocked(); err == nil {
t.Fatal("expected close error")
}
_ = file.Close()
}
func TestCloseLocked(t *testing.T) {
t.Cleanup(resetLoggingState)
w := &rollingFileWriter{}
if err := w.closeLocked(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
file, err := os.CreateTemp(t.TempDir(), "log")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
w.file = file
if err := w.closeLocked(); err != nil {
t.Fatalf("unexpected close error: %v", err)
}
if w.file != nil {
t.Fatal("expected file to be cleared")
}
if w.currentSize != 0 {
t.Fatalf("expected size reset, got %d", w.currentSize)
}
}
func TestCleanupOldFilesNoMaxAge(t *testing.T) {
t.Cleanup(resetLoggingState)
w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log"), maxAge: 0}
w.cleanupOldFiles()
}
func TestCleanupOldFilesReadDirError(t *testing.T) {
t.Cleanup(resetLoggingState)
readDirFn = func(string) ([]os.DirEntry, error) {
return nil, errors.New("read dir failed")
}
w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log"), maxAge: time.Hour}
w.cleanupOldFiles()
}
func TestCleanupOldFilesInfoError(t *testing.T) {
t.Cleanup(resetLoggingState)
readDirFn = func(string) ([]os.DirEntry, error) {
return []os.DirEntry{errDirEntry{name: "app.log.20200101"}}, nil
}
w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log"), maxAge: time.Hour}
w.cleanupOldFiles()
}
func TestCleanupOldFilesRemovesOld(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
oldFile := filepath.Join(dir, "app.log.20200101-000000")
newFile := filepath.Join(dir, "app.log.20250101-000000")
otherFile := filepath.Join(dir, "other.log.20200101")
if err := os.WriteFile(oldFile, []byte("old"), 0600); err != nil {
t.Fatalf("failed to write old file: %v", err)
}
if err := os.WriteFile(newFile, []byte("new"), 0600); err != nil {
t.Fatalf("failed to write new file: %v", err)
}
if err := os.WriteFile(otherFile, []byte("other"), 0600); err != nil {
t.Fatalf("failed to write other file: %v", err)
}
fixedNow := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)
nowFn = func() time.Time { return fixedNow }
if err := os.Chtimes(oldFile, fixedNow.Add(-48*time.Hour), fixedNow.Add(-48*time.Hour)); err != nil {
t.Fatalf("failed to set old file time: %v", err)
}
if err := os.Chtimes(newFile, fixedNow.Add(-time.Hour), fixedNow.Add(-time.Hour)); err != nil {
t.Fatalf("failed to set new file time: %v", err)
}
w := &rollingFileWriter{path: path, maxAge: 24 * time.Hour}
w.cleanupOldFiles()
if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
t.Fatalf("expected old file to be removed")
}
if _, err := os.Stat(newFile); err != nil {
t.Fatalf("expected new file to remain: %v", err)
}
if _, err := os.Stat(otherFile); err != nil {
t.Fatalf("expected other file to remain: %v", err)
}
}
func TestStatFileFnDefault(t *testing.T) {
t.Cleanup(resetLoggingState)
file, err := os.CreateTemp(t.TempDir(), "log")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
t.Cleanup(func() { _ = file.Close() })
if _, err := statFileFn(file); err != nil {
t.Fatalf("statFileFn error: %v", err)
}
}
func TestCompressAndRemove(t *testing.T) {
t.Run("OpenError", func(t *testing.T) {
t.Cleanup(resetLoggingState)
openFn = func(string) (*os.File, error) {
return nil, errors.New("open failed")
}
compressAndRemove("/does/not/exist")
})
t.Run("OpenFileError", func(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0600); err != nil {
t.Fatalf("failed to write file: %v", err)
}
openFileFn = func(string, int, os.FileMode) (*os.File, error) {
return nil, errors.New("open file failed")
}
compressAndRemove(path)
})
t.Run("CopyError", func(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0600); err != nil {
t.Fatalf("failed to write file: %v", err)
}
copyFn = func(io.Writer, io.Reader) (int64, error) {
return 0, errors.New("copy failed")
}
compressAndRemove(path)
})
t.Run("CloseError", func(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0600); err != nil {
t.Fatalf("failed to write file: %v", err)
}
errWriter := errWriteCloser{err: errors.New("write failed")}
gzipNewWriterFn = func(io.Writer) *gzip.Writer {
return gzip.NewWriter(errWriter)
}
copyFn = func(io.Writer, io.Reader) (int64, error) {
return 0, nil
}
compressAndRemove(path)
})
t.Run("Success", func(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0600); err != nil {
t.Fatalf("failed to write file: %v", err)
}
compressAndRemove(path)
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatal("expected original file to be removed")
}
if _, err := os.Stat(path + ".gz"); err != nil {
t.Fatalf("expected gzip file to exist: %v", err)
}
})
t.Run("RejectsSymlinkSource", func(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
target := filepath.Join(dir, "target.log")
if err := os.WriteFile(target, []byte("data"), 0o600); err != nil {
t.Fatalf("failed to write target file: %v", err)
}
symlinkPath := filepath.Join(dir, "app.log")
requireSymlinkOrSkip(t, target, symlinkPath)
compressAndRemove(symlinkPath)
if _, err := os.Stat(symlinkPath + ".gz"); err == nil {
t.Fatal("expected no gzip output for symlink source path")
}
})
t.Run("RejectsSymlinkDestination", func(t *testing.T) {
t.Cleanup(resetLoggingState)
dir := t.TempDir()
path := filepath.Join(dir, "app.log")
if err := os.WriteFile(path, []byte("data"), 0o600); err != nil {
t.Fatalf("failed to write source file: %v", err)
}
target := filepath.Join(dir, "target.gz")
if err := os.WriteFile(target, []byte("target"), 0o600); err != nil {
t.Fatalf("failed to write destination target file: %v", err)
}
requireSymlinkOrSkip(t, target, path+".gz")
compressAndRemove(path)
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected source file to remain when destination path is unsafe: %v", err)
}
if _, err := os.Stat(path + ".gz"); err != nil {
t.Fatalf("expected symlink destination path to remain: %v", err)
}
})
}
// Test that the logging package doesn't panic under concurrent use
func TestConcurrentLogging(t *testing.T) {
t.Cleanup(resetLoggingState)
buf := &lockedBuffer{}
mu.Lock()
baseWriter = buf
baseLogger = zerolog.New(buf).With().Timestamp().Logger()
log.Logger = baseLogger
mu.Unlock()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
log.Info().Int("iteration", n).Msg("concurrent log")
}(i)
}
wg.Wait()
if buf.Len() == 0 {
t.Fatal("expected log output from concurrent logging")
}
}
type lockedBuffer struct {
mu sync.Mutex
buf bytes.Buffer
}
func (b *lockedBuffer) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Write(p)
}
func (b *lockedBuffer) Len() int {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Len()
}
type errDirEntry struct {
name string
}
func (e errDirEntry) Name() string { return e.name }
func (e errDirEntry) IsDir() bool { return false }
func (e errDirEntry) Type() os.FileMode {
return 0
}
func (e errDirEntry) Info() (os.FileInfo, error) {
return nil, errors.New("info error")
}
type errWriteCloser struct {
err error
}
func (e errWriteCloser) Write(p []byte) (int, error) {
return 0, e.err
}
func requireSymlinkOrSkip(t *testing.T, target, link string) {
t.Helper()
if err := os.Symlink(target, link); err != nil {
t.Skipf("symlink not supported in this environment: %v", err)
}
}