package logger_test
import (
"bufio"
"os"
"path/filepath"
"strings"
"testing"
"time"
"workshop.perforce.com/p4lf/internal/logger"
)
func newTestLogger(t *testing.T, maxSize int64, debug int) (*logger.Logger, string) {
t.Helper()
dir := t.TempDir()
logPath := filepath.Join(dir, "p4lf.log")
l, err := logger.New(logPath, maxSize, debug)
if err != nil {
t.Fatalf("logger.New: %v", err)
}
t.Cleanup(func() { l.Close() })
return l, logPath
}
func readLog(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile %q: %v", path, err)
}
return string(data)
}
// TestLogger_InfoWritten verifies that Infof produces an [INFO] line.
func TestLogger_InfoWritten(t *testing.T) {
l, logPath := newTestLogger(t, 0, 0)
l.Infof("hello %s", "world")
l.Close()
if !strings.Contains(readLog(t, logPath), "[INFO] hello world") {
t.Errorf("expected [INFO] line in log; got:\n%s", readLog(t, logPath))
}
}
// TestLogger_WarnAndErrorWritten verifies WARN and ERROR levels.
func TestLogger_WarnAndErrorWritten(t *testing.T) {
l, logPath := newTestLogger(t, 0, 0)
l.Warnf("something fishy")
l.Errorf("something broken")
l.Close()
content := readLog(t, logPath)
if !strings.Contains(content, "[WARN] something fishy") {
t.Errorf("expected [WARN] line; got:\n%s", content)
}
if !strings.Contains(content, "[ERROR] something broken") {
t.Errorf("expected [ERROR] line; got:\n%s", content)
}
}
// TestLogger_DebugFiltered verifies that Debugf output is suppressed when debug=0.
func TestLogger_DebugFiltered(t *testing.T) {
l, logPath := newTestLogger(t, 0, 0)
l.Debugf("this should be suppressed")
l.Close()
if strings.Contains(readLog(t, logPath), "suppressed") {
t.Error("debug message appeared in log when debug=0")
}
}
// TestLogger_DebugEnabled verifies that Debugf output appears when debug=1.
func TestLogger_DebugEnabled(t *testing.T) {
l, logPath := newTestLogger(t, 0, 1)
l.Debugf("debug detail")
l.Close()
if !strings.Contains(readLog(t, logPath), "[DEBUG] debug detail") {
t.Errorf("expected [DEBUG] line when debug=1; got:\n%s", readLog(t, logPath))
}
}
// TestLogger_TimestampFormat verifies that each line starts with an ISO-8601 UTC timestamp.
func TestLogger_TimestampFormat(t *testing.T) {
l, logPath := newTestLogger(t, 0, 0)
l.Infof("timestamped message")
l.Close()
f, err := os.Open(logPath)
if err != nil {
t.Fatal(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Scan()
line := scanner.Text()
// Expected prefix: 2006-01-02T15:04:05.000Z
if len(line) < 24 || line[4] != '-' || line[7] != '-' || line[10] != 'T' || !strings.HasSuffix(strings.Fields(line)[0], "Z") {
t.Errorf("line does not start with expected ISO timestamp: %q", line)
}
}
// TestLogger_Rotate verifies that an explicit Rotate() call creates a rotated
// log file and that subsequent writes go to a fresh log at the original path.
func TestLogger_Rotate(t *testing.T) {
l, logPath := newTestLogger(t, 0, 0)
dir := filepath.Dir(logPath)
l.Infof("before rotation")
if err := l.Rotate(); err != nil {
t.Fatalf("Rotate: %v", err)
}
l.Infof("after rotation")
l.Close()
// Original log should exist and contain only post-rotation content.
if !strings.Contains(readLog(t, logPath), "after rotation") {
t.Error("post-rotate content missing from current log")
}
if strings.Contains(readLog(t, logPath), "before rotation") {
t.Error("pre-rotate content unexpectedly in current log")
}
// A rotated file (plain .log or gzipped .log.gz) must appear within 2s.
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
plain, _ := filepath.Glob(filepath.Join(dir, "p4lf.*.log"))
gzipped, _ := filepath.Glob(filepath.Join(dir, "p4lf.*.log.gz"))
if len(plain) > 0 || len(gzipped) > 0 {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Error("no rotated log file found after Rotate()")
}
// TestLogger_MaxLogSizeRotation verifies that the log is automatically rotated
// when the size threshold is exceeded.
func TestLogger_MaxLogSizeRotation(t *testing.T) {
const maxSize = 200
l, logPath := newTestLogger(t, maxSize, 0)
dir := filepath.Dir(logPath)
// Each log line is ~60+ bytes; 10 lines will exceed 200 bytes.
for i := 0; i < 10; i++ {
l.Infof("log line %d — padding to trigger auto-rotation threshold", i)
}
l.Close()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
plain, _ := filepath.Glob(filepath.Join(dir, "p4lf.*.log"))
gzipped, _ := filepath.Glob(filepath.Join(dir, "p4lf.*.log.gz"))
if len(plain) > 0 || len(gzipped) > 0 {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Errorf("no rotation occurred after exceeding MaxLogSize=%d", maxSize)
}
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 33126 | C. Thomas Tyler |
Add regression tests for tailer and logger packages tailer_test.go (6 tests): - ReadFromStart: verifies all existing content returned on first run - ReadFromEnd: verifies existing content skipped, only appended bytes returned - StateResume: save state, new tailer picks up exactly where previous left off - RotationDetected: inode change triggers reset to offset 0 on new file - NoNewData: empty file returns nil chunk - OffsetAccumulation: consecutive flushes produce contiguous offset windows logger_test.go (6 tests): - InfoWritten: Infof produces [INFO] line - WarnAndErrorWritten: WARN and ERROR levels appear correctly - DebugFiltered: Debugf suppressed when debug=0 - DebugEnabled: Debugf appears when debug=1 - TimestampFormat: each line starts with ISO-8601 UTC timestamp - Rotate: explicit Rotate() creates rotated file, fresh log at original path - MaxLogSizeRotation: automatic rotation fires when size threshold exceeded Co-authored-by: Copilot <[email protected]> |