logger_test.go #1

  • //
  • p4lf/
  • dev/
  • internal/
  • logger/
  • logger_test.go
  • View
  • Commits
  • Open Download .zip Download (5 KB)
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]>