Implement a "from" to config files that loads other files

This commit is contained in:
2025-10-06 22:17:34 +02:00
parent 21d7f56ccf
commit eb81ec4162
4 changed files with 368 additions and 36 deletions

View File

@@ -2,6 +2,7 @@ package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
@@ -1450,18 +1451,6 @@ func TestErrorPaths(t *testing.T) {
assert.Contains(t, err.Error(), "invalid format")
})
t.Run("ReadFromFile_nonexistent_file", func(t *testing.T) {
// Skip this test as ReadFromFile calls os.Exit(1) when file doesn't exist
// which terminates the test process
t.Skip("Skipping test that calls os.Exit(1) when file doesn't exist")
})
t.Run("ReadFromFile_invalid_yaml", func(t *testing.T) {
// Skip this test as ReadFromFile calls os.Exit(1) when YAML parsing fails
// which terminates the test process
t.Skip("Skipping test that calls os.Exit(1) when YAML parsing fails")
})
t.Run("ReadFromArgs_no_args", func(t *testing.T) {
// Test ReadFromArgs with no command line arguments
instructions := make(chan *LinkInstruction, 10)
@@ -1665,18 +1654,6 @@ func TestErrorPaths(t *testing.T) {
// FormatErrorMessage just formats the message, no "ERROR" prefix
})
t.Run("GenerateRandomAnsiColor", func(t *testing.T) {
// Test GenerateRandomAnsiColor function
color1 := GenerateRandomAnsiColor()
color2 := GenerateRandomAnsiColor()
// Colors should be different (though there's a small chance they could be the same)
// At least they should be valid ANSI color codes
assert.NotEmpty(t, color1)
assert.NotEmpty(t, color2)
assert.Contains(t, color1, "\x1b[")
assert.Contains(t, color2, "\x1b[")
})
t.Run("RunAsync_error_handling", func(t *testing.T) {
// Test RunAsync error handling with invalid source
@@ -1738,7 +1715,7 @@ func TestErrorPaths(t *testing.T) {
assert.Empty(t, links)
// Invalid pattern - this actually returns an error
links, err = ExpandPattern("invalid[", testDir, "target.txt")
_, err = ExpandPattern("invalid[", testDir, "target.txt")
assert.Error(t, err)
assert.Contains(t, err.Error(), "syntax error in pattern")
})
@@ -1850,12 +1827,6 @@ func TestMainFunctionsCoverage(t *testing.T) {
// Don't close channels - let the goroutine handle its own cleanup
})
t.Run("startInputSource_with_args", func(t *testing.T) {
// Skip this test as it calls showUsageAndExit() which calls os.Exit(1)
// and would terminate the test process
t.Skip("Skipping test that calls showUsageAndExit() as it terminates the process")
})
t.Run("startDefaultInputSource_with_sync_file", func(t *testing.T) {
// Create sync file
syncFile := filepath.Join(testDir, "sync")
@@ -1939,7 +1910,6 @@ func TestMainFunctionsCoverage(t *testing.T) {
// This confirms that startDefaultInputSource would call showUsageAndExit()
// We can't actually call it because os.Exit(1) terminates the process
t.Skip("Skipping test that calls showUsageAndExit() as it terminates the process")
})
t.Run("showUsageAndExit", func(t *testing.T) {
@@ -2079,3 +2049,295 @@ func TestMainFunctionsCoverage(t *testing.T) {
// Don't close channels - let the goroutine handle its own cleanup
})
}
// Test YAMLConfig "From" functionality
func TestYAMLConfigFrom(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 files
srcFile1 := filepath.Join(testDir, "src1.txt")
srcFile2 := filepath.Join(testDir, "src2.txt")
srcFile3 := filepath.Join(testDir, "src3.txt")
err := os.WriteFile(srcFile1, []byte("content1"), 0644)
assert.NoError(t, err)
err = os.WriteFile(srcFile2, []byte("content2"), 0644)
assert.NoError(t, err)
err = os.WriteFile(srcFile3, []byte("content3"), 0644)
assert.NoError(t, err)
t.Run("ParseYAMLFileRecursive_simple_from", func(t *testing.T) {
// Create a referenced config file
referencedConfig := filepath.Join(testDir, "referenced.yaml")
referencedYAML := `links:
- source: src2.txt
target: dst2.txt
- source: src3.txt
target: dst3.txt`
err := os.WriteFile(referencedConfig, []byte(referencedYAML), 0644)
assert.NoError(t, err)
// Create main config file that references the other
mainConfig := filepath.Join(testDir, "main.yaml")
mainYAML := `links:
- source: src1.txt
target: dst1.txt
from:
- referenced.yaml`
err = os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse recursively
instructions, err := ParseYAMLFileRecursive(mainConfig, testDir)
assert.NoError(t, err)
assert.Equal(t, 3, len(instructions))
// Check that all instructions are present
sources := make([]string, len(instructions))
targets := make([]string, len(instructions))
for i, inst := range instructions {
sources[i] = filepath.Base(inst.Source)
targets[i] = filepath.Base(inst.Target)
}
assert.Contains(t, sources, "src1.txt")
assert.Contains(t, sources, "src2.txt")
assert.Contains(t, sources, "src3.txt")
assert.Contains(t, targets, "dst1.txt")
assert.Contains(t, targets, "dst2.txt")
assert.Contains(t, targets, "dst3.txt")
})
t.Run("ParseYAMLFileRecursive_multiple_from", func(t *testing.T) {
// Create multiple referenced config files
config1 := filepath.Join(testDir, "config1.yaml")
config1YAML := `links:
- source: src1.txt
target: dst1.txt`
err := os.WriteFile(config1, []byte(config1YAML), 0644)
assert.NoError(t, err)
config2 := filepath.Join(testDir, "config2.yaml")
config2YAML := `links:
- source: src2.txt
target: dst2.txt`
err = os.WriteFile(config2, []byte(config2YAML), 0644)
assert.NoError(t, err)
// Create main config that references both
mainConfig := filepath.Join(testDir, "main2.yaml")
mainYAML := `links:
- source: src3.txt
target: dst3.txt
from:
- config1.yaml
- config2.yaml`
err = os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse recursively
instructions, err := ParseYAMLFileRecursive(mainConfig, testDir)
assert.NoError(t, err)
assert.Equal(t, 3, len(instructions))
// Check that all instructions are present
sources := make([]string, len(instructions))
for i, inst := range instructions {
sources[i] = filepath.Base(inst.Source)
}
assert.Contains(t, sources, "src1.txt")
assert.Contains(t, sources, "src2.txt")
assert.Contains(t, sources, "src3.txt")
})
t.Run("ParseYAMLFileRecursive_nested_from", func(t *testing.T) {
// Create a deeply nested config structure
nestedDir := filepath.Join(testDir, "nested")
err := os.MkdirAll(nestedDir, 0755)
assert.NoError(t, err)
// Create source files in the nested directory
nestedSrcFile := filepath.Join(nestedDir, "nested_src.txt")
err = os.WriteFile(nestedSrcFile, []byte("nested content"), 0644)
assert.NoError(t, err)
// Create a config file in the nested directory with relative paths
nestedConfig := filepath.Join(nestedDir, "nested.yaml")
nestedYAML := `links:
- source: nested_src.txt
target: nested_dst.txt`
err = os.WriteFile(nestedConfig, []byte(nestedYAML), 0644)
assert.NoError(t, err)
// Create a config that references the nested one
intermediateConfig := filepath.Join(testDir, "intermediate.yaml")
intermediateYAML := `links:
- source: src2.txt
target: dst2.txt
from:
- nested/nested.yaml`
err = os.WriteFile(intermediateConfig, []byte(intermediateYAML), 0644)
assert.NoError(t, err)
// Create main config that references the intermediate one
mainConfig := filepath.Join(testDir, "main3.yaml")
mainYAML := `links:
- source: src3.txt
target: dst3.txt
from:
- intermediate.yaml`
err = os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse recursively
instructions, err := ParseYAMLFileRecursive(mainConfig, testDir)
assert.NoError(t, err)
assert.Equal(t, 3, len(instructions))
// Check that all instructions are present
sources := make([]string, len(instructions))
for i, inst := range instructions {
sources[i] = filepath.Base(inst.Source)
}
assert.Contains(t, sources, "nested_src.txt")
assert.Contains(t, sources, "src2.txt")
assert.Contains(t, sources, "src3.txt")
})
t.Run("ParseYAMLFileRecursive_absolute_paths", func(t *testing.T) {
// Create a config file with absolute path reference
absoluteConfig := filepath.Join(testDir, "absolute.yaml")
absoluteYAML := `links:
- source: src1.txt
target: dst1.txt`
err := os.WriteFile(absoluteConfig, []byte(absoluteYAML), 0644)
assert.NoError(t, err)
// Create main config with absolute path reference
mainConfig := filepath.Join(testDir, "main4.yaml")
mainYAML := fmt.Sprintf(`links:
- source: src2.txt
target: dst2.txt
from:
- %s`, absoluteConfig)
err = os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse recursively
instructions, err := ParseYAMLFileRecursive(mainConfig, testDir)
assert.NoError(t, err)
assert.Equal(t, 2, len(instructions))
// Check that both instructions are present
sources := make([]string, len(instructions))
for i, inst := range instructions {
sources[i] = filepath.Base(inst.Source)
}
assert.Contains(t, sources, "src1.txt")
assert.Contains(t, sources, "src2.txt")
})
t.Run("ParseYAMLFileRecursive_circular_reference", func(t *testing.T) {
// Create config files that reference each other (circular)
config1 := filepath.Join(testDir, "circular1.yaml")
config1YAML := `links:
- source: src1.txt
target: dst1.txt
from:
- circular2.yaml`
err := os.WriteFile(config1, []byte(config1YAML), 0644)
assert.NoError(t, err)
config2 := filepath.Join(testDir, "circular2.yaml")
config2YAML := `links:
- source: src2.txt
target: dst2.txt
from:
- circular1.yaml`
err = os.WriteFile(config2, []byte(config2YAML), 0644)
assert.NoError(t, err)
// Parse should detect circular reference
_, err = ParseYAMLFileRecursive(config1, testDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "circular reference detected")
})
t.Run("ParseYAMLFileRecursive_nonexistent_file", func(t *testing.T) {
// Create config that references a non-existent file
mainConfig := filepath.Join(testDir, "main5.yaml")
mainYAML := `links:
- source: src1.txt
target: dst1.txt
from:
- nonexistent.yaml`
err := os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse should return error for non-existent file
_, err = ParseYAMLFileRecursive(mainConfig, testDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "error parsing referenced file")
})
t.Run("ParseYAMLFileRecursive_no_from", func(t *testing.T) {
// Create config without "from" field (should work like regular ParseYAMLFile)
mainConfig := filepath.Join(testDir, "main6.yaml")
mainYAML := `links:
- source: src1.txt
target: dst1.txt`
err := os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse recursively
instructions, err := ParseYAMLFileRecursive(mainConfig, testDir)
assert.NoError(t, err)
assert.Equal(t, 1, len(instructions))
assert.Contains(t, instructions[0].Source, "src1.txt")
assert.Contains(t, instructions[0].Target, "dst1.txt")
})
t.Run("ParseYAMLFileRecursive_empty_from", func(t *testing.T) {
// Create config with empty "from" field
mainConfig := filepath.Join(testDir, "main7.yaml")
mainYAML := `links:
- source: src1.txt
target: dst1.txt
from: []`
err := os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse recursively
instructions, err := ParseYAMLFileRecursive(mainConfig, testDir)
assert.NoError(t, err)
assert.Equal(t, 1, len(instructions))
assert.Contains(t, instructions[0].Source, "src1.txt")
assert.Contains(t, instructions[0].Target, "dst1.txt")
})
t.Run("ParseYAMLFileRecursive_self_reference", func(t *testing.T) {
// Create config that references itself
mainConfig := filepath.Join(testDir, "main8.yaml")
mainYAML := `links:
- source: src1.txt
target: dst1.txt
from:
- main8.yaml`
err := os.WriteFile(mainConfig, []byte(mainYAML), 0644)
assert.NoError(t, err)
// Parse should detect self-reference
_, err = ParseYAMLFileRecursive(mainConfig, testDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "circular reference detected")
})
}