Files
synclib/instruction_test.go
2025-10-06 20:05:15 +02:00

1615 lines
43 KiB
Go

package main
import (
"flag"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// Test helper to create a temporary test directory WITHIN the project
func createTestDir(t *testing.T) string {
// Get the project directory (current working directory)
projectDir, err := os.Getwd()
assert.NoError(t, err)
// Create test directory WITHIN the project
testDir := filepath.Join(projectDir, "test_temp")
err = os.MkdirAll(testDir, 0755)
assert.NoError(t, err)
return testDir
}
// Test helper to ensure we're working within the project directory
func ensureInProjectDir(t *testing.T, testDir string) {
// Get current working directory (should be project directory)
projectDir, err := os.Getwd()
assert.NoError(t, err)
// Ensure test directory is within the project directory
if !strings.HasPrefix(testDir, projectDir) {
t.Fatalf("Test directory %s is not within project directory %s", testDir, projectDir)
}
// Additional guard: ensure we're not accessing system directories
if strings.Contains(testDir, "/tmp/") || strings.Contains(testDir, "\\Temp\\") ||
strings.Contains(testDir, "/var/tmp/") || strings.Contains(testDir, "\\AppData\\") {
t.Fatalf("Test directory %s appears to be outside project directory", testDir)
}
}
// Test helper to clean up test directory
func cleanupTestDir(t *testing.T, testDir string) {
// If current working dir is inside the testDir, move out before removal (Windows locks directories in use)
if wd, _ := os.Getwd(); strings.HasPrefix(wd, testDir) {
_ = os.Chdir(filepath.Dir(testDir))
}
// We MUST remove the directory - this is critical for test isolation
err := os.RemoveAll(testDir)
assert.NoError(t, err, "Failed to remove test directory %s", testDir)
}
func TestParseInstruction(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create test files
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
t.Run("Basic instruction", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt", testDir)
assert.NoError(t, err)
assert.Contains(t, instruction.Source, "src.txt")
assert.Contains(t, instruction.Target, "dst.txt")
assert.False(t, instruction.Force)
assert.False(t, instruction.Hard)
assert.False(t, instruction.Delete)
})
t.Run("Instruction with force flag", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt,force=true", testDir)
assert.NoError(t, err)
assert.True(t, instruction.Force)
})
t.Run("Instruction with hard flag", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt,hard=true", testDir)
assert.NoError(t, err)
assert.True(t, instruction.Hard)
})
t.Run("Instruction with delete flag", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt,delete=true", testDir)
assert.NoError(t, err)
assert.True(t, instruction.Delete)
assert.True(t, instruction.Force) // Delete implies Force
})
t.Run("Legacy format", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt,true,false,true", testDir)
assert.NoError(t, err)
assert.True(t, instruction.Force)
assert.False(t, instruction.Hard)
assert.True(t, instruction.Delete)
})
t.Run("Comment line", func(t *testing.T) {
_, err := ParseInstruction("# This is a comment", testDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "comment line")
})
t.Run("Invalid format", func(t *testing.T) {
_, err := ParseInstruction("src.txt", testDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not enough parameters")
})
}
func TestLinkInstruction_RunAsync(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
t.Run("Create symlink", func(t *testing.T) {
instruction := LinkInstruction{
Source: srcFile,
Target: filepath.Join(testDir, "link.txt"),
}
status := make(chan error)
go instruction.RunAsync(status)
err := <-status
assert.NoError(t, err)
// Verify symlink was created
linkPath := filepath.Join(testDir, "link.txt")
assert.True(t, FileExists(linkPath))
isSymlink, err := IsSymlink(linkPath)
assert.NoError(t, err)
assert.True(t, isSymlink)
})
t.Run("Create hard link", func(t *testing.T) {
instruction := LinkInstruction{
Source: srcFile,
Target: filepath.Join(testDir, "hardlink.txt"),
Hard: true,
}
status := make(chan error)
go instruction.RunAsync(status)
err := <-status
assert.NoError(t, err)
// Verify hard link was created
linkPath := filepath.Join(testDir, "hardlink.txt")
assert.True(t, FileExists(linkPath))
// Verify it's the same file (hard link)
assert.True(t, AreSame(srcFile, linkPath))
})
t.Run("Target exists without force", func(t *testing.T) {
// Create existing target
existingFile := filepath.Join(testDir, "existing.txt")
err := os.WriteFile(existingFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingFile,
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.Error(t, err)
assert.Contains(t, err.Error(), "target")
assert.Contains(t, err.Error(), "exists")
})
t.Run("Target exists with force", func(t *testing.T) {
// Create existing target
existingFile := filepath.Join(testDir, "existing2.txt")
err := os.WriteFile(existingFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingFile,
Force: true,
Delete: true, // Need delete flag to overwrite existing file
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.NoError(t, err)
// Verify symlink was created (overwriting existing file)
assert.True(t, FileExists(existingFile))
isSymlink, err := IsSymlink(existingFile)
assert.NoError(t, err)
assert.True(t, isSymlink)
})
t.Run("Source does not exist", func(t *testing.T) {
instruction := LinkInstruction{
Source: filepath.Join(testDir, "nonexistent.txt"),
Target: filepath.Join(testDir, "link.txt"),
}
status := make(chan error)
go instruction.RunAsync(status)
err := <-status
assert.Error(t, err)
assert.Contains(t, err.Error(), "does not exist")
})
t.Run("Same source and target", func(t *testing.T) {
instruction := LinkInstruction{
Source: srcFile,
Target: srcFile,
}
status := make(chan error)
go instruction.RunAsync(status)
err := <-status
assert.NoError(t, err) // Should succeed but do nothing
})
}
func TestLinkInstruction_Undo(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
t.Run("Undo symlink", func(t *testing.T) {
// Create symlink first
linkPath := filepath.Join(testDir, "link.txt")
err := os.Symlink(srcFile, linkPath)
assert.NoError(t, err)
instruction := LinkInstruction{
Target: linkPath,
}
instruction.Undo()
// Verify symlink was removed
assert.False(t, FileExists(linkPath))
})
t.Run("Undo non-existent target", func(t *testing.T) {
instruction := LinkInstruction{
Target: filepath.Join(testDir, "nonexistent.txt"),
}
// Should not error
instruction.Undo()
})
t.Run("Undo regular file", func(t *testing.T) {
// Create regular file
regularFile := filepath.Join(testDir, "regular.txt")
err := os.WriteFile(regularFile, []byte("regular content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Target: regularFile,
}
instruction.Undo()
// Verify file still exists (not a symlink, so not removed)
assert.True(t, FileExists(regularFile))
})
}
func TestParseYAMLFile(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create test files
srcDir := filepath.Join(testDir, "src")
err := os.MkdirAll(srcDir, 0755)
assert.NoError(t, err)
file1 := filepath.Join(srcDir, "file1.txt")
file2 := filepath.Join(srcDir, "file2.txt")
file3 := filepath.Join(srcDir, "file3.log")
err = os.WriteFile(file1, []byte("content1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("content2"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file3, []byte("log content"), 0644)
assert.NoError(t, err)
t.Run("YAML with glob pattern", func(t *testing.T) {
yamlContent := `links:
- source: src/*.txt
target: dst
force: true
hard: false`
yamlFile := filepath.Join(testDir, "sync.yaml")
err := os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
instructions, err := ParseYAMLFile(yamlFile, testDir)
assert.NoError(t, err)
assert.Equal(t, 2, len(instructions))
// Check that we have the expected files
sources := make([]string, len(instructions))
for i, inst := range instructions {
sources[i] = inst.Source
}
// Check that we have the expected files (using contains to handle path normalization)
hasFile1 := false
hasFile2 := false
hasFile3 := false
for _, source := range sources {
if strings.Contains(source, "file1.txt") {
hasFile1 = true
}
if strings.Contains(source, "file2.txt") {
hasFile2 = true
}
if strings.Contains(source, "file3.log") {
hasFile3 = true
}
}
assert.True(t, hasFile1, "Should contain file1.txt")
assert.True(t, hasFile2, "Should contain file2.txt")
assert.False(t, hasFile3, "Should not contain file3.log")
// Check flags
for _, inst := range instructions {
assert.True(t, inst.Force)
assert.False(t, inst.Hard)
assert.False(t, inst.Delete)
}
})
t.Run("YAML with single file", func(t *testing.T) {
yamlContent := `links:
- source: src/file1.txt
target: dst/single.txt
force: true`
yamlFile := filepath.Join(testDir, "sync2.yaml")
err := os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
instructions, err := ParseYAMLFile(yamlFile, testDir)
assert.NoError(t, err)
assert.Equal(t, 1, len(instructions))
assert.Contains(t, instructions[0].Source, "file1.txt")
assert.Contains(t, instructions[0].Target, "dst/single.txt")
assert.True(t, instructions[0].Force)
})
t.Run("Invalid YAML", func(t *testing.T) {
yamlFile := filepath.Join(testDir, "invalid.yaml")
err := os.WriteFile(yamlFile, []byte("invalid: yaml: content: [["), 0644)
assert.NoError(t, err)
_, err = ParseYAMLFile(yamlFile, testDir)
assert.Error(t, err)
})
t.Run("Non-existent YAML file", func(t *testing.T) {
_, err := ParseYAMLFile(filepath.Join(testDir, "nonexistent.yaml"), testDir)
assert.Error(t, err)
})
}
func TestExpandPattern(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create test files
srcDir := filepath.Join(testDir, "src")
err := os.MkdirAll(srcDir, 0755)
assert.NoError(t, err)
file1 := filepath.Join(srcDir, "file1.txt")
file2 := filepath.Join(srcDir, "file2.txt")
file3 := filepath.Join(srcDir, "file3.log")
err = os.WriteFile(file1, []byte("content1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("content2"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file3, []byte("log content"), 0644)
assert.NoError(t, err)
t.Run("Glob pattern", func(t *testing.T) {
links, err := ExpandPattern("src/*.txt", testDir, "dst")
assert.NoError(t, err)
assert.Equal(t, 2, len(links))
sources := make([]string, len(links))
for i, link := range links {
sources[i] = link.Source
}
// Check that we have the expected files (using contains to handle path normalization)
hasFile1 := false
hasFile2 := false
hasFile3 := false
for _, source := range sources {
if strings.Contains(source, "file1.txt") {
hasFile1 = true
}
if strings.Contains(source, "file2.txt") {
hasFile2 = true
}
if strings.Contains(source, "file3.log") {
hasFile3 = true
}
}
assert.True(t, hasFile1, "Should contain file1.txt")
assert.True(t, hasFile2, "Should contain file2.txt")
assert.False(t, hasFile3, "Should not contain file3.log")
})
t.Run("Single file pattern", func(t *testing.T) {
links, err := ExpandPattern("src/file1.txt", testDir, "dst/single.txt")
assert.NoError(t, err)
assert.Equal(t, 1, len(links))
assert.Contains(t, links[0].Source, "file1.txt")
assert.Contains(t, links[0].Target, "dst/single.txt")
})
t.Run("Non-existent pattern", func(t *testing.T) {
links, err := ExpandPattern("src/nonexistent.txt", testDir, "dst")
assert.NoError(t, err)
assert.Equal(t, 0, len(links))
})
}
func TestGetSyncFilesRecursively(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create nested directory structure
subdir1 := filepath.Join(testDir, "subdir1")
subdir2 := filepath.Join(testDir, "subdir2", "nested")
err := os.MkdirAll(subdir1, 0755)
assert.NoError(t, err)
err = os.MkdirAll(subdir2, 0755)
assert.NoError(t, err)
// Create sync files
sync1 := filepath.Join(testDir, "sync.yaml")
sync2 := filepath.Join(subdir1, "sync.yml")
sync3 := filepath.Join(subdir2, "sync.yaml")
err = os.WriteFile(sync1, []byte("links: []"), 0644)
assert.NoError(t, err)
err = os.WriteFile(sync2, []byte("links: []"), 0644)
assert.NoError(t, err)
err = os.WriteFile(sync3, []byte("links: []"), 0644)
assert.NoError(t, err)
t.Run("Find sync files recursively", func(t *testing.T) {
files := make(chan string, 10)
status := make(chan error)
go GetSyncFilesRecursively(testDir, files, status)
var foundFiles []string
for {
file, ok := <-files
if !ok {
break
}
foundFiles = append(foundFiles, file)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.NoError(t, err)
}
assert.Equal(t, 3, len(foundFiles))
assert.Contains(t, foundFiles, sync1)
assert.Contains(t, foundFiles, sync2)
assert.Contains(t, foundFiles, sync3)
})
}
func TestReadFromFile(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
t.Run("CSV format", func(t *testing.T) {
csvContent := "src.txt,dst.txt,force=true\nsrc.txt,dst2.txt,hard=true"
csvFile := filepath.Join(testDir, "test.csv")
err := os.WriteFile(csvFile, []byte(csvContent), 0644)
assert.NoError(t, err)
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
go ReadFromFile(csvFile, instructions, status, true)
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.NoError(t, err)
}
assert.Equal(t, 2, len(readInstructions))
assert.True(t, readInstructions[0].Force)
assert.True(t, readInstructions[1].Hard)
})
t.Run("YAML format", func(t *testing.T) {
yamlContent := `links:
- source: src.txt
target: dst.yaml
force: true`
yamlFile := filepath.Join(testDir, "test.yaml")
err := os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
go ReadFromFile(yamlFile, instructions, status, true)
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.NoError(t, err)
}
assert.Equal(t, 1, len(readInstructions))
assert.True(t, readInstructions[0].Force)
})
}
// Test main.go functions that are completely missing coverage
func TestMainFunctions(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
t.Run("IsPipeInput", func(t *testing.T) {
// Test with non-pipe input (should return false)
result := IsPipeInput()
assert.False(t, result)
})
t.Run("ReadFromFilesRecursively", func(t *testing.T) {
// Create sync files in nested directories
subdir1 := filepath.Join(testDir, "subdir1")
subdir2 := filepath.Join(testDir, "subdir2", "nested")
err := os.MkdirAll(subdir1, 0755)
assert.NoError(t, err)
err = os.MkdirAll(subdir2, 0755)
assert.NoError(t, err)
// Create sync files
sync1 := filepath.Join(testDir, "sync.yaml")
sync2 := filepath.Join(subdir1, "sync.yml")
sync3 := filepath.Join(subdir2, "sync.yaml")
err = os.WriteFile(sync1, []byte("[]"), 0644)
assert.NoError(t, err)
err = os.WriteFile(sync2, []byte("[]"), 0644)
assert.NoError(t, err)
err = os.WriteFile(sync3, []byte("[]"), 0644)
assert.NoError(t, err)
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
go ReadFromFilesRecursively(testDir, instructions, status)
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.NoError(t, err)
}
// Should have processed 3 files (even though they're empty)
assert.Equal(t, 0, len(readInstructions)) // Empty YAML files
})
t.Run("ReadFromArgs", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Test ReadFromArgs by calling it directly
// We can't easily mock flag.Args() so we'll test the core logic
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
// Simulate the ReadFromArgs logic manually
LogInfo("Reading input from args")
// Test with valid instruction
instruction, err := ParseInstruction("src.txt,dst.txt,force=true", testDir)
assert.NoError(t, err)
instructions <- &instruction
// Test with invalid instruction (should be handled gracefully)
_, err = ParseInstruction("invalid format", testDir)
assert.Error(t, err) // This should error but not crash
close(instructions)
close(status)
// Verify instruction was created correctly
assert.True(t, instruction.Force)
assert.Contains(t, instruction.Source, "src.txt")
assert.Contains(t, instruction.Target, "dst.txt")
})
t.Run("ReadFromStdin", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Test ReadFromStdin by calling it directly
// We can't easily mock os.Stdin so we'll test the core logic
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
// Simulate the ReadFromStdin logic manually
LogInfo("Reading input from stdin")
// Test with valid instruction (simulating stdin input)
instruction, err := ParseInstruction("src.txt,dst.txt", testDir)
assert.NoError(t, err)
instructions <- &instruction
// Test with invalid instruction (should be handled gracefully)
_, err = ParseInstruction("invalid format", testDir)
assert.Error(t, err) // This should error but not crash
close(instructions)
close(status)
// Verify instruction was created correctly
assert.Contains(t, instruction.Source, "src.txt")
assert.Contains(t, instruction.Target, "dst.txt")
})
}
// Test the untested ReadFromArgs and ReadFromStdin functions
func TestReadFromFunctions(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
t.Run("ReadFromArgs_with_valid_instructions", func(t *testing.T) {
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
// Drive ReadFromArgs by injecting args via a custom FlagSet
origCmd := flag.CommandLine
defer func() { flag.CommandLine = origCmd }()
fs := flag.NewFlagSet("test", flag.ContinueOnError)
// Include two valid instructions and one comment and one blank
_ = fs.Parse([]string{
"src.txt,dst.txt,force=true",
"src.txt,dst2.txt,hard=true",
"#comment",
"",
})
flag.CommandLine = fs
go ReadFromArgs(instructions, status)
// Collect results
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
// ReadFromArgs with no args should not error
assert.NoError(t, err)
}
// Expect 2 parsed instructions (comment/blank ignored)
assert.Equal(t, 2, len(readInstructions))
assert.True(t, readInstructions[0].Force)
assert.True(t, readInstructions[1].Hard)
})
t.Run("ReadFromStdin_with_valid_instructions", func(t *testing.T) {
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
// Feed stdin via a pipe with multiple lines (valid, invalid, comment, blank)
r, w, err := os.Pipe()
assert.NoError(t, err)
origStdin := os.Stdin
os.Stdin = r
defer func() { os.Stdin = origStdin }()
go ReadFromStdin(instructions, status)
_, _ = w.WriteString("src.txt,dst.txt\n")
_, _ = w.WriteString("src.txt,dst2.txt,delete=true\n")
_, _ = w.WriteString("invalid format\n")
_, _ = w.WriteString("#comment\n")
_, _ = w.WriteString("\n")
_ = w.Close()
// Collect results
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
// ReadFromStdin with no input should not error
assert.NoError(t, err)
}
// Expect 2 parsed instructions (invalid/comment/blank ignored)
assert.Equal(t, 2, len(readInstructions))
assert.False(t, readInstructions[0].Delete)
assert.True(t, readInstructions[1].Delete)
})
t.Run("ReadFromArgs_error_handling", func(t *testing.T) {
// Test that ReadFromArgs handles errors gracefully
// This tests the error handling path in the function
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
go ReadFromArgs(instructions, status)
// Should complete without panicking even with no args
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.NoError(t, err)
}
assert.Equal(t, 0, len(readInstructions))
})
t.Run("ReadFromStdin_error_handling", func(t *testing.T) {
// Test that ReadFromStdin handles errors gracefully
instructions := make(chan *LinkInstruction, 10)
status := make(chan error)
go ReadFromStdin(instructions, status)
// Should complete without panicking even with no stdin
var readInstructions []*LinkInstruction
for {
inst, ok := <-instructions
if !ok {
break
}
readInstructions = append(readInstructions, inst)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.NoError(t, err)
}
assert.Equal(t, 0, len(readInstructions))
})
}
// Test missing instruction.go paths
func TestInstructionEdgeCases(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
t.Run("LinkInstruction_String_with_all_flags", func(t *testing.T) {
instruction := LinkInstruction{
Source: "src.txt",
Target: "dst.txt",
Force: true,
Hard: true,
Delete: true,
}
result := instruction.String()
assert.Contains(t, result, "force=true")
assert.Contains(t, result, "hard=true")
assert.Contains(t, result, "delete=true")
})
t.Run("LinkInstruction_String_with_no_flags", func(t *testing.T) {
instruction := LinkInstruction{
Source: "src.txt",
Target: "dst.txt",
}
result := instruction.String()
assert.NotContains(t, result, "force=true")
assert.NotContains(t, result, "hard=true")
assert.NotContains(t, result, "delete=true")
})
t.Run("ParseInstruction_with_malformed_flags", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt,invalid=flag,another=bad", testDir)
assert.NoError(t, err)
// Should not crash, just ignore invalid flags
assert.Contains(t, instruction.Source, "src.txt")
assert.Contains(t, instruction.Target, "dst.txt")
})
t.Run("ParseInstruction_with_empty_values", func(t *testing.T) {
instruction, err := ParseInstruction("src.txt,dst.txt,force=", testDir)
assert.NoError(t, err)
// Empty value should be treated as false
assert.False(t, instruction.Force)
})
t.Run("ParseInstruction_with_whitespace", func(t *testing.T) {
instruction, err := ParseInstruction(" src.txt , dst.txt , force=true ", testDir)
assert.NoError(t, err)
assert.Contains(t, instruction.Source, "src.txt")
assert.Contains(t, instruction.Target, "dst.txt")
assert.True(t, instruction.Force)
})
t.Run("LinkInstruction_RunAsync_target_exists_symlink", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create existing symlink
existingLink := filepath.Join(testDir, "existing.txt")
err = os.Symlink(srcFile, existingLink)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingLink,
Force: true,
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.NoError(t, err)
// Verify symlink was replaced
assert.True(t, FileExists(existingLink))
})
t.Run("LinkInstruction_RunAsync_target_exists_regular_file", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create existing regular file
existingFile := filepath.Join(testDir, "existing.txt")
err = os.WriteFile(existingFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingFile,
Force: true,
Hard: true, // This should allow overwriting regular files
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.NoError(t, err)
// Verify file was replaced with symlink
assert.True(t, FileExists(existingFile))
})
t.Run("LinkInstruction_RunAsync_target_exists_regular_file_no_hard", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create existing regular file
existingFile := filepath.Join(testDir, "existing2.txt")
err = os.WriteFile(existingFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingFile,
Force: true,
Hard: false, // This should NOT allow overwriting regular files
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.Error(t, err)
assert.Contains(t, err.Error(), "refusing to delte actual")
})
t.Run("LinkInstruction_RunAsync_target_exists_regular_file_with_delete", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create existing regular file
existingFile := filepath.Join(testDir, "existing3.txt")
err = os.WriteFile(existingFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingFile,
Force: true,
Delete: true, // This should allow deleting regular files
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.NoError(t, err)
// Verify file was replaced with symlink
assert.True(t, FileExists(existingFile))
})
t.Run("LinkInstruction_RunAsync_target_exists_directory", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create existing directory
existingDir := filepath.Join(testDir, "existing_dir")
err = os.MkdirAll(existingDir, 0755)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: existingDir,
Force: true,
Delete: true, // Need delete flag to overwrite directory
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.NoError(t, err)
// Verify directory was replaced with symlink
assert.True(t, FileExists(existingDir))
})
t.Run("LinkInstruction_RunAsync_create_target_directory", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Target in non-existent directory
targetDir := filepath.Join(testDir, "newdir", "subdir")
targetFile := filepath.Join(targetDir, "link.txt")
instruction := LinkInstruction{
Source: srcFile,
Target: targetFile,
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.NoError(t, err)
// Verify directory was created and symlink was created
assert.True(t, FileExists(targetFile))
})
t.Run("LinkInstruction_RunAsync_hard_link_error", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create target that will cause hard link to fail
targetFile := filepath.Join(testDir, "link.txt")
err = os.WriteFile(targetFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: targetFile,
Hard: true,
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.Error(t, err)
assert.Contains(t, err.Error(), "target")
assert.Contains(t, err.Error(), "exists")
})
t.Run("LinkInstruction_RunAsync_symlink_error", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("test content"), 0644)
assert.NoError(t, err)
// Create target that will cause symlink to fail
targetFile := filepath.Join(testDir, "link.txt")
err = os.WriteFile(targetFile, []byte("existing content"), 0644)
assert.NoError(t, err)
instruction := LinkInstruction{
Source: srcFile,
Target: targetFile,
Hard: false,
}
status := make(chan error)
go instruction.RunAsync(status)
err = <-status
assert.Error(t, err)
assert.Contains(t, err.Error(), "target")
assert.Contains(t, err.Error(), "exists")
})
}
// Test missing util.go paths
func TestUtilEdgeCases(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
t.Run("IsSymlink_with_nonexistent_file", func(t *testing.T) {
result, err := IsSymlink("nonexistent.txt")
assert.Error(t, err)
assert.False(t, result)
})
t.Run("IsSymlink_with_regular_file", func(t *testing.T) {
// Create regular file
file := filepath.Join(testDir, "regular.txt")
err := os.WriteFile(file, []byte("content"), 0644)
assert.NoError(t, err)
result, err := IsSymlink(file)
assert.NoError(t, err)
assert.False(t, result)
})
t.Run("IsSymlink_with_symlink", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("content"), 0644)
assert.NoError(t, err)
// Create symlink
linkFile := filepath.Join(testDir, "link.txt")
err = os.Symlink(srcFile, linkFile)
assert.NoError(t, err)
result, err := IsSymlink(linkFile)
assert.NoError(t, err)
assert.True(t, result)
})
t.Run("FileExists_with_nonexistent_file", func(t *testing.T) {
result := FileExists("nonexistent.txt")
assert.False(t, result)
})
t.Run("FileExists_with_regular_file", func(t *testing.T) {
// Create regular file
file := filepath.Join(testDir, "regular.txt")
err := os.WriteFile(file, []byte("content"), 0644)
assert.NoError(t, err)
result := FileExists(file)
assert.True(t, result)
})
t.Run("FileExists_with_symlink", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src2.txt")
err := os.WriteFile(srcFile, []byte("content"), 0644)
assert.NoError(t, err)
// Create symlink
linkFile := filepath.Join(testDir, "link2.txt")
err = os.Symlink(srcFile, linkFile)
assert.NoError(t, err)
result := FileExists(linkFile)
assert.True(t, result)
})
t.Run("NormalizePath_with_absolute_path", func(t *testing.T) {
absPath := filepath.Join(testDir, "file.txt")
result := NormalizePath(absPath, testDir)
assert.Contains(t, result, "file.txt")
})
t.Run("NormalizePath_with_relative_path", func(t *testing.T) {
result := NormalizePath("file.txt", testDir)
assert.Contains(t, result, "file.txt")
})
t.Run("NormalizePath_with_quotes", func(t *testing.T) {
result := NormalizePath("\"file.txt\"", testDir)
assert.NotContains(t, result, "\"")
})
t.Run("NormalizePath_with_backslashes", func(t *testing.T) {
result := NormalizePath("file\\path.txt", testDir)
assert.Contains(t, result, "file/path.txt")
})
t.Run("AreSame_with_same_file", func(t *testing.T) {
// Create file
file := filepath.Join(testDir, "file.txt")
err := os.WriteFile(file, []byte("content"), 0644)
assert.NoError(t, err)
result := AreSame(file, file)
assert.True(t, result)
})
t.Run("AreSame_with_different_files", func(t *testing.T) {
// Create two different files
file1 := filepath.Join(testDir, "file1.txt")
file2 := filepath.Join(testDir, "file2.txt")
err := os.WriteFile(file1, []byte("content1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(file2, []byte("content2"), 0644)
assert.NoError(t, err)
result := AreSame(file1, file2)
assert.False(t, result)
})
t.Run("AreSame_with_hard_link", func(t *testing.T) {
// Create file
file1 := filepath.Join(testDir, "file3.txt")
err := os.WriteFile(file1, []byte("content"), 0644)
assert.NoError(t, err)
// Create hard link
file2 := filepath.Join(testDir, "file4.txt")
err = os.Link(file1, file2)
assert.NoError(t, err)
result := AreSame(file1, file2)
assert.True(t, result)
})
t.Run("AreSame_with_nonexistent_files", func(t *testing.T) {
result := AreSame("nonexistent1.txt", "nonexistent2.txt")
assert.False(t, result)
})
t.Run("ConvertHome_with_tilde", func(t *testing.T) {
result, err := ConvertHome("~/file.txt")
assert.NoError(t, err)
assert.NotContains(t, result, "~")
assert.Contains(t, result, "file.txt")
})
t.Run("ConvertHome_without_tilde", func(t *testing.T) {
result, err := ConvertHome("file.txt")
assert.NoError(t, err)
assert.Equal(t, "file.txt", result)
})
t.Run("ConvertHome_with_tilde_error", func(t *testing.T) {
// This is hard to test without mocking os.UserHomeDir
// But we can test the normal case
result, err := ConvertHome("~/file.txt")
assert.NoError(t, err)
assert.NotEmpty(t, result)
})
t.Run("GetSyncFilesRecursively_with_error", func(t *testing.T) {
// Test with non-existent directory
nonexistentDir := filepath.Join(testDir, "nonexistent")
files := make(chan string, 10)
status := make(chan error)
go GetSyncFilesRecursively(nonexistentDir, files, status)
var foundFiles []string
for {
file, ok := <-files
if !ok {
break
}
foundFiles = append(foundFiles, file)
}
// Check for errors
for {
err, ok := <-status
if !ok {
break
}
assert.Error(t, err)
}
assert.Equal(t, 0, len(foundFiles))
})
}
// Test missing logger.go paths
func TestLoggerFunctions(t *testing.T) {
t.Run("LogInfo", func(t *testing.T) {
// Test that LogInfo doesn't crash
LogInfo("Test info message")
})
t.Run("LogError", func(t *testing.T) {
// Test that LogError doesn't crash
LogError("Test error message")
})
t.Run("LogSuccess", func(t *testing.T) {
// Test that LogSuccess doesn't crash
LogSuccess("Test success message")
})
t.Run("LogTarget", func(t *testing.T) {
// Test that LogTarget doesn't crash
LogTarget("Test target message")
})
t.Run("LogSource", func(t *testing.T) {
// Test that LogSource doesn't crash
LogSource("Test source message")
})
t.Run("LogImportant", func(t *testing.T) {
// Test that LogImportant doesn't crash
LogImportant("Test important message")
})
t.Run("LogPath", func(t *testing.T) {
// Test that LogPath doesn't crash
LogPath("Test path value")
})
t.Run("FormatSourcePath", func(t *testing.T) {
// Test that FormatSourcePath doesn't crash
result := FormatSourcePath("test/path")
assert.Contains(t, result, "test/path")
})
t.Run("FormatTargetPath", func(t *testing.T) {
// Test that FormatTargetPath doesn't crash
result := FormatTargetPath("test/path")
assert.Contains(t, result, "test/path")
})
t.Run("FormatPathValue", func(t *testing.T) {
// Test that FormatPathValue doesn't crash
result := FormatPathValue("test/path")
assert.Contains(t, result, "test/path")
})
}
// Test missing instruction.go YAML parsing paths
func TestYAMLEdgeCases(t *testing.T) {
testDir := createTestDir(t)
defer cleanupTestDir(t, testDir)
// Ensure we're working within the project directory
ensureInProjectDir(t, testDir)
// Change to test directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
os.Chdir(testDir)
t.Run("ParseYAMLFile_with_direct_list", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("content"), 0644)
assert.NoError(t, err)
// YAML with direct list (not wrapped in 'links')
yamlContent := `- source: src.txt
target: dst.txt
force: true`
yamlFile := filepath.Join(testDir, "direct.yaml")
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
instructions, err := ParseYAMLFile(yamlFile, testDir)
assert.NoError(t, err)
assert.Equal(t, 1, len(instructions))
assert.True(t, instructions[0].Force)
})
t.Run("ParseYAMLFile_with_empty_links", func(t *testing.T) {
yamlContent := `[]`
yamlFile := filepath.Join(testDir, "empty.yaml")
err := os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
instructions, err := ParseYAMLFile(yamlFile, testDir)
assert.NoError(t, err)
assert.Equal(t, 0, len(instructions))
})
t.Run("ParseYAMLFile_with_invalid_yaml_structure", func(t *testing.T) {
yamlContent := `invalid: yaml: structure: [`
yamlFile := filepath.Join(testDir, "invalid.yaml")
err := os.WriteFile(yamlFile, []byte(yamlContent), 0644)
assert.NoError(t, err)
_, err = ParseYAMLFile(yamlFile, testDir)
assert.Error(t, err)
})
t.Run("ExpandPattern_with_directory_match", func(t *testing.T) {
// Create directory structure
srcDir := filepath.Join(testDir, "src")
err := os.MkdirAll(srcDir, 0755)
assert.NoError(t, err)
// Create a directory (not a file)
dirPath := filepath.Join(srcDir, "subdir")
err = os.MkdirAll(dirPath, 0755)
assert.NoError(t, err)
// Create a file in the directory
filePath := filepath.Join(dirPath, "file.txt")
err = os.WriteFile(filePath, []byte("content"), 0644)
assert.NoError(t, err)
// Pattern that matches directory
links, err := ExpandPattern("src/**/*", testDir, "dst")
assert.NoError(t, err)
// Should skip directories and only include files
// Note: The pattern might match both the directory and the file
assert.GreaterOrEqual(t, len(links), 1)
hasFile := false
for _, link := range links {
if strings.Contains(link.Source, "file.txt") {
hasFile = true
break
}
}
assert.True(t, hasFile, "Should contain file.txt")
})
t.Run("ExpandPattern_with_single_file_target", func(t *testing.T) {
// Create source file
srcFile := filepath.Join(testDir, "src.txt")
err := os.WriteFile(srcFile, []byte("content"), 0644)
assert.NoError(t, err)
// Target is a file (not directory)
targetFile := filepath.Join(testDir, "target.txt")
links, err := ExpandPattern("src.txt", testDir, targetFile)
assert.NoError(t, err)
assert.Equal(t, 1, len(links))
assert.Equal(t, targetFile, links[0].Target)
})
t.Run("ExpandPattern_with_multiple_files_single_target", func(t *testing.T) {
// Create source files
srcFile1 := filepath.Join(testDir, "src1.txt")
srcFile2 := filepath.Join(testDir, "src2.txt")
err := os.WriteFile(srcFile1, []byte("content1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(srcFile2, []byte("content2"), 0644)
assert.NoError(t, err)
// Target is a file (not directory)
targetFile := filepath.Join(testDir, "target.txt")
links, err := ExpandPattern("src*.txt", testDir, targetFile)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(links), 2)
// Each link should have the same target file (the ExpandPattern logic should handle this)
for _, link := range links {
// The target should be the same for all links when target is a file
assert.Contains(t, link.Target, "target.txt")
}
})
t.Run("IsYAMLFile", func(t *testing.T) {
assert.True(t, IsYAMLFile("test.yaml"))
assert.True(t, IsYAMLFile("test.yml"))
assert.False(t, IsYAMLFile("test.txt"))
assert.False(t, IsYAMLFile("test"))
})
}