Remove some unused shit and write tests for coverage

This commit is contained in:
2025-12-19 12:12:42 +01:00
parent 1df0263a42
commit da5b621cb6
19 changed files with 1892 additions and 390 deletions

View File

@@ -2,7 +2,6 @@ package utils
import (
"os"
"strconv"
"strings"
logger "git.site.quack-lab.dev/dave/cylogger"
@@ -11,16 +10,6 @@ import (
// fileLogger is a scoped logger for the utils/file package.
var fileLogger = logger.Default.WithPrefix("utils/file")
func CleanPath(path string) string {
// Use the centralized ResolvePath function
return ResolvePath(path)
}
func ToAbs(path string) string {
// Use the centralized ResolvePath function
return ResolvePath(path)
}
// LimitString truncates a string to maxLen and adds "..." if truncated
func LimitString(s string, maxLen int) string {
limitStringLogger := fileLogger.WithPrefix("LimitString").WithField("originalLength", len(s)).WithField("maxLength", maxLen)
@@ -35,19 +24,6 @@ func LimitString(s string, maxLen int) string {
return limited
}
// StrToFloat converts a string to a float64, returning 0 on error.
func StrToFloat(s string) float64 {
strToFloatLogger := fileLogger.WithPrefix("StrToFloat").WithField("inputString", s)
strToFloatLogger.Debug("Attempting to convert string to float")
f, err := strconv.ParseFloat(s, 64)
if err != nil {
strToFloatLogger.Warning("Failed to convert string %q to float, returning 0: %v", s, err)
return 0
}
strToFloatLogger.Trace("Successfully converted %q to float: %f", s, f)
return f
}
func ResetWhereNecessary(associations map[string]FileCommandAssociation, db DB) error {
resetWhereNecessaryLogger := fileLogger.WithPrefix("ResetWhereNecessary")
resetWhereNecessaryLogger.Debug("Starting reset where necessary operation")

209
utils/file_test.go Normal file
View File

@@ -0,0 +1,209 @@
package utils
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLimitString(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
expected string
}{
{
name: "Short string",
input: "hello",
maxLen: 10,
expected: "hello",
},
{
name: "Exact length",
input: "hello",
maxLen: 5,
expected: "hello",
},
{
name: "Too long",
input: "hello world",
maxLen: 8,
expected: "hello...",
},
{
name: "With newlines",
input: "hello\nworld",
maxLen: 20,
expected: "hello\\nworld",
},
{
name: "With newlines truncated",
input: "hello\nworld\nfoo\nbar",
maxLen: 15,
expected: "hello\\nworld...",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := LimitString(tt.input, tt.maxLen)
assert.Equal(t, tt.expected, result)
})
}
}
func TestResetWhereNecessary(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "reset-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create test files
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
file3 := filepath.Join(tmpDir, "file3.txt")
err = os.WriteFile(file1, []byte("original1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("original2"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file3, []byte("original3"), 0644)
assert.NoError(t, err)
// Modify files
err = os.WriteFile(file1, []byte("modified1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("modified2"), 0644)
assert.NoError(t, err)
// Create mock DB
db, err := GetDB()
assert.NoError(t, err)
err = db.SaveFile(file1, []byte("original1"))
assert.NoError(t, err)
err = db.SaveFile(file2, []byte("original2"))
assert.NoError(t, err)
// file3 not in DB
// Create associations with reset commands
associations := map[string]FileCommandAssociation{
file1: {
File: file1,
Commands: []ModifyCommand{
{Name: "cmd1", Reset: true},
},
},
file2: {
File: file2,
IsolateCommands: []ModifyCommand{
{Name: "cmd2", Reset: true},
},
},
file3: {
File: file3,
Commands: []ModifyCommand{
{Name: "cmd3", Reset: false}, // No reset
},
},
}
// Run reset
err = ResetWhereNecessary(associations, db)
assert.NoError(t, err)
// Verify file1 was reset
data, _ := os.ReadFile(file1)
assert.Equal(t, "original1", string(data))
// Verify file2 was reset
data, _ = os.ReadFile(file2)
assert.Equal(t, "original2", string(data))
// Verify file3 was NOT reset
data, _ = os.ReadFile(file3)
assert.Equal(t, "original3", string(data))
}
func TestResetWhereNecessaryMissingFromDB(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "reset-missing-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a test file that's been modified
file1 := filepath.Join(tmpDir, "file1.txt")
err = os.WriteFile(file1, []byte("modified_content"), 0644)
assert.NoError(t, err)
// Create DB but DON'T save file to it
db, err := GetDB()
assert.NoError(t, err)
// Create associations with reset command
associations := map[string]FileCommandAssociation{
file1: {
File: file1,
Commands: []ModifyCommand{
{Name: "cmd1", Reset: true},
},
},
}
// Run reset - should use current disk content as fallback
err = ResetWhereNecessary(associations, db)
assert.NoError(t, err)
// Verify file was "reset" to current content (saved to DB for next time)
data, _ := os.ReadFile(file1)
assert.Equal(t, "modified_content", string(data))
// Verify it was saved to DB
savedData, err := db.GetFile(file1)
assert.NoError(t, err)
assert.Equal(t, "modified_content", string(savedData))
}
func TestResetAllFiles(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "reset-all-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create test files
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
err = os.WriteFile(file1, []byte("original1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("original2"), 0644)
assert.NoError(t, err)
// Create mock DB and save originals
db, err := GetDB()
assert.NoError(t, err)
err = db.SaveFile(file1, []byte("original1"))
assert.NoError(t, err)
err = db.SaveFile(file2, []byte("original2"))
assert.NoError(t, err)
// Modify files
err = os.WriteFile(file1, []byte("modified1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("modified2"), 0644)
assert.NoError(t, err)
// Verify they're modified
data, _ := os.ReadFile(file1)
assert.Equal(t, "modified1", string(data))
// Reset all
err = ResetAllFiles(db)
assert.NoError(t, err)
// Verify both were reset
data, _ = os.ReadFile(file1)
assert.Equal(t, "original1", string(data))
data, _ = os.ReadFile(file2)
assert.Equal(t, "original2", string(data))
}

View File

@@ -86,28 +86,17 @@ func SplitPattern(pattern string) (string, string) {
splitPatternLogger.Debug("Splitting pattern")
splitPatternLogger.Trace("Original pattern: %q", pattern)
// Resolve the pattern first to handle ~ expansion and make it absolute
resolvedPattern := ResolvePath(pattern)
splitPatternLogger.Trace("Resolved pattern: %q", resolvedPattern)
// Split the pattern first to separate static and wildcard parts
static, remainingPattern := doublestar.SplitPattern(pattern)
splitPatternLogger.Trace("After split: static=%q, pattern=%q", static, remainingPattern)
static, pattern := doublestar.SplitPattern(resolvedPattern)
// Resolve the static part to handle ~ expansion and make it absolute
// ResolvePath already normalizes to forward slashes
static = ResolvePath(static)
splitPatternLogger.Trace("Resolved static part: %q", static)
// Ensure static part is properly resolved
if static == "" {
cwd, err := os.Getwd()
if err != nil {
splitPatternLogger.Error("Error getting current working directory: %v", err)
return "", ""
}
static = cwd
splitPatternLogger.Debug("Static part is empty, defaulting to current working directory: %q", static)
} else {
// Static part should already be resolved by ResolvePath
static = strings.ReplaceAll(static, "\\", "/")
}
splitPatternLogger.Trace("Final static path: %q, Remaining pattern: %q", static, pattern)
return static, pattern
splitPatternLogger.Trace("Final static path: %q, Remaining pattern: %q", static, remainingPattern)
return static, remainingPattern
}
type FileCommandAssociation struct {
@@ -140,7 +129,7 @@ func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[s
static, pattern := SplitPattern(glob)
associateFilesLogger.Trace("Glob parts for %q → static=%q pattern=%q", glob, static, pattern)
// Use resolved file for matching
// Use resolved file for matching (already normalized to forward slashes by ResolvePath)
absFile := resolvedFile
associateFilesLogger.Trace("Absolute file path resolved for matching: %q", absFile)
@@ -283,9 +272,6 @@ func LoadCommands(args []string) ([]ModifyCommand, map[string]interface{}, error
loadCommandsLogger.Error("Failed to load TOML commands from argument %q: %v", arg, err)
return nil, nil, fmt.Errorf("failed to load commands from TOML files: %w", err)
}
for k, v := range newVariables {
variables[k] = v
}
} else {
// Default to YAML for .yml, .yaml, or any other extension
loadCommandsLogger.Debug("Loading YAML commands from %q", arg)
@@ -294,9 +280,9 @@ func LoadCommands(args []string) ([]ModifyCommand, map[string]interface{}, error
loadCommandsLogger.Error("Failed to load YAML commands from argument %q: %v", arg, err)
return nil, nil, fmt.Errorf("failed to load commands from cook files: %w", err)
}
for k, v := range newVariables {
variables[k] = v
}
}
for k, v := range newVariables {
variables[k] = v
}
loadCommandsLogger.Debug("Successfully loaded %d commands from %q", len(newCommands), arg)
@@ -485,24 +471,8 @@ func LoadCommandsFromTomlFile(tomlFileData []byte) ([]ModifyCommand, map[string]
}
}
// If we found commands in the wrapped structure, use those
if len(tomlData.Commands) > 0 {
commands = tomlData.Commands
loadTomlCommandLogger.Debug("Found %d commands in wrapped TOML structure", len(commands))
} else {
// Try to parse as direct array (similar to YAML format)
directCommands := []ModifyCommand{}
err = toml.Unmarshal(tomlFileData, &directCommands)
if err != nil {
loadTomlCommandLogger.Error("Failed to unmarshal TOML file data as direct array: %v", err)
return nil, nil, fmt.Errorf("failed to unmarshal TOML file as direct array: %w", err)
}
if len(directCommands) > 0 {
commands = directCommands
loadTomlCommandLogger.Debug("Found %d commands in direct TOML array", len(directCommands))
}
}
// Use commands from wrapped structure
commands = tomlData.Commands
loadTomlCommandLogger.Debug("Successfully unmarshaled %d commands and %d variables", len(commands), len(variables))
loadTomlCommandLogger.Trace("Unmarshaled commands: %v", commands)
loadTomlCommandLogger.Trace("Unmarshaled variables: %v", variables)

View File

@@ -0,0 +1,313 @@
package utils
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAggregateGlobsWithDuplicates(t *testing.T) {
commands := []ModifyCommand{
{Files: []string{"*.txt", "*.md"}},
{Files: []string{"*.txt", "*.go"}}, // *.txt is duplicate
{Files: []string{"test/**/*.xml"}},
}
globs := AggregateGlobs(commands)
// Should deduplicate
assert.Equal(t, 4, len(globs))
// AggregateGlobs resolves paths, which uses forward slashes internally
assert.Contains(t, globs, ResolvePath("*.txt"))
assert.Contains(t, globs, ResolvePath("*.md"))
assert.Contains(t, globs, ResolvePath("*.go"))
assert.Contains(t, globs, ResolvePath("test/**/*.xml"))
}
func TestExpandGlobsWithActualFiles(t *testing.T) {
// Create temp dir with test files
tmpDir, err := os.MkdirTemp("", "glob-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create test files
testFile1 := filepath.Join(tmpDir, "test1.txt")
testFile2 := filepath.Join(tmpDir, "test2.txt")
testFile3 := filepath.Join(tmpDir, "test.md")
os.WriteFile(testFile1, []byte("test"), 0644)
os.WriteFile(testFile2, []byte("test"), 0644)
os.WriteFile(testFile3, []byte("test"), 0644)
// Change to temp directory so glob pattern can find files
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
// Test expanding globs using ResolvePath to normalize the pattern
globs := map[string]struct{}{
ResolvePath("*.txt"): {},
}
files, err := ExpandGlobs(globs)
assert.NoError(t, err)
assert.Equal(t, 2, len(files))
}
func TestSplitPatternWithTilde(t *testing.T) {
pattern := "~/test/*.txt"
static, pat := SplitPattern(pattern)
// Should expand ~
assert.NotEqual(t, "~", static)
assert.Contains(t, pat, "*.txt")
}
func TestLoadCommandsWithDisabled(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "disabled-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
yamlContent := `
variables:
test: "value"
commands:
- name: "enabled_cmd"
regex: "test"
lua: "v1 * 2"
files: ["*.txt"]
- name: "disabled_cmd"
regex: "test2"
lua: "v1 * 3"
files: ["*.txt"]
disable: true
`
yamlFile := filepath.Join(tmpDir, "test.yml")
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
// Change to temp directory so LoadCommands can find the file with a simple pattern
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
commands, variables, err := LoadCommands([]string{"test.yml"})
assert.NoError(t, err)
// Should only load enabled command
assert.Equal(t, 1, len(commands))
assert.Equal(t, "enabled_cmd", commands[0].Name)
// Should still load variables
assert.Equal(t, 1, len(variables))
}
func TestFilterCommandsByName(t *testing.T) {
commands := []ModifyCommand{
{Name: "test_multiply"},
{Name: "test_divide"},
{Name: "other_command"},
{Name: "test_add"},
}
// Filter by "test"
filtered := FilterCommands(commands, "test")
assert.Equal(t, 3, len(filtered))
// Filter by multiple
filtered = FilterCommands(commands, "multiply,divide")
assert.Equal(t, 2, len(filtered))
}
func TestCountGlobsBeforeDedup(t *testing.T) {
commands := []ModifyCommand{
{Files: []string{"*.txt", "*.md", "*.go"}},
{Files: []string{"*.xml"}},
{Files: []string{"test/**/*.txt", "data/**/*.json"}},
}
count := CountGlobsBeforeDedup(commands)
assert.Equal(t, 6, count)
}
func TestMatchesWithMemoization(t *testing.T) {
path := "test/file.txt"
glob := "**/*.txt"
// First call
matches1, err1 := Matches(path, glob)
assert.NoError(t, err1)
assert.True(t, matches1)
// Second call should use memo
matches2, err2 := Matches(path, glob)
assert.NoError(t, err2)
assert.Equal(t, matches1, matches2)
}
func TestValidateCommand(t *testing.T) {
tests := []struct {
name string
cmd ModifyCommand
wantErr bool
}{
{
name: "Valid command",
cmd: ModifyCommand{
Regex: "test",
Lua: "v1 * 2",
Files: []string{"*.txt"},
},
wantErr: false,
},
{
name: "Valid JSON mode without regex",
cmd: ModifyCommand{
JSON: true,
Lua: "data.value = data.value * 2; modified = true",
Files: []string{"*.json"},
},
wantErr: false,
},
{
name: "Missing regex in non-JSON mode",
cmd: ModifyCommand{
Lua: "v1 * 2",
Files: []string{"*.txt"},
},
wantErr: true,
},
{
name: "Missing Lua",
cmd: ModifyCommand{
Regex: "test",
Files: []string{"*.txt"},
},
wantErr: true,
},
{
name: "Missing files",
cmd: ModifyCommand{
Regex: "test",
Lua: "v1 * 2",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cmd.Validate()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestLoadCommandsFromTomlWithVariables(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "toml-vars-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
tomlContent := `[variables]
multiplier = 3
prefix = "PREFIX_"
[[commands]]
name = "test_cmd"
regex = "value = !num"
lua = "v1 * multiplier"
files = ["*.txt"]
`
tomlFile := filepath.Join(tmpDir, "test.toml")
err = os.WriteFile(tomlFile, []byte(tomlContent), 0644)
assert.NoError(t, err)
// Change to temp directory so glob pattern can find the file
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
commands, variables, err := LoadCommandsFromTomlFiles("test.toml")
assert.NoError(t, err)
assert.Equal(t, 1, len(commands))
assert.Equal(t, 2, len(variables))
assert.Equal(t, int64(3), variables["multiplier"])
assert.Equal(t, "PREFIX_", variables["prefix"])
}
func TestConvertYAMLToTOMLSkipExisting(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "convert-skip-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create YAML file
yamlContent := `
commands:
- name: "test"
regex: "value"
lua: "v1 * 2"
files: ["*.txt"]
`
yamlFile := filepath.Join(tmpDir, "test.yml")
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
// Create TOML file (should skip conversion)
tomlFile := filepath.Join(tmpDir, "test.toml")
err = os.WriteFile(tomlFile, []byte("# existing"), 0644)
assert.NoError(t, err)
// Change to temp dir
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
// Should skip existing TOML
err = ConvertYAMLToTOML("test.yml")
assert.NoError(t, err)
// TOML content should be unchanged
content, _ := os.ReadFile(tomlFile)
assert.Equal(t, "# existing", string(content))
}
func TestLoadCommandsWithTomlExtension(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "toml-ext-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
tomlContent := `
[variables]
test_var = "value"
[[commands]]
name = "TestCmd"
regex = "test"
lua = "return true"
files = ["*.txt"]
`
tomlFile := filepath.Join(tmpDir, "test.toml")
err = os.WriteFile(tomlFile, []byte(tomlContent), 0644)
assert.NoError(t, err)
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
// This should trigger the .toml suffix check in LoadCommands
commands, variables, err := LoadCommands([]string{"test.toml"})
assert.NoError(t, err)
assert.Len(t, commands, 1)
assert.Equal(t, "TestCmd", commands[0].Name)
assert.Len(t, variables, 1)
assert.Equal(t, "value", variables["test_var"])
}

View File

@@ -0,0 +1,93 @@
package utils
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
// TestConvertYAMLToTOMLReadError tests error handling when YAML file can't be read
func TestConvertYAMLToTOMLReadError(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "convert-read-error-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create YAML file with no read permissions (on Unix) or delete it after creation
yamlFile := filepath.Join(tmpDir, "test.yml")
err = os.WriteFile(yamlFile, []byte("commands:\n - name: test\n"), 0000)
assert.NoError(t, err)
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
// This should fail to read but not crash
err = ConvertYAMLToTOML("test.yml")
// Function continues on error, doesn't return error
assert.NoError(t, err)
// Fix permissions for cleanup
os.Chmod(yamlFile, 0644)
}
// TestConvertYAMLToTOMLParseError tests error handling when YAML is invalid
func TestConvertYAMLToTOMLParseError(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "convert-parse-error-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create invalid YAML
yamlFile := filepath.Join(tmpDir, "invalid.yml")
err = os.WriteFile(yamlFile, []byte("commands:\n - [this is not valid yaml}}"), 0644)
assert.NoError(t, err)
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
// This should fail to parse but not crash
err = ConvertYAMLToTOML("invalid.yml")
assert.NoError(t, err)
// TOML file should not exist
_, statErr := os.Stat(filepath.Join(tmpDir, "invalid.toml"))
assert.True(t, os.IsNotExist(statErr))
}
// TestConvertYAMLToTOMLWriteError tests error handling when TOML file can't be written
func TestConvertYAMLToTOMLWriteError(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("Skipping write permission test in CI")
}
tmpDir, err := os.MkdirTemp("", "convert-write-error-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create valid YAML
yamlFile := filepath.Join(tmpDir, "test.yml")
err = os.WriteFile(yamlFile, []byte("commands:\n - name: test\n regex: test\n lua: v1\n files: [test.txt]\n"), 0644)
assert.NoError(t, err)
// Create output directory with no write permissions
outputDir := filepath.Join(tmpDir, "readonly")
err = os.Mkdir(outputDir, 0555)
assert.NoError(t, err)
defer os.Chmod(outputDir, 0755) // Fix for cleanup
origDir, _ := os.Getwd()
defer os.Chdir(origDir)
os.Chdir(tmpDir)
// Move YAML into readonly dir
newYamlFile := filepath.Join(outputDir, "test.yml")
os.Rename(yamlFile, newYamlFile)
os.Chdir(outputDir)
// This should fail to write but not crash
err = ConvertYAMLToTOML("test.yml")
assert.NoError(t, err)
}

View File

@@ -3,7 +3,6 @@ package utils
import (
"os"
"path/filepath"
"runtime"
"strings"
logger "git.site.quack-lab.dev/dave/cylogger"
@@ -12,93 +11,69 @@ import (
// pathLogger is a scoped logger for the utils/path package.
var pathLogger = logger.Default.WithPrefix("utils/path")
// ResolvePath resolves a file path by:
// 1. Expanding ~ to the user's home directory
// 2. Making the path absolute if it's relative
// 3. Normalizing path separators to forward slashes
// 4. Cleaning the path
// ResolvePath resolves a path to an absolute path, handling ~ expansion and cleaning
func ResolvePath(path string) string {
resolvePathLogger := pathLogger.WithPrefix("ResolvePath").WithField("inputPath", path)
resolvePathLogger.Debug("Resolving path")
resolvePathLogger.Trace("Resolving path: %q", path)
// Handle empty path
if path == "" {
resolvePathLogger.Warning("Empty path provided")
resolvePathLogger.Trace("Empty path, returning empty string")
return ""
}
// Step 1: Expand ~ to home directory
originalPath := path
// Check if path is absolute
if filepath.IsAbs(path) {
resolvePathLogger.Trace("Path is already absolute: %q", path)
cleaned := filepath.ToSlash(filepath.Clean(path))
resolvePathLogger.Trace("Cleaned absolute path: %q", cleaned)
return cleaned
}
// Handle ~ expansion
if strings.HasPrefix(path, "~") {
home := os.Getenv("HOME")
if home == "" {
// Fallback for Windows
if runtime.GOOS == "windows" {
home = os.Getenv("USERPROFILE")
}
}
if home != "" {
if path == "~" {
path = home
} else if strings.HasPrefix(path, "~/") {
path = filepath.Join(home, path[2:])
} else {
// Handle cases like ~username
// For now, just replace ~ with home directory
path = strings.Replace(path, "~", home, 1)
}
resolvePathLogger.Debug("Expanded tilde to home directory: home=%s, result=%s", home, path)
homeDir, _ := os.UserHomeDir()
if strings.HasPrefix(path, "~/") || strings.HasPrefix(path, "~\\") {
path = filepath.Join(homeDir, path[2:])
} else if path == "~" {
path = homeDir
} else {
resolvePathLogger.Warning("Could not determine home directory for tilde expansion")
// ~something (like ~~), treat first ~ as home expansion, rest as literal
path = homeDir + path[1:]
}
resolvePathLogger.Trace("Expanded ~ to home directory: %q", path)
}
// Step 2: Make path absolute if it's not already
// Make absolute if not already
if !filepath.IsAbs(path) {
cwd, err := os.Getwd()
absPath, err := filepath.Abs(path)
if err != nil {
resolvePathLogger.Error("Failed to get current working directory: %v", err)
return path // Return as-is if we can't get CWD
resolvePathLogger.Error("Failed to get absolute path: %v", err)
return filepath.ToSlash(filepath.Clean(path))
}
path = filepath.Join(cwd, path)
resolvePathLogger.Debug("Made relative path absolute: cwd=%s, result=%s", cwd, path)
resolvePathLogger.Trace("Made path absolute: %q -> %q", path, absPath)
path = absPath
}
// Step 3: Clean the path
path = filepath.Clean(path)
resolvePathLogger.Debug("Cleaned path: result=%s", path)
// Step 4: Normalize path separators to forward slashes for consistency
path = strings.ReplaceAll(path, "\\", "/")
resolvePathLogger.Debug("Final resolved path: original=%s, final=%s", originalPath, path)
return path
}
// ResolvePathForLogging is the same as ResolvePath but includes more detailed logging
// for debugging purposes
func ResolvePathForLogging(path string) string {
return ResolvePath(path)
}
// IsAbsolutePath checks if a path is absolute (including tilde expansion)
func IsAbsolutePath(path string) bool {
// Check for tilde expansion first
if strings.HasPrefix(path, "~") {
return true // Tilde paths become absolute after expansion
}
return filepath.IsAbs(path)
// Clean the path and normalize to forward slashes for consistency
cleaned := filepath.ToSlash(filepath.Clean(path))
resolvePathLogger.Trace("Final cleaned path: %q", cleaned)
return cleaned
}
// GetRelativePath returns the relative path from base to target
func GetRelativePath(base, target string) (string, error) {
resolvedBase := ResolvePath(base)
resolvedTarget := ResolvePath(target)
getRelativePathLogger := pathLogger.WithPrefix("GetRelativePath")
getRelativePathLogger.Debug("Getting relative path from %q to %q", base, target)
relPath, err := filepath.Rel(resolvedBase, resolvedTarget)
relPath, err := filepath.Rel(base, target)
if err != nil {
getRelativePathLogger.Error("Failed to get relative path: %v", err)
return "", err
}
// Normalize to forward slashes
return strings.ReplaceAll(relPath, "\\", "/"), nil
}
// Use forward slashes for consistency
relPath = filepath.ToSlash(relPath)
getRelativePathLogger.Debug("Relative path: %q", relPath)
return relPath, nil
}

View File

@@ -224,52 +224,6 @@ func TestResolvePathComplexTilde(t *testing.T) {
}
}
func TestIsAbsolutePath(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{
name: "Empty path",
input: "",
expected: false,
},
{
name: "Absolute Unix path",
input: "/absolute/path",
expected: func() bool {
if runtime.GOOS == "windows" {
// On Windows, paths starting with / are not considered absolute
return false
}
return true
}(),
},
{
name: "Relative path",
input: "relative/path",
expected: false,
},
{
name: "Tilde expansion (becomes absolute)",
input: "~/path",
expected: true,
},
{
name: "Windows absolute path",
input: "C:\\Windows\\System32",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsAbsolutePath(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetRelativePath(t *testing.T) {
// Create temporary directories for testing