mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 11:34:03 +00:00
feat: many features, check TODO.md
This commit is contained in:
228
utils/errors.go
Normal file
228
utils/errors.go
Normal file
@@ -0,0 +1,228 @@
|
||||
// Package utils provides common utility functions.
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ErrorType represents the category of error.
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
// ErrorTypeUnknown represents an unknown error type.
|
||||
ErrorTypeUnknown ErrorType = iota
|
||||
// ErrorTypeCLI represents command-line interface errors.
|
||||
ErrorTypeCLI
|
||||
// ErrorTypeFileSystem represents file system operation errors.
|
||||
ErrorTypeFileSystem
|
||||
// ErrorTypeProcessing represents file processing errors.
|
||||
ErrorTypeProcessing
|
||||
// ErrorTypeConfiguration represents configuration errors.
|
||||
ErrorTypeConfiguration
|
||||
// ErrorTypeIO represents input/output errors.
|
||||
ErrorTypeIO
|
||||
// ErrorTypeValidation represents validation errors.
|
||||
ErrorTypeValidation
|
||||
)
|
||||
|
||||
// String returns the string representation of the error type.
|
||||
func (e ErrorType) String() string {
|
||||
switch e {
|
||||
case ErrorTypeCLI:
|
||||
return "CLI"
|
||||
case ErrorTypeFileSystem:
|
||||
return "FileSystem"
|
||||
case ErrorTypeProcessing:
|
||||
return "Processing"
|
||||
case ErrorTypeConfiguration:
|
||||
return "Configuration"
|
||||
case ErrorTypeIO:
|
||||
return "IO"
|
||||
case ErrorTypeValidation:
|
||||
return "Validation"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// StructuredError represents a structured error with type, code, and context.
|
||||
type StructuredError struct {
|
||||
Type ErrorType
|
||||
Code string
|
||||
Message string
|
||||
Cause error
|
||||
Context map[string]any
|
||||
FilePath string
|
||||
Line int
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *StructuredError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("%s [%s]: %s: %v", e.Type, e.Code, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("%s [%s]: %s", e.Type, e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause error.
|
||||
func (e *StructuredError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// WithContext adds context information to the error.
|
||||
func (e *StructuredError) WithContext(key string, value any) *StructuredError {
|
||||
if e.Context == nil {
|
||||
e.Context = make(map[string]any)
|
||||
}
|
||||
e.Context[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// WithFilePath adds file path information to the error.
|
||||
func (e *StructuredError) WithFilePath(filePath string) *StructuredError {
|
||||
e.FilePath = filePath
|
||||
return e
|
||||
}
|
||||
|
||||
// WithLine adds line number information to the error.
|
||||
func (e *StructuredError) WithLine(line int) *StructuredError {
|
||||
e.Line = line
|
||||
return e
|
||||
}
|
||||
|
||||
// NewStructuredError creates a new structured error.
|
||||
func NewStructuredError(errorType ErrorType, code, message string) *StructuredError {
|
||||
return &StructuredError{
|
||||
Type: errorType,
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NewStructuredErrorf creates a new structured error with formatted message.
|
||||
func NewStructuredErrorf(errorType ErrorType, code, format string, args ...any) *StructuredError {
|
||||
return &StructuredError{
|
||||
Type: errorType,
|
||||
Code: code,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
// WrapError wraps an existing error with structured error information.
|
||||
func WrapError(err error, errorType ErrorType, code, message string) *StructuredError {
|
||||
return &StructuredError{
|
||||
Type: errorType,
|
||||
Code: code,
|
||||
Message: message,
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
// WrapErrorf wraps an existing error with formatted message.
|
||||
func WrapErrorf(err error, errorType ErrorType, code, format string, args ...any) *StructuredError {
|
||||
return &StructuredError{
|
||||
Type: errorType,
|
||||
Code: code,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Common error codes for each type
|
||||
const (
|
||||
// CLI Error Codes
|
||||
CodeCLIMissingSource = "MISSING_SOURCE"
|
||||
CodeCLIInvalidArgs = "INVALID_ARGS"
|
||||
|
||||
// FileSystem Error Codes
|
||||
CodeFSPathResolution = "PATH_RESOLUTION"
|
||||
CodeFSPermission = "PERMISSION_DENIED"
|
||||
CodeFSNotFound = "NOT_FOUND"
|
||||
CodeFSAccess = "ACCESS_DENIED"
|
||||
|
||||
// Processing Error Codes
|
||||
CodeProcessingFileRead = "FILE_READ"
|
||||
CodeProcessingCollection = "COLLECTION"
|
||||
CodeProcessingTraversal = "TRAVERSAL"
|
||||
CodeProcessingEncode = "ENCODE"
|
||||
|
||||
// Configuration Error Codes
|
||||
CodeConfigValidation = "VALIDATION"
|
||||
CodeConfigMissing = "MISSING"
|
||||
|
||||
// IO Error Codes
|
||||
CodeIOFileCreate = "FILE_CREATE"
|
||||
CodeIOFileWrite = "FILE_WRITE"
|
||||
CodeIOEncoding = "ENCODING"
|
||||
CodeIOWrite = "WRITE"
|
||||
CodeIORead = "READ"
|
||||
CodeIOClose = "CLOSE"
|
||||
|
||||
// Validation Error Codes
|
||||
CodeValidationFormat = "FORMAT"
|
||||
CodeValidationFileType = "FILE_TYPE"
|
||||
CodeValidationSize = "SIZE_LIMIT"
|
||||
)
|
||||
|
||||
// Predefined error constructors for common error scenarios
|
||||
|
||||
// NewCLIMissingSourceError creates a CLI error for missing source argument.
|
||||
func NewCLIMissingSourceError() *StructuredError {
|
||||
return NewStructuredError(ErrorTypeCLI, CodeCLIMissingSource, "usage: gibidify -source <source_directory> [--destination <output_file>] [--format=json|yaml|markdown]")
|
||||
}
|
||||
|
||||
// NewFileSystemError creates a file system error.
|
||||
func NewFileSystemError(code, message string) *StructuredError {
|
||||
return NewStructuredError(ErrorTypeFileSystem, code, message)
|
||||
}
|
||||
|
||||
// NewProcessingError creates a processing error.
|
||||
func NewProcessingError(code, message string) *StructuredError {
|
||||
return NewStructuredError(ErrorTypeProcessing, code, message)
|
||||
}
|
||||
|
||||
// NewIOError creates an IO error.
|
||||
func NewIOError(code, message string) *StructuredError {
|
||||
return NewStructuredError(ErrorTypeIO, code, message)
|
||||
}
|
||||
|
||||
// NewValidationError creates a validation error.
|
||||
func NewValidationError(code, message string) *StructuredError {
|
||||
return NewStructuredError(ErrorTypeValidation, code, message)
|
||||
}
|
||||
|
||||
// LogError logs an error with a consistent format if the error is not nil.
|
||||
// The operation parameter describes what was being attempted.
|
||||
// Additional context can be provided via the args parameter.
|
||||
func LogError(operation string, err error, args ...any) {
|
||||
if err != nil {
|
||||
msg := operation
|
||||
if len(args) > 0 {
|
||||
// Format the operation string with the provided arguments
|
||||
msg = fmt.Sprintf(operation, args...)
|
||||
}
|
||||
|
||||
// Check if it's a structured error and log with additional context
|
||||
if structErr, ok := err.(*StructuredError); ok {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error_type": structErr.Type.String(),
|
||||
"error_code": structErr.Code,
|
||||
"context": structErr.Context,
|
||||
"file_path": structErr.FilePath,
|
||||
"line": structErr.Line,
|
||||
}).Errorf("%s: %v", msg, err)
|
||||
} else {
|
||||
logrus.Errorf("%s: %v", msg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogErrorf logs an error with a formatted message if the error is not nil.
|
||||
// This is a convenience wrapper around LogError for cases where formatting is needed.
|
||||
func LogErrorf(err error, format string, args ...any) {
|
||||
if err != nil {
|
||||
LogError(format, err, args...)
|
||||
}
|
||||
}
|
||||
242
utils/errors_test.go
Normal file
242
utils/errors_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// captureLogOutput captures logrus output for testing
|
||||
func captureLogOutput(f func()) string {
|
||||
var buf bytes.Buffer
|
||||
logrus.SetOutput(&buf)
|
||||
defer logrus.SetOutput(logrus.StandardLogger().Out)
|
||||
f()
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestLogError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
operation string
|
||||
err error
|
||||
args []any
|
||||
wantLog string
|
||||
wantEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "nil error should not log",
|
||||
operation: "test operation",
|
||||
err: nil,
|
||||
args: nil,
|
||||
wantEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "basic error logging",
|
||||
operation: "failed to read file",
|
||||
err: errors.New("permission denied"),
|
||||
args: nil,
|
||||
wantLog: "failed to read file: permission denied",
|
||||
},
|
||||
{
|
||||
name: "error with formatting args",
|
||||
operation: "failed to process file %s",
|
||||
err: errors.New("file too large"),
|
||||
args: []any{"test.txt"},
|
||||
wantLog: "failed to process file test.txt: file too large",
|
||||
},
|
||||
{
|
||||
name: "error with multiple formatting args",
|
||||
operation: "failed to copy from %s to %s",
|
||||
err: errors.New("disk full"),
|
||||
args: []any{"source.txt", "dest.txt"},
|
||||
wantLog: "failed to copy from source.txt to dest.txt: disk full",
|
||||
},
|
||||
{
|
||||
name: "wrapped error",
|
||||
operation: "database operation failed",
|
||||
err: fmt.Errorf("connection error: %w", errors.New("timeout")),
|
||||
args: nil,
|
||||
wantLog: "database operation failed: connection error: timeout",
|
||||
},
|
||||
{
|
||||
name: "empty operation string",
|
||||
operation: "",
|
||||
err: errors.New("some error"),
|
||||
args: nil,
|
||||
wantLog: ": some error",
|
||||
},
|
||||
{
|
||||
name: "operation with percentage sign",
|
||||
operation: "processing 50% complete",
|
||||
err: errors.New("interrupted"),
|
||||
args: nil,
|
||||
wantLog: "processing 50% complete: interrupted",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := captureLogOutput(func() {
|
||||
LogError(tt.operation, tt.err, tt.args...)
|
||||
})
|
||||
|
||||
if tt.wantEmpty {
|
||||
if output != "" {
|
||||
t.Errorf("LogError() logged output when error was nil: %q", output)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(output, tt.wantLog) {
|
||||
t.Errorf("LogError() output = %q, want to contain %q", output, tt.wantLog)
|
||||
}
|
||||
|
||||
// Verify it's logged at ERROR level
|
||||
if !strings.Contains(output, "level=error") {
|
||||
t.Errorf("LogError() should log at ERROR level, got: %q", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
format string
|
||||
args []any
|
||||
wantLog string
|
||||
wantEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "nil error should not log",
|
||||
err: nil,
|
||||
format: "operation %s failed",
|
||||
args: []any{"test"},
|
||||
wantEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "basic formatted error",
|
||||
err: errors.New("not found"),
|
||||
format: "file %s not found",
|
||||
args: []any{"config.yaml"},
|
||||
wantLog: "file config.yaml not found: not found",
|
||||
},
|
||||
{
|
||||
name: "multiple format arguments",
|
||||
err: errors.New("invalid range"),
|
||||
format: "value %d is not between %d and %d",
|
||||
args: []any{150, 0, 100},
|
||||
wantLog: "value 150 is not between 0 and 100: invalid range",
|
||||
},
|
||||
{
|
||||
name: "no format arguments",
|
||||
err: errors.New("generic error"),
|
||||
format: "operation failed",
|
||||
args: nil,
|
||||
wantLog: "operation failed: generic error",
|
||||
},
|
||||
{
|
||||
name: "format with different types",
|
||||
err: errors.New("type mismatch"),
|
||||
format: "expected %s but got %d",
|
||||
args: []any{"string", 42},
|
||||
wantLog: "expected string but got 42: type mismatch",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := captureLogOutput(func() {
|
||||
LogErrorf(tt.err, tt.format, tt.args...)
|
||||
})
|
||||
|
||||
if tt.wantEmpty {
|
||||
if output != "" {
|
||||
t.Errorf("LogErrorf() logged output when error was nil: %q", output)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(output, tt.wantLog) {
|
||||
t.Errorf("LogErrorf() output = %q, want to contain %q", output, tt.wantLog)
|
||||
}
|
||||
|
||||
// Verify it's logged at ERROR level
|
||||
if !strings.Contains(output, "level=error") {
|
||||
t.Errorf("LogErrorf() should log at ERROR level, got: %q", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogErrorConcurrency(t *testing.T) {
|
||||
// Test that LogError is safe for concurrent use
|
||||
done := make(chan bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(n int) {
|
||||
LogError("concurrent operation", fmt.Errorf("error %d", n))
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogErrorfConcurrency(t *testing.T) {
|
||||
// Test that LogErrorf is safe for concurrent use
|
||||
done := make(chan bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(n int) {
|
||||
LogErrorf(fmt.Errorf("error %d", n), "concurrent operation %d", n)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLogError benchmarks the LogError function
|
||||
func BenchmarkLogError(b *testing.B) {
|
||||
err := errors.New("benchmark error")
|
||||
// Disable output during benchmark
|
||||
logrus.SetOutput(bytes.NewBuffer(nil))
|
||||
defer logrus.SetOutput(logrus.StandardLogger().Out)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LogError("benchmark operation", err)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLogErrorf benchmarks the LogErrorf function
|
||||
func BenchmarkLogErrorf(b *testing.B) {
|
||||
err := errors.New("benchmark error")
|
||||
// Disable output during benchmark
|
||||
logrus.SetOutput(bytes.NewBuffer(nil))
|
||||
defer logrus.SetOutput(logrus.StandardLogger().Out)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LogErrorf(err, "benchmark operation %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLogErrorNil benchmarks LogError with nil error (no-op case)
|
||||
func BenchmarkLogErrorNil(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LogError("benchmark operation", nil)
|
||||
}
|
||||
}
|
||||
26
utils/paths.go
Normal file
26
utils/paths.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Package utils provides common utility functions.
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// GetAbsolutePath returns the absolute path for the given path.
|
||||
// It wraps filepath.Abs with consistent error handling.
|
||||
func GetAbsolutePath(path string) (string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path for %s: %w", path, err)
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
// GetBaseName returns the base name for the given path, handling special cases.
|
||||
func GetBaseName(absPath string) string {
|
||||
baseName := filepath.Base(absPath)
|
||||
if baseName == "." || baseName == "" {
|
||||
return "output"
|
||||
}
|
||||
return baseName
|
||||
}
|
||||
262
utils/paths_test.go
Normal file
262
utils/paths_test.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetAbsolutePath(t *testing.T) {
|
||||
// Get current working directory for tests
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantPrefix string
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
skipWindows bool
|
||||
}{
|
||||
{
|
||||
name: "absolute path unchanged",
|
||||
path: cwd,
|
||||
wantPrefix: cwd,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "relative path current directory",
|
||||
path: ".",
|
||||
wantPrefix: cwd,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "relative path parent directory",
|
||||
path: "..",
|
||||
wantPrefix: filepath.Dir(cwd),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "relative path with file",
|
||||
path: "test.txt",
|
||||
wantPrefix: filepath.Join(cwd, "test.txt"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "relative path with subdirectory",
|
||||
path: "subdir/file.go",
|
||||
wantPrefix: filepath.Join(cwd, "subdir", "file.go"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
path: "",
|
||||
wantPrefix: cwd,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "path with tilde",
|
||||
path: "~/test",
|
||||
wantPrefix: filepath.Join(cwd, "~", "test"),
|
||||
wantErr: false,
|
||||
skipWindows: false,
|
||||
},
|
||||
{
|
||||
name: "path with multiple separators",
|
||||
path: "path//to///file",
|
||||
wantPrefix: filepath.Join(cwd, "path", "to", "file"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "path with trailing separator",
|
||||
path: "path/",
|
||||
wantPrefix: filepath.Join(cwd, "path"),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.skipWindows && runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping test on Windows")
|
||||
}
|
||||
|
||||
got, err := GetAbsolutePath(tt.path)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("GetAbsolutePath() error = nil, wantErr %v", tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErrMsg != "" && !strings.Contains(err.Error(), tt.wantErrMsg) {
|
||||
t.Errorf("GetAbsolutePath() error = %v, want error containing %v", err, tt.wantErrMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetAbsolutePath() unexpected error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Clean the expected path for comparison
|
||||
wantClean := filepath.Clean(tt.wantPrefix)
|
||||
gotClean := filepath.Clean(got)
|
||||
|
||||
if gotClean != wantClean {
|
||||
t.Errorf("GetAbsolutePath() = %v, want %v", gotClean, wantClean)
|
||||
}
|
||||
|
||||
// Verify the result is actually absolute
|
||||
if !filepath.IsAbs(got) {
|
||||
t.Errorf("GetAbsolutePath() returned non-absolute path: %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAbsolutePathSpecialCases(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping Unix-specific tests on Windows")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() (string, func())
|
||||
path string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "symlink to directory",
|
||||
setup: func() (string, func()) {
|
||||
tmpDir := t.TempDir()
|
||||
target := filepath.Join(tmpDir, "target")
|
||||
link := filepath.Join(tmpDir, "link")
|
||||
|
||||
if err := os.Mkdir(target, 0o755); err != nil {
|
||||
t.Fatalf("Failed to create target directory: %v", err)
|
||||
}
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("Failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
return link, func() {}
|
||||
},
|
||||
path: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken symlink",
|
||||
setup: func() (string, func()) {
|
||||
tmpDir := t.TempDir()
|
||||
link := filepath.Join(tmpDir, "broken_link")
|
||||
|
||||
if err := os.Symlink("/nonexistent/path", link); err != nil {
|
||||
t.Fatalf("Failed to create broken symlink: %v", err)
|
||||
}
|
||||
|
||||
return link, func() {}
|
||||
},
|
||||
path: "",
|
||||
wantErr: false, // filepath.Abs still works with broken symlinks
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path, cleanup := tt.setup()
|
||||
defer cleanup()
|
||||
|
||||
if tt.path == "" {
|
||||
tt.path = path
|
||||
}
|
||||
|
||||
got, err := GetAbsolutePath(tt.path)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetAbsolutePath() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil && !filepath.IsAbs(got) {
|
||||
t.Errorf("GetAbsolutePath() returned non-absolute path: %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAbsolutePathConcurrency(t *testing.T) {
|
||||
// Test that GetAbsolutePath is safe for concurrent use
|
||||
paths := []string{".", "..", "test.go", "subdir/file.txt", "/tmp/test"}
|
||||
done := make(chan bool)
|
||||
|
||||
for _, p := range paths {
|
||||
go func(path string) {
|
||||
_, _ = GetAbsolutePath(path)
|
||||
done <- true
|
||||
}(p)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for range paths {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAbsolutePathErrorFormatting(t *testing.T) {
|
||||
// This test verifies error message formatting
|
||||
// We need to trigger an actual error from filepath.Abs
|
||||
// On Unix systems, we can't easily trigger filepath.Abs errors
|
||||
// so we'll just verify the error wrapping works correctly
|
||||
|
||||
// Create a test that would fail if filepath.Abs returns an error
|
||||
path := "test/path"
|
||||
got, err := GetAbsolutePath(path)
|
||||
if err != nil {
|
||||
// If we somehow get an error, verify it's properly formatted
|
||||
if !strings.Contains(err.Error(), "failed to get absolute path for") {
|
||||
t.Errorf("Error message format incorrect: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), path) {
|
||||
t.Errorf("Error message should contain original path: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Normal case - just verify we got a valid absolute path
|
||||
if !filepath.IsAbs(got) {
|
||||
t.Errorf("Expected absolute path, got: %v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetAbsolutePath benchmarks the GetAbsolutePath function
|
||||
func BenchmarkGetAbsolutePath(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = GetAbsolutePath("test/path/file.go")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetAbsolutePathAbs benchmarks with already absolute path
|
||||
func BenchmarkGetAbsolutePathAbs(b *testing.B) {
|
||||
absPath := "/home/user/test/file.go"
|
||||
if runtime.GOOS == "windows" {
|
||||
absPath = "C:\\Users\\test\\file.go"
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = GetAbsolutePath(absPath)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetAbsolutePathCurrent benchmarks with current directory
|
||||
func BenchmarkGetAbsolutePathCurrent(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = GetAbsolutePath(".")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user