config_test.go #2

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