diff --git a/.gitignore b/.gitignore index 1f2f1f4..fa51965 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ cln cln.log .qodo *.log +*.out diff --git a/go.mod b/go.mod index 41ee0bc..a3c97fa 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,9 @@ go 1.21.7 require gopkg.in/yaml.v3 v3.0.1 require github.com/bmatcuk/doublestar/v4 v4.8.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect +) diff --git a/go.sum b/go.sum index b827d61..b69818a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/instruction_test.go b/instruction_test.go new file mode 100644 index 0000000..59f57f0 --- /dev/null +++ b/instruction_test.go @@ -0,0 +1,1614 @@ +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")) + }) +}