Implement a "from" to config files that loads other files
This commit is contained in:
6
go.mod
6
go.mod
@@ -4,10 +4,12 @@ go 1.21.7
|
|||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
||||||
require github.com/bmatcuk/doublestar/v4 v4.8.1
|
require (
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
@@ -20,6 +20,7 @@ type LinkInstruction struct {
|
|||||||
|
|
||||||
type YAMLConfig struct {
|
type YAMLConfig struct {
|
||||||
Links []LinkInstruction `yaml:"links"`
|
Links []LinkInstruction `yaml:"links"`
|
||||||
|
From []string `yaml:"from,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instruction *LinkInstruction) Tidy() {
|
func (instruction *LinkInstruction) Tidy() {
|
||||||
@@ -318,9 +319,76 @@ func ParseYAMLFile(filename, workdir string) ([]LinkInstruction, error) {
|
|||||||
return expanded, nil
|
return expanded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseYAMLFileRecursive parses a YAML file and recursively processes any "From" references
|
||||||
|
func ParseYAMLFileRecursive(filename, workdir string) ([]LinkInstruction, error) {
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
return parseYAMLFileRecursive(filename, workdir, visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseYAMLFileRecursive is the internal recursive function that tracks visited files to prevent cycles
|
||||||
|
func parseYAMLFileRecursive(filename, workdir string, visited map[string]bool) ([]LinkInstruction, error) {
|
||||||
|
// Normalize the filename to prevent cycles with different path representations
|
||||||
|
normalizedFilename, err := filepath.Abs(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error normalizing filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cycles
|
||||||
|
if visited[normalizedFilename] {
|
||||||
|
return nil, fmt.Errorf("circular reference detected: %s", filename)
|
||||||
|
}
|
||||||
|
visited[normalizedFilename] = true
|
||||||
|
defer delete(visited, normalizedFilename)
|
||||||
|
|
||||||
|
// Parse the current file
|
||||||
|
instructions, err := ParseYAMLFile(filename, workdir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file to check for "From" references
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading YAML file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config YAMLConfig
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
// If parsing as YAMLConfig fails, there are no "From" references to process
|
||||||
|
return instructions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process "From" references
|
||||||
|
for _, fromFile := range config.From {
|
||||||
|
// Convert relative paths to absolute paths based on the current file's directory
|
||||||
|
fromPath := fromFile
|
||||||
|
if !filepath.IsAbs(fromPath) {
|
||||||
|
currentDir := filepath.Dir(filename)
|
||||||
|
fromPath = filepath.Join(currentDir, fromPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the path
|
||||||
|
fromPath = filepath.Clean(fromPath)
|
||||||
|
|
||||||
|
// Recursively parse the referenced file
|
||||||
|
// Use the directory of the referenced file as the workdir for pattern expansion
|
||||||
|
fromWorkdir := filepath.Dir(fromPath)
|
||||||
|
fromInstructions, err := parseYAMLFileRecursive(fromPath, fromWorkdir, visited)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing referenced file %s: %w", fromFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the instructions from the referenced file
|
||||||
|
instructions = append(instructions, fromInstructions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instructions, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ExpandPattern(source, workdir, target string) (links []LinkInstruction, err error) {
|
func ExpandPattern(source, workdir, target string) (links []LinkInstruction, err error) {
|
||||||
static, pattern := doublestar.SplitPattern(source)
|
static, pattern := doublestar.SplitPattern(source)
|
||||||
if static == "" {
|
if static == "" || static == "." {
|
||||||
static = workdir
|
static = workdir
|
||||||
}
|
}
|
||||||
LogInfo("Static part: %s", static)
|
LogInfo("Static part: %s", static)
|
||||||
|
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1450,18 +1451,6 @@ func TestErrorPaths(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "invalid format")
|
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) {
|
t.Run("ReadFromArgs_no_args", func(t *testing.T) {
|
||||||
// Test ReadFromArgs with no command line arguments
|
// Test ReadFromArgs with no command line arguments
|
||||||
instructions := make(chan *LinkInstruction, 10)
|
instructions := make(chan *LinkInstruction, 10)
|
||||||
@@ -1665,18 +1654,6 @@ func TestErrorPaths(t *testing.T) {
|
|||||||
// FormatErrorMessage just formats the message, no "ERROR" prefix
|
// 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) {
|
t.Run("RunAsync_error_handling", func(t *testing.T) {
|
||||||
// Test RunAsync error handling with invalid source
|
// Test RunAsync error handling with invalid source
|
||||||
@@ -1738,7 +1715,7 @@ func TestErrorPaths(t *testing.T) {
|
|||||||
assert.Empty(t, links)
|
assert.Empty(t, links)
|
||||||
|
|
||||||
// Invalid pattern - this actually returns an error
|
// 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.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "syntax error in pattern")
|
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
|
// 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) {
|
t.Run("startDefaultInputSource_with_sync_file", func(t *testing.T) {
|
||||||
// Create sync file
|
// Create sync file
|
||||||
syncFile := filepath.Join(testDir, "sync")
|
syncFile := filepath.Join(testDir, "sync")
|
||||||
@@ -1939,7 +1910,6 @@ func TestMainFunctionsCoverage(t *testing.T) {
|
|||||||
|
|
||||||
// This confirms that startDefaultInputSource would call showUsageAndExit()
|
// This confirms that startDefaultInputSource would call showUsageAndExit()
|
||||||
// We can't actually call it because os.Exit(1) terminates the process
|
// 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) {
|
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
|
// 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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
2
main.go
2
main.go
@@ -231,7 +231,7 @@ func ReadFromFile(input string, output chan *LinkInstruction, status chan error,
|
|||||||
// Check if this is a YAML file
|
// Check if this is a YAML file
|
||||||
if IsYAMLFile(input) {
|
if IsYAMLFile(input) {
|
||||||
LogInfo("Parsing as YAML file")
|
LogInfo("Parsing as YAML file")
|
||||||
instructions, err := ParseYAMLFile(input, filepath.Dir(input))
|
instructions, err := ParseYAMLFileRecursive(input, filepath.Dir(input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Failed to parse YAML file %s: %v",
|
LogError("Failed to parse YAML file %s: %v",
|
||||||
FormatSourcePath(input), err)
|
FormatSourcePath(input), err)
|
||||||
|
Reference in New Issue
Block a user