mirror of
https://github.com/yarlson/lnk.git
synced 2025-08-31 18:01:41 +02:00
feat(add): implement multiple file addition with atomic operation
This commit is contained in:
44
cmd/add.go
44
cmd/add.go
@@ -9,22 +9,33 @@ import (
|
||||
|
||||
func newAddCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <file>",
|
||||
Short: "✨ Add a file to lnk management",
|
||||
Long: "Moves a file to the lnk repository and creates a symlink in its place.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "add <file>...",
|
||||
Short: "✨ Add files to lnk management",
|
||||
Long: "Moves files to the lnk repository and creates symlinks in their place. Supports multiple files.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := args[0]
|
||||
host, _ := cmd.Flags().GetString("host")
|
||||
|
||||
lnk := core.NewLnk(core.WithHost(host))
|
||||
|
||||
if err := lnk.Add(filePath); err != nil {
|
||||
// Use appropriate method based on number of files
|
||||
if len(args) == 1 {
|
||||
// Single file - use existing Add method for backward compatibility
|
||||
if err := lnk.Add(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Multiple files - use AddMultiple for atomic operation
|
||||
if err := lnk.AddMultiple(args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Display results
|
||||
if len(args) == 1 {
|
||||
// Single file - maintain existing output format for backward compatibility
|
||||
filePath := args[0]
|
||||
basename := filepath.Base(filePath)
|
||||
if host != "" {
|
||||
printf(cmd, "✨ \033[1mAdded %s to lnk (host: %s)\033[0m\n", basename, host)
|
||||
@@ -33,6 +44,25 @@ func newAddCmd() *cobra.Command {
|
||||
printf(cmd, "✨ \033[1mAdded %s to lnk\033[0m\n", basename)
|
||||
printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s\033[0m\n", filePath, filePath)
|
||||
}
|
||||
} else {
|
||||
// Multiple files - show summary
|
||||
if host != "" {
|
||||
printf(cmd, "✨ \033[1mAdded %d items to lnk (host: %s)\033[0m\n", len(args), host)
|
||||
} else {
|
||||
printf(cmd, "✨ \033[1mAdded %d items to lnk\033[0m\n", len(args))
|
||||
}
|
||||
|
||||
// List each added file
|
||||
for _, filePath := range args {
|
||||
basename := filepath.Base(filePath)
|
||||
if host != "" {
|
||||
printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s.lnk/...\033[0m\n", basename, host)
|
||||
} else {
|
||||
printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/...\033[0m\n", basename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf(cmd, " 📝 Use \033[1mlnk push\033[0m to sync to remote\n")
|
||||
return nil
|
||||
},
|
||||
|
109
cmd/root_test.go
109
cmd/root_test.go
@@ -277,7 +277,7 @@ func (suite *CLITestSuite) TestErrorHandling() {
|
||||
name: "add help",
|
||||
args: []string{"add", "--help"},
|
||||
wantErr: false,
|
||||
outContains: "Moves a file to the lnk repository",
|
||||
outContains: "Moves files to the lnk repository",
|
||||
},
|
||||
{
|
||||
name: "list help",
|
||||
@@ -790,8 +790,8 @@ func (suite *CLITestSuite) TestInitWithBootstrap() {
|
||||
err := os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo in remote
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
// Initialize git repo in remote with main branch
|
||||
cmd := exec.Command("git", "init", "--bare", "--initial-branch=main")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
@@ -835,7 +835,7 @@ touch remote-bootstrap-ran.txt
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "push", "origin", "master")
|
||||
cmd = exec.Command("git", "push", "origin", "main")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
@@ -863,8 +863,8 @@ func (suite *CLITestSuite) TestInitWithBootstrapDisabled() {
|
||||
err := os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo in remote
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
// Initialize git repo in remote with main branch
|
||||
cmd := exec.Command("git", "init", "--bare", "--initial-branch=main")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
@@ -898,7 +898,7 @@ touch should-not-exist.txt
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "push", "origin", "master")
|
||||
cmd = exec.Command("git", "push", "origin", "main")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
@@ -917,6 +917,101 @@ touch should-not-exist.txt
|
||||
suite.NoFileExists(markerFile)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestAddCommandMultipleFiles() {
|
||||
// Initialize repository
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create multiple test files
|
||||
testFile1 := filepath.Join(suite.tempDir, ".bashrc")
|
||||
err = os.WriteFile(testFile1, []byte("export PATH1"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile2 := filepath.Join(suite.tempDir, ".vimrc")
|
||||
err = os.WriteFile(testFile2, []byte("set number"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile3 := filepath.Join(suite.tempDir, ".gitconfig")
|
||||
err = os.WriteFile(testFile3, []byte("[user]\n name = test"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test add command with multiple files - should succeed
|
||||
err = suite.runCommand("add", testFile1, testFile2, testFile3)
|
||||
suite.NoError(err, "Adding multiple files should succeed")
|
||||
|
||||
// Check output shows all files were added
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Added 3 items to lnk")
|
||||
suite.Contains(output, ".bashrc")
|
||||
suite.Contains(output, ".vimrc")
|
||||
suite.Contains(output, ".gitconfig")
|
||||
|
||||
// Verify all files are now symlinks
|
||||
for _, file := range []string{testFile1, testFile2, testFile3} {
|
||||
info, err := os.Lstat(file)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
}
|
||||
|
||||
// Verify all files exist in storage
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
suite.FileExists(filepath.Join(lnkDir, ".bashrc"))
|
||||
suite.FileExists(filepath.Join(lnkDir, ".vimrc"))
|
||||
suite.FileExists(filepath.Join(lnkDir, ".gitconfig"))
|
||||
|
||||
// Verify .lnk file contains all entries
|
||||
lnkFile := filepath.Join(lnkDir, ".lnk")
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(".bashrc\n.gitconfig\n.vimrc\n", string(lnkContent))
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestAddCommandMixedTypes() {
|
||||
// Initialize repository
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create a file
|
||||
testFile := filepath.Join(suite.tempDir, ".vimrc")
|
||||
err = os.WriteFile(testFile, []byte("set number"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a directory with content
|
||||
testDir := filepath.Join(suite.tempDir, ".config", "git")
|
||||
err = os.MkdirAll(testDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
configFile := filepath.Join(testDir, "config")
|
||||
err = os.WriteFile(configFile, []byte("[user]"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test add command with mixed files and directories - should succeed
|
||||
err = suite.runCommand("add", testFile, testDir)
|
||||
suite.NoError(err, "Adding mixed files and directories should succeed")
|
||||
|
||||
// Check output shows both items were added
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Added 2 items to lnk")
|
||||
suite.Contains(output, ".vimrc")
|
||||
suite.Contains(output, "git")
|
||||
|
||||
// Verify both are now symlinks
|
||||
info1, err := os.Lstat(testFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info1.Mode()&os.ModeSymlink)
|
||||
|
||||
info2, err := os.Lstat(testDir)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info2.Mode()&os.ModeSymlink)
|
||||
|
||||
// Verify storage
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
suite.FileExists(filepath.Join(lnkDir, ".vimrc"))
|
||||
suite.DirExists(filepath.Join(lnkDir, ".config", "git"))
|
||||
suite.FileExists(filepath.Join(lnkDir, ".config", "git", "config"))
|
||||
}
|
||||
|
||||
func TestCLISuite(t *testing.T) {
|
||||
suite.Run(t, new(CLITestSuite))
|
||||
}
|
||||
|
@@ -257,6 +257,153 @@ func (l *Lnk) Add(filePath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddMultiple adds multiple files or directories to the repository in a single transaction
|
||||
func (l *Lnk) AddMultiple(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Phase 1: Validate all paths first
|
||||
var relativePaths []string
|
||||
var absolutePaths []string
|
||||
var infos []os.FileInfo
|
||||
|
||||
for _, filePath := range paths {
|
||||
// Validate the file or directory
|
||||
if err := l.fs.ValidateFileForAdd(filePath); err != nil {
|
||||
return fmt.Errorf("validation failed for %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Get absolute path
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path for %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Get relative path for tracking
|
||||
relativePath, err := getRelativePath(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative path for %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Check if this relative path is already managed
|
||||
managedItems, err := l.getManagedItems()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get managed items: %w", err)
|
||||
}
|
||||
for _, item := range managedItems {
|
||||
if item == relativePath {
|
||||
return fmt.Errorf("❌ File is already managed by lnk: \033[31m%s\033[0m", relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat path %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
relativePaths = append(relativePaths, relativePath)
|
||||
absolutePaths = append(absolutePaths, absPath)
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
// Phase 2: Process all files - move to repository and create symlinks
|
||||
var rollbackActions []func() error
|
||||
|
||||
for i, absPath := range absolutePaths {
|
||||
relativePath := relativePaths[i]
|
||||
info := infos[i]
|
||||
|
||||
// Generate repository path from relative path
|
||||
storagePath := l.getHostStoragePath()
|
||||
destPath := filepath.Join(storagePath, relativePath)
|
||||
|
||||
// Ensure destination directory exists
|
||||
destDir := filepath.Dir(destPath)
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
// Rollback previous operations
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
// Move to repository
|
||||
if err := l.fs.Move(absPath, destPath, info); err != nil {
|
||||
// Rollback previous operations
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to move %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
if err := l.fs.CreateSymlink(destPath, absPath); err != nil {
|
||||
// Try to restore the file we just moved, then rollback others
|
||||
_ = l.fs.Move(destPath, absPath, info)
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to create symlink for %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
// Add to tracking
|
||||
if err := l.addManagedItem(relativePath); err != nil {
|
||||
// Restore this file and rollback others
|
||||
_ = os.Remove(absPath)
|
||||
_ = l.fs.Move(destPath, absPath, info)
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to update tracking file for %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
// Add rollback action for this file
|
||||
rollbackAction := l.createRollbackAction(absPath, destPath, relativePath, info)
|
||||
rollbackActions = append(rollbackActions, rollbackAction)
|
||||
}
|
||||
|
||||
// Phase 3: Git operations - add all files and create single commit
|
||||
for i, relativePath := range relativePaths {
|
||||
// For host-specific files, we need to add the relative path from repo root
|
||||
gitPath := relativePath
|
||||
if l.host != "" {
|
||||
gitPath = filepath.Join(l.host+".lnk", relativePath)
|
||||
}
|
||||
if err := l.git.Add(gitPath); err != nil {
|
||||
// Rollback all operations
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to add %s to git: %w", absolutePaths[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add .lnk file to the same commit
|
||||
if err := l.git.Add(l.getLnkFileName()); err != nil {
|
||||
// Rollback all operations
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to add tracking file to git: %w", err)
|
||||
}
|
||||
|
||||
// Commit all changes together
|
||||
commitMessage := fmt.Sprintf("lnk: added %d files", len(paths))
|
||||
if err := l.git.Commit(commitMessage); err != nil {
|
||||
// Rollback all operations
|
||||
l.rollbackOperations(rollbackActions)
|
||||
return fmt.Errorf("failed to commit changes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createRollbackAction creates a rollback function for a single file operation
|
||||
func (l *Lnk) createRollbackAction(absPath, destPath, relativePath string, info os.FileInfo) func() error {
|
||||
return func() error {
|
||||
_ = os.Remove(absPath)
|
||||
_ = l.removeManagedItem(relativePath)
|
||||
return l.fs.Move(destPath, absPath, info)
|
||||
}
|
||||
}
|
||||
|
||||
// rollbackOperations executes rollback actions in reverse order
|
||||
func (l *Lnk) rollbackOperations(rollbackActions []func() error) {
|
||||
for i := len(rollbackActions) - 1; i >= 0; i-- {
|
||||
_ = rollbackActions[i]()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes a symlink and restores the original file or directory
|
||||
func (l *Lnk) Remove(filePath string) error {
|
||||
// Get absolute path
|
||||
|
@@ -31,6 +31,9 @@ func (suite *CoreTestSuite) SetupTest() {
|
||||
err = os.Chdir(tempDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Set HOME to temp directory for consistent relative path calculation
|
||||
suite.T().Setenv("HOME", tempDir)
|
||||
|
||||
// Set XDG_CONFIG_HOME to temp directory
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", tempDir)
|
||||
|
||||
@@ -86,8 +89,8 @@ func (suite *CoreTestSuite) TestCoreFileOperations() {
|
||||
// The repository file will preserve the directory structure
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
|
||||
// Find the .bashrc file in the repository (it should be at the relative path)
|
||||
repoFile := filepath.Join(lnkDir, suite.tempDir, ".bashrc")
|
||||
// Find the .bashrc file in the repository (it should be at the relative path from HOME)
|
||||
repoFile := filepath.Join(lnkDir, ".bashrc")
|
||||
suite.FileExists(repoFile)
|
||||
|
||||
// Verify content is preserved
|
||||
@@ -137,8 +140,8 @@ func (suite *CoreTestSuite) TestCoreDirectoryOperations() {
|
||||
// Check that the repository directory preserves the structure
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
|
||||
// The directory should be at the relative path
|
||||
repoDir := filepath.Join(lnkDir, suite.tempDir, "testdir")
|
||||
// The directory should be at the relative path from HOME
|
||||
repoDir := filepath.Join(lnkDir, "testdir")
|
||||
suite.DirExists(repoDir)
|
||||
|
||||
// Remove the directory
|
||||
@@ -820,6 +823,273 @@ func (suite *CoreTestSuite) TestRunBootstrapScriptNotFound() {
|
||||
suite.Contains(err.Error(), "Bootstrap script not found")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestAddMultiple() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create multiple test files
|
||||
file1 := filepath.Join(suite.tempDir, "file1.txt")
|
||||
file2 := filepath.Join(suite.tempDir, "file2.txt")
|
||||
file3 := filepath.Join(suite.tempDir, "file3.txt")
|
||||
|
||||
content1 := "content1"
|
||||
content2 := "content2"
|
||||
content3 := "content3"
|
||||
|
||||
err = os.WriteFile(file1, []byte(content1), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file2, []byte(content2), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file3, []byte(content3), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test AddMultiple method - should succeed
|
||||
paths := []string{file1, file2, file3}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.NoError(err, "AddMultiple should succeed")
|
||||
|
||||
// Verify all files are now symlinks
|
||||
for _, file := range paths {
|
||||
info, err := os.Lstat(file)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "File should be a symlink: %s", file)
|
||||
}
|
||||
|
||||
// Verify all files exist in storage
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
suite.FileExists(filepath.Join(lnkDir, "file1.txt"))
|
||||
suite.FileExists(filepath.Join(lnkDir, "file2.txt"))
|
||||
suite.FileExists(filepath.Join(lnkDir, "file3.txt"))
|
||||
|
||||
// Verify .lnk file contains all entries
|
||||
lnkFile := filepath.Join(lnkDir, ".lnk")
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal("file1.txt\nfile2.txt\nfile3.txt\n", string(lnkContent))
|
||||
|
||||
// Verify Git commit was created
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.NoError(err)
|
||||
suite.T().Logf("Commits: %v", commits)
|
||||
// Should have at least 1 commit for the batch add
|
||||
suite.GreaterOrEqual(len(commits), 1)
|
||||
// The most recent commit should mention multiple files
|
||||
suite.Contains(commits[0], "added 3 files")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestAddMultipleWithConflicts() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create test files
|
||||
file1 := filepath.Join(suite.tempDir, "file1.txt")
|
||||
file2 := filepath.Join(suite.tempDir, "file2.txt")
|
||||
file3 := filepath.Join(suite.tempDir, "file3.txt")
|
||||
|
||||
err = os.WriteFile(file1, []byte("content1"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file2, []byte("content2"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file3, []byte("content3"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add file2 individually first
|
||||
err = suite.lnk.Add(file2)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Now try to add all three - should fail due to conflict with file2
|
||||
paths := []string{file1, file2, file3}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.Error(err, "AddMultiple should fail due to conflict")
|
||||
suite.Contains(err.Error(), "already managed")
|
||||
|
||||
// Verify no partial changes were made
|
||||
// file1 and file3 should still be regular files, not symlinks
|
||||
info1, err := os.Lstat(file1)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info1.Mode()&os.ModeSymlink, "file1 should not be a symlink")
|
||||
|
||||
info3, err := os.Lstat(file3)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info3.Mode()&os.ModeSymlink, "file3 should not be a symlink")
|
||||
|
||||
// file2 should still be managed (was added before)
|
||||
info2, err := os.Lstat(file2)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info2.Mode()&os.ModeSymlink, "file2 should remain a symlink")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestAddMultipleRollback() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create test files - one will be invalid to force rollback
|
||||
file1 := filepath.Join(suite.tempDir, "file1.txt")
|
||||
file2 := filepath.Join(suite.tempDir, "nonexistent.txt") // This doesn't exist
|
||||
file3 := filepath.Join(suite.tempDir, "file3.txt")
|
||||
|
||||
err = os.WriteFile(file1, []byte("content1"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file3, []byte("content3"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
// Note: file2 is intentionally not created
|
||||
|
||||
// Try to add all files - should fail and rollback
|
||||
paths := []string{file1, file2, file3}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.Error(err, "AddMultiple should fail due to nonexistent file")
|
||||
|
||||
// Verify rollback - no files should be symlinks
|
||||
info1, err := os.Lstat(file1)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info1.Mode()&os.ModeSymlink, "file1 should not be a symlink after rollback")
|
||||
|
||||
info3, err := os.Lstat(file3)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info3.Mode()&os.ModeSymlink, "file3 should not be a symlink after rollback")
|
||||
|
||||
// Verify no files in storage
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
suite.NoFileExists(filepath.Join(lnkDir, "file1.txt"))
|
||||
suite.NoFileExists(filepath.Join(lnkDir, "file3.txt"))
|
||||
|
||||
// Verify .lnk file is empty or doesn't contain these files
|
||||
lnkFile := filepath.Join(lnkDir, ".lnk")
|
||||
if _, err := os.Stat(lnkFile); err == nil {
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.NoError(err)
|
||||
content := string(lnkContent)
|
||||
suite.NotContains(content, "file1.txt")
|
||||
suite.NotContains(content, "file3.txt")
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestValidateMultiplePaths() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a mix of valid and invalid paths
|
||||
validFile := filepath.Join(suite.tempDir, "valid.txt")
|
||||
err = os.WriteFile(validFile, []byte("content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
nonexistentFile := filepath.Join(suite.tempDir, "nonexistent.txt")
|
||||
// Don't create this file
|
||||
|
||||
// Create a valid directory
|
||||
validDir := filepath.Join(suite.tempDir, "validdir")
|
||||
err = os.MkdirAll(validDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test validation fails early with detailed error
|
||||
paths := []string{validFile, nonexistentFile, validDir}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.Error(err, "Should fail due to nonexistent file")
|
||||
suite.Contains(err.Error(), "validation failed")
|
||||
suite.Contains(err.Error(), "nonexistent.txt")
|
||||
|
||||
// Verify no partial changes were made
|
||||
info, err := os.Lstat(validFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "valid file should not be a symlink")
|
||||
|
||||
info, err = os.Lstat(validDir)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "valid directory should not be a symlink")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestAtomicRollbackOnFailure() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create test files
|
||||
file1 := filepath.Join(suite.tempDir, "file1.txt")
|
||||
file2 := filepath.Join(suite.tempDir, "file2.txt")
|
||||
file3 := filepath.Join(suite.tempDir, "file3.txt")
|
||||
|
||||
content1 := "original content 1"
|
||||
content2 := "original content 2"
|
||||
content3 := "original content 3"
|
||||
|
||||
err = os.WriteFile(file1, []byte(content1), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file2, []byte(content2), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(file3, []byte(content3), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add file2 individually first to create a conflict
|
||||
err = suite.lnk.Add(file2)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Store original states
|
||||
info1Before, err := os.Lstat(file1)
|
||||
suite.Require().NoError(err)
|
||||
info3Before, err := os.Lstat(file3)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Try to add all files - should fail and rollback completely
|
||||
paths := []string{file1, file2, file3}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.Error(err, "Should fail due to conflict with file2")
|
||||
|
||||
// Verify complete rollback
|
||||
info1After, err := os.Lstat(file1)
|
||||
suite.NoError(err)
|
||||
suite.Equal(info1Before.Mode(), info1After.Mode(), "file1 mode should be unchanged")
|
||||
|
||||
info3After, err := os.Lstat(file3)
|
||||
suite.NoError(err)
|
||||
suite.Equal(info3Before.Mode(), info3After.Mode(), "file3 mode should be unchanged")
|
||||
|
||||
// Verify original contents are preserved
|
||||
content1After, err := os.ReadFile(file1)
|
||||
suite.NoError(err)
|
||||
suite.Equal(content1, string(content1After), "file1 content should be preserved")
|
||||
|
||||
content3After, err := os.ReadFile(file3)
|
||||
suite.NoError(err)
|
||||
suite.Equal(content3, string(content3After), "file3 content should be preserved")
|
||||
|
||||
// file2 should still be managed (was added before)
|
||||
info2, err := os.Lstat(file2)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info2.Mode()&os.ModeSymlink, "file2 should remain a symlink")
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TestDetailedErrorMessages() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test with multiple types of errors
|
||||
validFile := filepath.Join(suite.tempDir, "valid.txt")
|
||||
err = os.WriteFile(validFile, []byte("content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
nonexistentFile := filepath.Join(suite.tempDir, "does-not-exist.txt")
|
||||
alreadyManagedFile := filepath.Join(suite.tempDir, "already-managed.txt")
|
||||
err = os.WriteFile(alreadyManagedFile, []byte("managed"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add one file first to create conflict
|
||||
err = suite.lnk.Add(alreadyManagedFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test with nonexistent file
|
||||
paths := []string{validFile, nonexistentFile}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.Error(err, "Should fail due to nonexistent file")
|
||||
suite.Contains(err.Error(), "validation failed", "Error should mention validation failure")
|
||||
suite.Contains(err.Error(), "does-not-exist.txt", "Error should include specific filename")
|
||||
|
||||
// Test with already managed file
|
||||
paths = []string{validFile, alreadyManagedFile}
|
||||
err = suite.lnk.AddMultiple(paths)
|
||||
suite.Error(err, "Should fail due to already managed file")
|
||||
suite.Contains(err.Error(), "already managed", "Error should mention already managed")
|
||||
suite.Contains(err.Error(), "already-managed.txt", "Error should include specific filename")
|
||||
}
|
||||
|
||||
func TestCoreSuite(t *testing.T) {
|
||||
suite.Run(t, new(CoreTestSuite))
|
||||
}
|
||||
|
Reference in New Issue
Block a user