426 lines
13 KiB
Go
426 lines
13 KiB
Go
package core
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLinkFile(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
|
|
src := filepath.Join(tmp, "source.txt")
|
|
dst := filepath.Join(tmp, "link.txt")
|
|
|
|
// Create source file
|
|
if err := os.WriteFile(src, []byte("hello"), 0o644); err != nil {
|
|
t.Fatalf("failed to create source: %v", err)
|
|
}
|
|
|
|
// Test: create symlink
|
|
if err := LinkFile(src, dst); err != nil {
|
|
t.Errorf("LinkFile failed: %v", err)
|
|
}
|
|
|
|
// Verify symlink
|
|
info, err := os.Lstat(dst)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat link: %v", err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink == 0 {
|
|
t.Error("dst is not a symlink")
|
|
}
|
|
|
|
// Test: idempotent - same symlink again should succeed
|
|
if err := LinkFile(src, dst); err != nil {
|
|
t.Errorf("LinkFile second time failed: %v", err)
|
|
}
|
|
|
|
// Test: conflict - different source should error
|
|
src2 := filepath.Join(tmp, "source2.txt")
|
|
if err := os.WriteFile(src2, []byte("world"), 0o644); err != nil {
|
|
t.Fatalf("failed to create source2: %v", err)
|
|
}
|
|
|
|
if err := LinkFile(src2, dst); err == nil {
|
|
t.Error("LinkFile should fail with conflicting symlink")
|
|
}
|
|
|
|
// Test: conflict - non-symlink file should error
|
|
dst2 := filepath.Join(tmp, "regular.txt")
|
|
if err := os.WriteFile(dst2, []byte("content"), 0o644); err != nil {
|
|
t.Fatalf("failed to create regular file: %v", err)
|
|
}
|
|
|
|
if err := LinkFile(src, dst2); err == nil {
|
|
t.Error("LinkFile should fail when target exists and is not a symlink")
|
|
}
|
|
}
|
|
|
|
func TestFindStaleLinks(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
filesDir := filepath.Join(tmp, "files")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
if err := os.MkdirAll(filesDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create files dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create target dir: %v", err)
|
|
}
|
|
|
|
// Create a file in files/
|
|
src := filepath.Join(filesDir, "test.txt")
|
|
if err := os.WriteFile(src, []byte("content"), 0o644); err != nil {
|
|
t.Fatalf("failed to create source: %v", err)
|
|
}
|
|
|
|
// Create a subdirectory with a file
|
|
subDir := filepath.Join(filesDir, "subdir")
|
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create subdir: %v", err)
|
|
}
|
|
src2 := filepath.Join(subDir, "nested.txt")
|
|
if err := os.WriteFile(src2, []byte("nested"), 0o644); err != nil {
|
|
t.Fatalf("failed to create nested source: %v", err)
|
|
}
|
|
|
|
// Create symlinks
|
|
dst := filepath.Join(targetDir, "test.txt")
|
|
if err := os.Symlink(src, dst); err != nil {
|
|
t.Fatalf("failed to create symlink: %v", err)
|
|
}
|
|
|
|
targetSubDir := filepath.Join(targetDir, "subdir")
|
|
if err := os.MkdirAll(targetSubDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create target subdir: %v", err)
|
|
}
|
|
dst2 := filepath.Join(targetSubDir, "nested.txt")
|
|
if err := os.Symlink(src2, dst2); err != nil {
|
|
t.Fatalf("failed to create nested symlink: %v", err)
|
|
}
|
|
|
|
// Test: no stale links when sources exist
|
|
stale, err := FindStaleLinks(filesDir, targetDir)
|
|
if err != nil {
|
|
t.Errorf("FindStaleLinks failed: %v", err)
|
|
}
|
|
if len(stale) != 0 {
|
|
t.Errorf("expected 0 stale links, got %d", len(stale))
|
|
}
|
|
|
|
// Note: The current implementation walks filesDir, so it can't detect
|
|
// stale links for files that have been deleted from the repo.
|
|
// This is a known limitation - we'd need to walk the target directory
|
|
// to find orphaned symlinks, but that has performance issues with
|
|
// large target directories (like ~).
|
|
}
|
|
|
|
func TestFindStaleLinks_IgnoresNonRepoSymlinks(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
filesDir := filepath.Join(tmp, "files")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
if err := os.MkdirAll(filesDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create files dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create target dir: %v", err)
|
|
}
|
|
|
|
// Create a file outside the repo
|
|
external := filepath.Join(tmp, "external.txt")
|
|
if err := os.WriteFile(external, []byte("external"), 0o644); err != nil {
|
|
t.Fatalf("failed to create external file: %v", err)
|
|
}
|
|
|
|
// Create a symlink to external file (should be ignored by FindStaleLinks)
|
|
dst := filepath.Join(targetDir, "link.txt")
|
|
if err := os.Symlink(external, dst); err != nil {
|
|
t.Fatalf("failed to create symlink: %v", err)
|
|
}
|
|
|
|
// Test: should not report external symlinks as stale
|
|
stale, err := FindStaleLinks(filesDir, targetDir)
|
|
if err != nil {
|
|
t.Errorf("FindStaleLinks failed: %v", err)
|
|
}
|
|
if len(stale) != 0 {
|
|
t.Errorf("expected 0 stale links (external symlinks ignored), got %d", len(stale))
|
|
}
|
|
}
|
|
|
|
func TestApplyPackage(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
filesDir := filepath.Join(tmp, "files")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
// Create files structure
|
|
subDir := filepath.Join(filesDir, "config")
|
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create subdir: %v", err)
|
|
}
|
|
|
|
file1 := filepath.Join(filesDir, "readme.txt")
|
|
if err := os.WriteFile(file1, []byte("readme"), 0o644); err != nil {
|
|
t.Fatalf("failed to create file1: %v", err)
|
|
}
|
|
|
|
file2 := filepath.Join(subDir, "app.conf")
|
|
if err := os.WriteFile(file2, []byte("config"), 0o644); err != nil {
|
|
t.Fatalf("failed to create file2: %v", err)
|
|
}
|
|
|
|
cfg := &packageConfig{} // empty config, no ignores
|
|
|
|
// Apply package
|
|
if err := ApplyPackage(filesDir, targetDir, cfg); err != nil {
|
|
t.Errorf("ApplyPackage failed: %v", err)
|
|
}
|
|
|
|
// Verify symlinks exist
|
|
targetFile1 := filepath.Join(targetDir, "readme.txt")
|
|
info, err := os.Lstat(targetFile1)
|
|
if err != nil {
|
|
t.Errorf("target file1 not found: %v", err)
|
|
} else if info.Mode()&os.ModeSymlink == 0 {
|
|
t.Error("target file1 is not a symlink")
|
|
}
|
|
|
|
targetFile2 := filepath.Join(targetDir, "config", "app.conf")
|
|
info, err = os.Lstat(targetFile2)
|
|
if err != nil {
|
|
t.Errorf("target file2 not found: %v", err)
|
|
} else if info.Mode()&os.ModeSymlink == 0 {
|
|
t.Error("target file2 is not a symlink")
|
|
}
|
|
|
|
// Verify content is accessible through symlink
|
|
content, err := os.ReadFile(targetFile1)
|
|
if err != nil {
|
|
t.Errorf("failed to read through symlink: %v", err)
|
|
}
|
|
if string(content) != "readme" {
|
|
t.Errorf("wrong content through symlink: %s", string(content))
|
|
}
|
|
}
|
|
|
|
func TestApplyPackage_WithIgnores(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
filesDir := filepath.Join(tmp, "files")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
if err := os.MkdirAll(filesDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create files dir: %v", err)
|
|
}
|
|
|
|
// Create files, some to be ignored
|
|
mainFile := filepath.Join(filesDir, "main.txt")
|
|
if err := os.WriteFile(mainFile, []byte("main"), 0o644); err != nil {
|
|
t.Fatalf("failed to create main file: %v", err)
|
|
}
|
|
|
|
cacheDir := filepath.Join(filesDir, "cache")
|
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create cache dir: %v", err)
|
|
}
|
|
cacheFile := filepath.Join(cacheDir, "data.tmp")
|
|
if err := os.WriteFile(cacheFile, []byte("temp"), 0o644); err != nil {
|
|
t.Fatalf("failed to create cache file: %v", err)
|
|
}
|
|
|
|
// Create config with ignore pattern
|
|
cfg := &packageConfig{
|
|
compiledIgnores: []*regexp.Regexp{},
|
|
}
|
|
re, _ := GlobToRegexp("cache/**")
|
|
cfg.compiledIgnores = append(cfg.compiledIgnores, re)
|
|
|
|
// Apply package
|
|
if err := ApplyPackage(filesDir, targetDir, cfg); err != nil {
|
|
t.Errorf("ApplyPackage failed: %v", err)
|
|
}
|
|
|
|
// Verify main file is linked
|
|
targetMain := filepath.Join(targetDir, "main.txt")
|
|
if _, err := os.Lstat(targetMain); err != nil {
|
|
t.Errorf("main file should be linked: %v", err)
|
|
}
|
|
|
|
// Verify cache file is not linked (the directory may exist since we walk it
|
|
// before checking ignore patterns, but it should be empty)
|
|
targetCacheFile := filepath.Join(targetDir, "cache", "data.tmp")
|
|
if _, err := os.Lstat(targetCacheFile); !os.IsNotExist(err) {
|
|
t.Error("cache/data.tmp should not exist in target")
|
|
}
|
|
}
|
|
|
|
func TestRestoreOne(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
filesDir := filepath.Join(tmp, "files")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
if err := os.MkdirAll(filesDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create files dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create target dir: %v", err)
|
|
}
|
|
|
|
// Create source file in repo
|
|
src := filepath.Join(filesDir, "test.txt")
|
|
if err := os.WriteFile(src, []byte("original"), 0o644); err != nil {
|
|
t.Fatalf("failed to create source: %v", err)
|
|
}
|
|
|
|
// Create symlink in target
|
|
dst := filepath.Join(targetDir, "test.txt")
|
|
if err := os.Symlink(src, dst); err != nil {
|
|
t.Fatalf("failed to create symlink: %v", err)
|
|
}
|
|
|
|
// Restore (removes symlink, copies file)
|
|
filesAbs, _ := filepath.Abs(filesDir)
|
|
if err := RestoreOne(src, dst, filesAbs, false); err != nil {
|
|
t.Errorf("RestoreOne failed: %v", err)
|
|
}
|
|
|
|
// Verify symlink is gone
|
|
info, err := os.Lstat(dst)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat restored file: %v", err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
t.Error("file should not be a symlink after restore")
|
|
}
|
|
|
|
// Verify content is preserved
|
|
content, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Errorf("failed to read restored file: %v", err)
|
|
}
|
|
if string(content) != "original" {
|
|
t.Errorf("wrong content after restore: %s", string(content))
|
|
}
|
|
}
|
|
|
|
func TestApplyPackages_Overlay(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
baseDir := filepath.Join(tmp, "files")
|
|
linuxDir := filepath.Join(tmp, "files.linux")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
// Create base files
|
|
if err := os.MkdirAll(baseDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create base dir: %v", err)
|
|
}
|
|
baseFile := filepath.Join(baseDir, "common.txt")
|
|
if err := os.WriteFile(baseFile, []byte("common"), 0o644); err != nil {
|
|
t.Fatalf("failed to create base file: %v", err)
|
|
}
|
|
sharedFile := filepath.Join(baseDir, "shared.txt")
|
|
if err := os.WriteFile(sharedFile, []byte("shared"), 0o644); err != nil {
|
|
t.Fatalf("failed to create shared file: %v", err)
|
|
}
|
|
|
|
// Create Linux-specific override
|
|
if err := os.MkdirAll(linuxDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create linux dir: %v", err)
|
|
}
|
|
linuxFile := filepath.Join(linuxDir, "common.txt") // overrides base
|
|
if err := os.WriteFile(linuxFile, []byte("linux"), 0o644); err != nil {
|
|
t.Fatalf("failed to create linux file: %v", err)
|
|
}
|
|
|
|
cfg := &packageConfig{}
|
|
filesDirs := []string{baseDir, linuxDir}
|
|
|
|
// Apply both directories
|
|
if err := ApplyPackages(filesDirs, targetDir, cfg); err != nil {
|
|
t.Errorf("ApplyPackages failed: %v", err)
|
|
}
|
|
|
|
// Verify shared file from base is linked
|
|
targetShared := filepath.Join(targetDir, "shared.txt")
|
|
content, err := os.ReadFile(targetShared)
|
|
if err != nil {
|
|
t.Errorf("shared file not found: %v", err)
|
|
} else if string(content) != "shared" {
|
|
t.Errorf("shared file wrong content: %s", string(content))
|
|
}
|
|
|
|
// Verify common.txt is overridden by Linux variant
|
|
targetCommon := filepath.Join(targetDir, "common.txt")
|
|
content, err = os.ReadFile(targetCommon)
|
|
if err != nil {
|
|
t.Errorf("common file not found: %v", err)
|
|
} else if string(content) != "linux" {
|
|
t.Errorf("common file should be overridden by linux variant, got: %s", string(content))
|
|
}
|
|
|
|
// Verify symlink points to linux variant
|
|
src, err := os.Readlink(targetCommon)
|
|
if err != nil {
|
|
t.Errorf("failed to read symlink: %v", err)
|
|
} else if !strings.HasSuffix(src, "files.linux/common.txt") {
|
|
t.Errorf("symlink should point to linux variant, got: %s", src)
|
|
}
|
|
}
|
|
|
|
func TestFindStaleLinksMulti_WithVariants(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
baseDir := filepath.Join(tmp, "files")
|
|
linuxDir := filepath.Join(tmp, "files.linux")
|
|
targetDir := filepath.Join(tmp, "target")
|
|
|
|
if err := os.MkdirAll(baseDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create base dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(linuxDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create linux dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
|
t.Fatalf("failed to create target dir: %v", err)
|
|
}
|
|
|
|
// Create files
|
|
baseFile := filepath.Join(baseDir, "base.txt")
|
|
if err := os.WriteFile(baseFile, []byte("base"), 0o644); err != nil {
|
|
t.Fatalf("failed to create base file: %v", err)
|
|
}
|
|
linuxFile := filepath.Join(linuxDir, "linux.txt")
|
|
if err := os.WriteFile(linuxFile, []byte("linux"), 0o644); err != nil {
|
|
t.Fatalf("failed to create linux file: %v", err)
|
|
}
|
|
|
|
// Create symlinks
|
|
targetBase := filepath.Join(targetDir, "base.txt")
|
|
if err := os.Symlink(baseFile, targetBase); err != nil {
|
|
t.Fatalf("failed to create base symlink: %v", err)
|
|
}
|
|
targetLinux := filepath.Join(targetDir, "linux.txt")
|
|
if err := os.Symlink(linuxFile, targetLinux); err != nil {
|
|
t.Fatalf("failed to create linux symlink: %v", err)
|
|
}
|
|
|
|
// Delete the linux source file to create a stale link
|
|
if err := os.Remove(linuxFile); err != nil {
|
|
t.Fatalf("failed to remove linux file: %v", err)
|
|
}
|
|
|
|
filesDirs := []string{baseDir, linuxDir}
|
|
stale, err := FindStaleLinksMulti(filesDirs, targetDir)
|
|
if err != nil {
|
|
t.Errorf("FindStaleLinksMulti failed: %v", err)
|
|
}
|
|
|
|
if len(stale) != 1 {
|
|
t.Errorf("expected 1 stale link, got %d", len(stale))
|
|
}
|
|
if len(stale) > 0 && stale[0] != targetLinux {
|
|
t.Errorf("expected stale link to be %s, got %s", targetLinux, stale[0])
|
|
}
|
|
}
|