package config_test
import (
"os"
"path/filepath"
"testing"
"time"
"workshop.perforce.com/p4lf/internal/config"
)
func writeConfig(t *testing.T, content string) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "p4lf-*.cfg")
if err != nil {
t.Fatalf("creating temp config: %v", err)
}
f.WriteString(content)
f.Close()
return f.Name()
}
func TestLoad_Defaults(t *testing.T) {
path := writeConfig(t, "P4LogFile = /p4/1/logs/log\n")
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.LogTailDelay != 60*time.Second {
t.Errorf("LogTailDelay: got %v, want 60s", cfg.LogTailDelay)
}
if cfg.MaxLogChunks != 0 {
t.Errorf("MaxLogChunks: got %d, want 0", cfg.MaxLogChunks)
}
if !cfg.ReadFromStart {
t.Error("ReadFromStart: want true by default")
}
if cfg.Debug != 0 {
t.Errorf("Debug: got %d, want 0", cfg.Debug)
}
}
func TestLoad_AllSettings(t *testing.T) {
path := writeConfig(t, `
P4LogFile = /p4/1/logs/log
LogTailDelay = 30s
MaxLogChunks = 10
MinLogSpace = 5%
MaxLogSize = 50M
MaxRotationRecoverySize = 200M
StateFile = /tmp/p4lf.state
ReadFromStart = false
Debug = 2
`)
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.P4LogFile != "/p4/1/logs/log" {
t.Errorf("P4LogFile: got %q", cfg.P4LogFile)
}
if cfg.LogTailDelay != 30*time.Second {
t.Errorf("LogTailDelay: got %v, want 30s", cfg.LogTailDelay)
}
if cfg.MaxLogChunks != 10 {
t.Errorf("MaxLogChunks: got %d, want 10", cfg.MaxLogChunks)
}
if !cfg.MinLogSpace.IsPercent || cfg.MinLogSpace.Percent != 5 {
t.Errorf("MinLogSpace: got %+v, want 5%%", cfg.MinLogSpace)
}
if cfg.MaxLogSize != 50*1024*1024 {
t.Errorf("MaxLogSize: got %d, want %d", cfg.MaxLogSize, 50*1024*1024)
}
if cfg.MaxRotationRecoverySize != 200*1024*1024 {
t.Errorf("MaxRotationRecoverySize: got %d, want %d", cfg.MaxRotationRecoverySize, 200*1024*1024)
}
if cfg.StateFile != "/tmp/p4lf.state" {
t.Errorf("StateFile: got %q", cfg.StateFile)
}
if cfg.ReadFromStart {
t.Error("ReadFromStart: want false")
}
if cfg.Debug != 2 {
t.Errorf("Debug: got %d, want 2", cfg.Debug)
}
}
func TestLoad_DurationFormats(t *testing.T) {
cases := []struct {
input string
want time.Duration
}{
{"60s", 60 * time.Second},
{"3m", 3 * time.Minute},
{"1h", 1 * time.Hour},
{"120", 120 * time.Second},
}
for _, tc := range cases {
path := writeConfig(t, "P4LogFile = /log\nLogTailDelay = "+tc.input+"\n")
cfg, err := config.Load(path)
if err != nil {
t.Errorf("input %q: unexpected error: %v", tc.input, err)
continue
}
if cfg.LogTailDelay != tc.want {
t.Errorf("input %q: got %v, want %v", tc.input, cfg.LogTailDelay, tc.want)
}
}
}
func TestLoad_SizeFormats(t *testing.T) {
cases := []struct {
input string
want int64
}{
{"100M", 100 * 1024 * 1024},
{"2G", 2 * 1024 * 1024 * 1024},
{"1T", 1024 * 1024 * 1024 * 1024},
{"512K", 512 * 1024},
{"0", 0},
{"None", 0},
}
for _, tc := range cases {
path := writeConfig(t, "P4LogFile = /log\nMaxLogSize = "+tc.input+"\n")
cfg, err := config.Load(path)
if err != nil {
t.Errorf("input %q: unexpected error: %v", tc.input, err)
continue
}
if cfg.MaxLogSize != tc.want {
t.Errorf("input %q: got %d, want %d", tc.input, cfg.MaxLogSize, tc.want)
}
}
}
func TestLoad_SpaceSpecAbsolute(t *testing.T) {
path := writeConfig(t, "P4LogFile = /log\nMinLogSpace = 3G\n")
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.MinLogSpace.IsPercent {
t.Error("expected absolute size, got percent")
}
if cfg.MinLogSpace.Bytes != 3*1024*1024*1024 {
t.Errorf("Bytes: got %d, want %d", cfg.MinLogSpace.Bytes, 3*1024*1024*1024)
}
}
func TestLoad_MissingP4LogFile(t *testing.T) {
// Temporarily unset P4LOG if set.
old := os.Getenv("P4LOG")
os.Unsetenv("P4LOG")
defer os.Setenv("P4LOG", old)
path := writeConfig(t, "# no P4LogFile set\n")
_, err := config.Load(path)
if err == nil {
t.Error("expected error for missing P4LogFile, got nil")
}
}
func TestLoad_UnknownKey(t *testing.T) {
path := writeConfig(t, "P4LogFile = /log\nUnknownKey = value\n")
_, err := config.Load(path)
if err == nil {
t.Error("expected error for unknown key, got nil")
}
}
func TestLoad_Comments(t *testing.T) {
path := writeConfig(t, `# This is a comment
P4LogFile = /p4/1/logs/log
# LogTailDelay = 999s
`)
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// LogTailDelay should be default (60s), not 999s.
if cfg.LogTailDelay != 60*time.Second {
t.Errorf("commented-out key was applied: LogTailDelay = %v", cfg.LogTailDelay)
}
}
func TestLoad_MissingFile(t *testing.T) {
_, err := config.Load(filepath.Join(t.TempDir(), "nonexistent.cfg"))
if err == nil {
t.Error("expected error for missing config file, got nil")
}
}
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #2 | 32824 | C. Thomas Tyler |
Rename Go module path from github.com/rcowham/p4lf to workshop.perforce.com/p4lf. The rcowham prefix was a carryover from early research into go-libtail (which is hosted at github.com/rcowham/go-libtail). p4lf is hosted on the Perforce Public Depot / Workshop, not GitHub, so workshop.perforce.com is the correct canonical module path. Updated: go.mod, all *.go files with internal imports, Makefile (MODULE and LDFLAGS). Session logs retain the old path as accurate historical record. |
||
| #1 | 32818 | C. Thomas Tyler |
Initial implementation of p4lf in Go. Adds: - Go module (github.com/rcowham/p4lf) with fsnotify dependency - internal/config: KEY=VALUE config parser with all settings - internal/tailer: file reader with inode-based rotation detection and state file checkpoint/resume (inode + byte offset, JSON, atomic write) - internal/chunker: gzip chunk writer with MaxLogChunks/MinLogSpace guards - internal/logger: rotating log writer with gzip of rotated files - cmd/p4lf/main.go: service main loop, SIGHUP/SIGTERM/SIGINT handling, config hot-reload on modtime change - Makefile: build/test/install/release targets for Linux amd64/arm64, macOS arm64/amd64; version injected via ldflags - p4lf.cfg.example: fully documented example config - p4lf.service: systemd unit file (User=perforce, Restart=on-failure) - ai/session_log_2026-06-25.md: design session log - ai/session_log_2026-06-25-2.md: implementation session log - .p4ignore: added bin/ and dist/ |