Add AGENTS.md, simplify code, 2 fixes in log package, 3 bugs fixed.
- Add AGENTS.md for vibecoding ai data. - Remove github.com/pkg/errors dependency - writeWithContext — remove goroutine overhead, just check context before fn - copyWithContext — remove unnecessary panic recovery - Remove panic recovery tests (TestWriteWithContextPanic, TestCopyWithContextPanic, panicReader) - Error, Warn, Info, Debug, Fatal — accept ...any instead of string (enables log.Error(err)) - Close — don't panic if Log is nil, don't close stderr - Add unit tests (internal/log/main_test.go) - getClientIP — trim spaces from X-Forwarded-For, use net.SplitHostPort for IPv6 - isExcluded — normalize paths to forward slashes for cross-platform regex matching - writeWithContext godoc — fix stale comment about panic recovery - internal/log/main_test.go — log package tests (19 tests)
This commit is contained in:
@@ -9,9 +9,12 @@ import (
|
||||
)
|
||||
|
||||
// isExcluded checks if path matches any of the configured exclude patterns.
|
||||
// Paths are normalized to forward slashes for cross-platform regex matching.
|
||||
func isExcluded(path string) bool {
|
||||
normalized := filepath.ToSlash(path)
|
||||
|
||||
for _, re := range excludePatterns {
|
||||
if re.MatchString(path) {
|
||||
if re.MatchString(normalized) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ func normalizePath(p string) string {
|
||||
|
||||
// TestGetFilesFromDirectories checks if GetFilesFromDirectories returns the proper amount of strings, correct strings and no error.
|
||||
func TestGetFilesFromDirectories(t *testing.T) {
|
||||
// Ensure empty_dir exists for the test.
|
||||
os.MkdirAll("../../test_data/test1/foo/empty_dir", 0755)
|
||||
|
||||
var (
|
||||
input = Config{
|
||||
Directories: []string{"../../test_data/test1/foo", "../../test_data/test1/bar"},
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -107,47 +108,22 @@ func NewServer(configPath string) (*http.Server, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// writeWithContext executes fn in a goroutine and returns its result.
|
||||
// If the context is canceled before fn completes, ctx.Err() is returned.
|
||||
// fn should perform I/O that will unblock when the underlying pipe is closed
|
||||
// on context cancellation (e.g., writes to a pipe writer). Panics in fn are
|
||||
// recovered and returned as errors.
|
||||
// writeWithContext checks if context is canceled before executing fn.
|
||||
// Returns ctx.Err() if context is already canceled, otherwise returns fn().
|
||||
func writeWithContext(ctx context.Context, fn func() error) error {
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
done <- fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
done <- ctx.Err()
|
||||
default:
|
||||
done <- fn()
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-done:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
// copyWithContext copies data from src to dst using a 32KB buffer, respecting
|
||||
// context cancellation. If the context is canceled, the copy stops and returns
|
||||
// ctx.Err(). Panics are recovered and returned as errors.
|
||||
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (err error) { //nolint:nonamedreturns
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// ctx.Err().
|
||||
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) error {
|
||||
buf := make([]byte, 32*1024)
|
||||
|
||||
for {
|
||||
@@ -178,15 +154,15 @@ func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (err err
|
||||
// It checks X-Forwarded-For header first (for proxied requests), then falls back to RemoteAddr.
|
||||
func getClientIP(r *http.Request) string {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
return strings.Split(xff, ",")[0]
|
||||
return strings.TrimSpace(strings.Split(xff, ",")[0])
|
||||
}
|
||||
|
||||
ip := r.RemoteAddr
|
||||
if colon := strings.LastIndex(ip, ":"); colon != -1 {
|
||||
return ip[:colon]
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
return ip
|
||||
return host
|
||||
}
|
||||
|
||||
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
|
||||
@@ -338,37 +338,4 @@ func TestCopyWithContextCancellation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteWithContextPanic(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := writeWithContext(ctx, func() error {
|
||||
panic("test panic")
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("Expected error from panic, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "panic") {
|
||||
t.Errorf("Expected panic error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyWithContextPanic(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
// Use a reader that panics on Read
|
||||
panicReader := &panicReader{}
|
||||
dst := &strings.Builder{}
|
||||
err := copyWithContext(ctx, dst, panicReader)
|
||||
if err == nil {
|
||||
t.Error("Expected error from panic, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "panic") {
|
||||
t.Errorf("Expected panic error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type panicReader struct{}
|
||||
|
||||
func (pr *panicReader) Read(p []byte) (n int, err error) {
|
||||
panic("read panic")
|
||||
}
|
||||
|
||||
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
|
||||
@@ -156,7 +156,6 @@ func CreateTarGzStream(ctx context.Context, filepaths []string) io.ReadCloser {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -82,9 +82,9 @@ func Init(level, filename string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Error passes message directly to [slog.Error()].
|
||||
func Error(message string) {
|
||||
slog.Error(message)
|
||||
// Error logs args at error level using [slog.Error()].
|
||||
func Error(args ...any) {
|
||||
slog.Error(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Errorf logs message on error log level and allows to supply arguments in printf() style.
|
||||
@@ -92,9 +92,9 @@ func Errorf(format string, a ...any) {
|
||||
slog.Error(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Warn passes message directly to [slog.Warn()].
|
||||
func Warn(message string) {
|
||||
slog.Warn(message)
|
||||
// Warn logs args at warn level using [slog.Warn()].
|
||||
func Warn(args ...any) {
|
||||
slog.Warn(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
|
||||
@@ -102,9 +102,9 @@ func Warnf(format string, a ...any) {
|
||||
slog.Warn(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Info passes message directly to [slog.Info()].
|
||||
func Info(message string) {
|
||||
slog.Info(message)
|
||||
// Info logs args at info level using [slog.Info()].
|
||||
func Info(args ...any) {
|
||||
slog.Info(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Infof logs message on info log level and allows to supply arguments in printf() style.
|
||||
@@ -112,9 +112,9 @@ func Infof(format string, a ...any) {
|
||||
slog.Info(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Debug passes message directly to [slog.Debug()].
|
||||
func Debug(message string) {
|
||||
slog.Debug(message)
|
||||
// Debug logs args at debug level using [slog.Debug()].
|
||||
func Debug(args ...any) {
|
||||
slog.Debug(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
|
||||
@@ -122,9 +122,9 @@ func Debugf(format string, a ...any) {
|
||||
slog.Debug(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Fatal passes message directly to [slog.Error()] and quits with status code 1.
|
||||
func Fatal(message string) {
|
||||
slog.Error(message)
|
||||
// Fatal logs args at error level and quits with status code 1.
|
||||
func Fatal(args ...any) {
|
||||
slog.Error(fmt.Sprint(args...))
|
||||
Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -139,6 +139,10 @@ func Fatalf(format string, a ...any) {
|
||||
|
||||
// Close closes log file descriptor.
|
||||
func Close() {
|
||||
if Log == nil || Log == os.Stderr {
|
||||
return
|
||||
}
|
||||
|
||||
if err := Log.Close(); err != nil {
|
||||
msg := fmt.Sprintln("Failed to close log file:", err)
|
||||
slog.Error(msg)
|
||||
|
||||
357
internal/log/main_test.go
Normal file
357
internal/log/main_test.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// logFile holds the path to the current test log file for reading.
|
||||
var logFilePath string
|
||||
|
||||
// initTestLog sets up a temp log file and returns cleanup function.
|
||||
func initTestLog(t *testing.T, level string) func() {
|
||||
t.Helper()
|
||||
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
logFilePath = tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
if err := Init(level, logFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
Close()
|
||||
logFilePath = ""
|
||||
}
|
||||
}
|
||||
|
||||
// getLogContent reads the log file and returns its contents.
|
||||
func getLogContent(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
if logFilePath == "" {
|
||||
t.Fatal("logFilePath not set")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// TestInitDefaultLevel tests Init with empty level defaults to info.
|
||||
func TestInitDefaultLevel(t *testing.T) {
|
||||
cleanup := initTestLog(t, "")
|
||||
defer cleanup()
|
||||
|
||||
Info("info message")
|
||||
Debug("debug message")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "info message") {
|
||||
t.Error("Expected info message in log")
|
||||
}
|
||||
|
||||
if strings.Contains(content, "debug message") {
|
||||
t.Error("Debug message should not appear at info level")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitErrorLevel tests Init with error level.
|
||||
func TestInitErrorLevel(t *testing.T) {
|
||||
cleanup := initTestLog(t, "error")
|
||||
defer cleanup()
|
||||
|
||||
Error("error msg")
|
||||
Warn("warn msg")
|
||||
Info("info msg")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "error msg") {
|
||||
t.Error("Expected error message in log")
|
||||
}
|
||||
|
||||
if strings.Contains(content, "warn msg") {
|
||||
t.Error("Warn message should not appear at error level")
|
||||
}
|
||||
|
||||
if strings.Contains(content, "info msg") {
|
||||
t.Error("Info message should not appear at error level")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitWarnLevel tests Init with warn level.
|
||||
func TestInitWarnLevel(t *testing.T) {
|
||||
cleanup := initTestLog(t, "warn")
|
||||
defer cleanup()
|
||||
|
||||
Error("error msg")
|
||||
Warn("warn msg")
|
||||
Info("info msg")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "error msg") {
|
||||
t.Error("Expected error message in log")
|
||||
}
|
||||
|
||||
if !strings.Contains(content, "warn msg") {
|
||||
t.Error("Expected warn message in log")
|
||||
}
|
||||
|
||||
if strings.Contains(content, "info msg") {
|
||||
t.Error("Info message should not appear at warn level")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitDebugLevel tests Init with debug level shows everything.
|
||||
func TestInitDebugLevel(t *testing.T) {
|
||||
cleanup := initTestLog(t, "debug")
|
||||
defer cleanup()
|
||||
|
||||
Error("error msg")
|
||||
Warn("warn msg")
|
||||
Info("info msg")
|
||||
Debug("debug msg")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "error msg") {
|
||||
t.Error("Expected error message in log")
|
||||
}
|
||||
|
||||
if !strings.Contains(content, "warn msg") {
|
||||
t.Error("Expected warn message in log")
|
||||
}
|
||||
|
||||
if !strings.Contains(content, "info msg") {
|
||||
t.Error("Expected info message in log")
|
||||
}
|
||||
|
||||
if !strings.Contains(content, "debug msg") {
|
||||
t.Error("Expected debug message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitStderr tests Init with empty filename uses stderr.
|
||||
func TestInitStderr(t *testing.T) {
|
||||
err := Init("info", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if Log != os.Stderr {
|
||||
t.Error("Expected Log to be os.Stderr")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitInvalidPath tests Init with invalid file path returns error.
|
||||
func TestInitInvalidPath(t *testing.T) {
|
||||
err := Init("info", "/nonexistent/path/to/log.log")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid path, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorWithString tests Error with a string argument.
|
||||
func TestErrorWithString(t *testing.T) {
|
||||
cleanup := initTestLog(t, "error")
|
||||
defer cleanup()
|
||||
|
||||
Error("test error message")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "test error message") {
|
||||
t.Error("Expected error message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorWithError tests Error with an error argument.
|
||||
func TestErrorWithError(t *testing.T) {
|
||||
cleanup := initTestLog(t, "error")
|
||||
defer cleanup()
|
||||
|
||||
err := errors.New("something went wrong")
|
||||
Error(err)
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "something went wrong") {
|
||||
t.Error("Expected error message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorWithMultipleArgs tests Error with multiple arguments.
|
||||
func TestErrorWithMultipleArgs(t *testing.T) {
|
||||
cleanup := initTestLog(t, "error")
|
||||
defer cleanup()
|
||||
|
||||
err := errors.New("disk full")
|
||||
Error("Failed to write: ", err)
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "Failed to write:") {
|
||||
t.Error("Expected prefix in log")
|
||||
}
|
||||
|
||||
if !strings.Contains(content, "disk full") {
|
||||
t.Error("Expected error message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWarnWithString tests Warn with a string argument.
|
||||
func TestWarnWithString(t *testing.T) {
|
||||
cleanup := initTestLog(t, "warn")
|
||||
defer cleanup()
|
||||
|
||||
Warn("test warning")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "test warning") {
|
||||
t.Error("Expected warning message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInfoWithString tests Info with a string argument.
|
||||
func TestInfoWithString(t *testing.T) {
|
||||
cleanup := initTestLog(t, "info")
|
||||
defer cleanup()
|
||||
|
||||
Info("test info")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "test info") {
|
||||
t.Error("Expected info message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDebugWithString tests Debug with a string argument.
|
||||
func TestDebugWithString(t *testing.T) {
|
||||
cleanup := initTestLog(t, "debug")
|
||||
defer cleanup()
|
||||
|
||||
Debug("test debug")
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "test debug") {
|
||||
t.Error("Expected debug message in log")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorf tests Errorf with format string.
|
||||
func TestErrorf(t *testing.T) {
|
||||
cleanup := initTestLog(t, "error")
|
||||
defer cleanup()
|
||||
|
||||
Errorf("Failed to read %s: %v", "config.json", errors.New("not found"))
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "Failed to read config.json: not found") {
|
||||
t.Errorf("Expected formatted error in log, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWarnf tests Warnf with format string.
|
||||
func TestWarnf(t *testing.T) {
|
||||
cleanup := initTestLog(t, "warn")
|
||||
defer cleanup()
|
||||
|
||||
Warnf("Skipping %s: %d files", "dir", 5)
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "Skipping dir: 5 files") {
|
||||
t.Errorf("Expected formatted warning in log, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInfof tests Infof with format string.
|
||||
func TestInfof(t *testing.T) {
|
||||
cleanup := initTestLog(t, "info")
|
||||
defer cleanup()
|
||||
|
||||
Infof("Server started on %s:%d", "0.0.0.0", 8086)
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "Server started on 0.0.0.0:8086") {
|
||||
t.Errorf("Expected formatted info in log, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDebugf tests Debugf with format string.
|
||||
func TestDebugf(t *testing.T) {
|
||||
cleanup := initTestLog(t, "debug")
|
||||
defer cleanup()
|
||||
|
||||
Debugf("Processing file %s (%d bytes)", "test.txt", 1024)
|
||||
|
||||
content := getLogContent(t)
|
||||
|
||||
if !strings.Contains(content, "Processing file test.txt (1024 bytes)") {
|
||||
t.Errorf("Expected formatted debug in log, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClose tests that Close closes the log file.
|
||||
func TestClose(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test_log_*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
logFilePath = tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
if err := Init("info", logFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Close()
|
||||
|
||||
// Verify file is closed by trying to write (should fail).
|
||||
// But we can't easily test this without reopening.
|
||||
// Just verify no panic occurred.
|
||||
logFilePath = ""
|
||||
}
|
||||
|
||||
// TestCloseNil tests that Close doesn't panic when Log is nil.
|
||||
func TestCloseNil(t *testing.T) {
|
||||
Log = nil
|
||||
|
||||
// Should not panic.
|
||||
Close()
|
||||
}
|
||||
|
||||
// TestCloseStderr tests that Close doesn't close stderr.
|
||||
func TestCloseStderr(t *testing.T) {
|
||||
Log = os.Stderr
|
||||
|
||||
// Should not panic and should not close stderr.
|
||||
Close()
|
||||
|
||||
// Verify stderr is still usable.
|
||||
_, err := os.Stderr.WriteString("")
|
||||
if err != nil {
|
||||
t.Error("stderr should still be usable after Close()")
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
Reference in New Issue
Block a user