Remove some unused shit and write tests for coverage
This commit is contained in:
@@ -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
209
utils/file_test.go
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
313
utils/modifycommand_coverage_test.go
Normal file
313
utils/modifycommand_coverage_test.go
Normal 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"])
|
||||
}
|
||||
93
utils/modifycommand_yaml_convert_test.go
Normal file
93
utils/modifycommand_yaml_convert_test.go
Normal 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)
|
||||
}
|
||||
105
utils/path.go
105
utils/path.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user