mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-01 18:02:34 +02:00
feat(add): implement recursive file addition with progress tracking
This commit is contained in:
26
cmd/add.go
26
cmd/add.go
@@ -17,8 +17,23 @@ func newAddCmd() *cobra.Command {
|
|||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
host, _ := cmd.Flags().GetString("host")
|
host, _ := cmd.Flags().GetString("host")
|
||||||
|
recursive, _ := cmd.Flags().GetBool("recursive")
|
||||||
lnk := core.NewLnk(core.WithHost(host))
|
lnk := core.NewLnk(core.WithHost(host))
|
||||||
|
|
||||||
|
// Handle recursive mode
|
||||||
|
if recursive {
|
||||||
|
// Create progress callback for CLI display
|
||||||
|
progressCallback := func(current, total int, currentFile string) {
|
||||||
|
printf(cmd, "\r⏳ Processing %d/%d: %s", current, total, currentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := lnk.AddRecursiveWithProgress(args, progressCallback); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear progress line and show completion
|
||||||
|
printf(cmd, "\r")
|
||||||
|
} else {
|
||||||
// Use appropriate method based on number of files
|
// Use appropriate method based on number of files
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
// Single file - use existing Add method for backward compatibility
|
// Single file - use existing Add method for backward compatibility
|
||||||
@@ -31,9 +46,17 @@ func newAddCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display results
|
// Display results
|
||||||
if len(args) == 1 {
|
if recursive {
|
||||||
|
// Recursive mode - show different message
|
||||||
|
if host != "" {
|
||||||
|
printf(cmd, "✨ \033[1mAdded files recursively to lnk (host: %s)\033[0m\n", host)
|
||||||
|
} else {
|
||||||
|
printf(cmd, "✨ \033[1mAdded files recursively to lnk\033[0m\n")
|
||||||
|
}
|
||||||
|
} else if len(args) == 1 {
|
||||||
// Single file - maintain existing output format for backward compatibility
|
// Single file - maintain existing output format for backward compatibility
|
||||||
filePath := args[0]
|
filePath := args[0]
|
||||||
basename := filepath.Base(filePath)
|
basename := filepath.Base(filePath)
|
||||||
@@ -69,5 +92,6 @@ func newAddCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringP("host", "H", "", "Manage file for specific host (default: common configuration)")
|
cmd.Flags().StringP("host", "H", "", "Manage file for specific host (default: common configuration)")
|
||||||
|
cmd.Flags().BoolP("recursive", "r", false, "Add directory contents individually instead of the directory as a whole")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
107
cmd/root_test.go
107
cmd/root_test.go
@@ -1012,6 +1012,113 @@ func (suite *CLITestSuite) TestAddCommandMixedTypes() {
|
|||||||
suite.FileExists(filepath.Join(lnkDir, ".config", "git", "config"))
|
suite.FileExists(filepath.Join(lnkDir, ".config", "git", "config"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestAddCommandRecursiveFlag() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create a directory with nested files
|
||||||
|
testDir := filepath.Join(suite.tempDir, ".config", "zed")
|
||||||
|
err = os.MkdirAll(testDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create nested files
|
||||||
|
settingsFile := filepath.Join(testDir, "settings.json")
|
||||||
|
err = os.WriteFile(settingsFile, []byte(`{"theme": "dark"}`), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
keymapFile := filepath.Join(testDir, "keymap.json")
|
||||||
|
err = os.WriteFile(keymapFile, []byte(`{"ctrl+s": "save"}`), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create a subdirectory with files
|
||||||
|
themesDir := filepath.Join(testDir, "themes")
|
||||||
|
err = os.MkdirAll(themesDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
themeFile := filepath.Join(themesDir, "custom.json")
|
||||||
|
err = os.WriteFile(themeFile, []byte(`{"colors": {}}`), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test recursive flag - should process directory contents individually
|
||||||
|
err = suite.runCommand("add", "--recursive", testDir)
|
||||||
|
suite.NoError(err, "Adding directory recursively should succeed")
|
||||||
|
|
||||||
|
// Check output shows multiple files were processed
|
||||||
|
output := suite.stdout.String()
|
||||||
|
suite.Contains(output, "Added") // Should show some success message
|
||||||
|
|
||||||
|
// Verify individual files are now symlinks (not the directory itself)
|
||||||
|
info, err := os.Lstat(settingsFile)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "settings.json should be a symlink")
|
||||||
|
|
||||||
|
info, err = os.Lstat(keymapFile)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "keymap.json should be a symlink")
|
||||||
|
|
||||||
|
info, err = os.Lstat(themeFile)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "custom.json should be a symlink")
|
||||||
|
|
||||||
|
// The directory itself should NOT be a symlink
|
||||||
|
info, err = os.Lstat(testDir)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "Directory should not be a symlink")
|
||||||
|
|
||||||
|
// Verify files exist individually in storage
|
||||||
|
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||||
|
suite.FileExists(filepath.Join(lnkDir, ".config", "zed", "settings.json"))
|
||||||
|
suite.FileExists(filepath.Join(lnkDir, ".config", "zed", "keymap.json"))
|
||||||
|
suite.FileExists(filepath.Join(lnkDir, ".config", "zed", "themes", "custom.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestAddCommandRecursiveMultipleDirs() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create two directories with files
|
||||||
|
dir1 := filepath.Join(suite.tempDir, "dir1")
|
||||||
|
dir2 := filepath.Join(suite.tempDir, "dir2")
|
||||||
|
err = os.MkdirAll(dir1, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = os.MkdirAll(dir2, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create files in each directory
|
||||||
|
file1 := filepath.Join(dir1, "file1.txt")
|
||||||
|
file2 := filepath.Join(dir2, "file2.txt")
|
||||||
|
err = os.WriteFile(file1, []byte("content1"), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = os.WriteFile(file2, []byte("content2"), 0644)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test recursive flag with multiple directories
|
||||||
|
err = suite.runCommand("add", "--recursive", dir1, dir2)
|
||||||
|
suite.NoError(err, "Adding multiple directories recursively should succeed")
|
||||||
|
|
||||||
|
// Verify both files are symlinks
|
||||||
|
info, err := os.Lstat(file1)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "file1.txt should be a symlink")
|
||||||
|
|
||||||
|
info, err = os.Lstat(file2)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "file2.txt should be a symlink")
|
||||||
|
|
||||||
|
// Verify directories are not symlinks
|
||||||
|
info, err = os.Lstat(dir1)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "dir1 should not be a symlink")
|
||||||
|
|
||||||
|
info, err = os.Lstat(dir2)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "dir2 should not be a symlink")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCLISuite(t *testing.T) {
|
func TestCLISuite(t *testing.T) {
|
||||||
suite.Run(t, new(CLITestSuite))
|
suite.Run(t, new(CLITestSuite))
|
||||||
}
|
}
|
||||||
|
@@ -827,3 +827,263 @@ func (l *Lnk) RunBootstrapScript(scriptName string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// walkDirectory walks through a directory and returns all regular files
|
||||||
|
func (l *Lnk) walkDirectory(dirPath string) ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip directories - we only want files
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle symlinks: include them as files if they point to regular files
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
// For symlinks, we'll include them but the AddMultiple logic
|
||||||
|
// will handle validation appropriately
|
||||||
|
files = append(files, path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include regular files
|
||||||
|
if info.Mode().IsRegular() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to walk directory %s: %w", dirPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgressCallback defines the signature for progress reporting callbacks
|
||||||
|
type ProgressCallback func(current, total int, currentFile string)
|
||||||
|
|
||||||
|
// AddRecursiveWithProgress adds directory contents individually with progress reporting
|
||||||
|
func (l *Lnk) AddRecursiveWithProgress(paths []string, progress ProgressCallback) error {
|
||||||
|
var allFiles []string
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
// Get absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path for %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory
|
||||||
|
info, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// Walk directory to get all files
|
||||||
|
files, err := l.walkDirectory(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to walk directory %s: %w", path, err)
|
||||||
|
}
|
||||||
|
allFiles = append(allFiles, files...)
|
||||||
|
} else {
|
||||||
|
// It's a regular file, add it directly
|
||||||
|
allFiles = append(allFiles, absPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use AddMultiple for batch processing
|
||||||
|
if len(allFiles) == 0 {
|
||||||
|
return fmt.Errorf("no files found to add")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply progress threshold: only show progress for >10 files
|
||||||
|
const progressThreshold = 10
|
||||||
|
if len(allFiles) > progressThreshold && progress != nil {
|
||||||
|
return l.addMultipleWithProgress(allFiles, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For small operations, use regular AddMultiple without progress
|
||||||
|
return l.AddMultiple(allFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMultipleWithProgress adds multiple files with progress reporting
|
||||||
|
func (l *Lnk) addMultipleWithProgress(paths []string, progress ProgressCallback) error {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: Validate all paths first (same as AddMultiple)
|
||||||
|
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 with progress reporting
|
||||||
|
var rollbackActions []func() error
|
||||||
|
total := len(absolutePaths)
|
||||||
|
|
||||||
|
for i, absPath := range absolutePaths {
|
||||||
|
// Report progress
|
||||||
|
if progress != nil {
|
||||||
|
progress(i+1, total, filepath.Base(absPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 recursively", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRecursive adds directory contents individually instead of the directory as a whole
|
||||||
|
func (l *Lnk) AddRecursive(paths []string) error {
|
||||||
|
var allFiles []string
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
// Get absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path for %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory
|
||||||
|
info, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// Walk directory to get all files
|
||||||
|
files, err := l.walkDirectory(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to walk directory %s: %w", path, err)
|
||||||
|
}
|
||||||
|
allFiles = append(allFiles, files...)
|
||||||
|
} else {
|
||||||
|
// It's a regular file, add it directly
|
||||||
|
allFiles = append(allFiles, absPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use AddMultiple for batch processing
|
||||||
|
if len(allFiles) == 0 {
|
||||||
|
return fmt.Errorf("no files found to add")
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.AddMultiple(allFiles)
|
||||||
|
}
|
||||||
|
@@ -1090,6 +1090,254 @@ func (suite *CoreTestSuite) TestDetailedErrorMessages() {
|
|||||||
suite.Contains(err.Error(), "already-managed.txt", "Error should include specific filename")
|
suite.Contains(err.Error(), "already-managed.txt", "Error should include specific filename")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Task 2.2: Directory Walking Logic Tests
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestWalkDirectory() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create nested directory structure
|
||||||
|
configDir := filepath.Join(suite.tempDir, ".config", "myapp")
|
||||||
|
err = os.MkdirAll(configDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
themeDir := filepath.Join(configDir, "themes")
|
||||||
|
err = os.MkdirAll(themeDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create files in different levels
|
||||||
|
file1 := filepath.Join(configDir, "config.json")
|
||||||
|
file2 := filepath.Join(configDir, "settings.json")
|
||||||
|
file3 := filepath.Join(themeDir, "dark.json")
|
||||||
|
file4 := filepath.Join(themeDir, "light.json")
|
||||||
|
|
||||||
|
suite.Require().NoError(os.WriteFile(file1, []byte("config"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(file2, []byte("settings"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(file3, []byte("dark theme"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(file4, []byte("light theme"), 0644))
|
||||||
|
|
||||||
|
// Call walkDirectory method (which doesn't exist yet)
|
||||||
|
files, err := suite.lnk.walkDirectory(configDir)
|
||||||
|
suite.Require().NoError(err, "walkDirectory should succeed")
|
||||||
|
|
||||||
|
// Should find all 4 files
|
||||||
|
suite.Len(files, 4, "Should find all files in nested structure")
|
||||||
|
|
||||||
|
// Check that all expected files are found (order may vary)
|
||||||
|
expectedFiles := []string{file1, file2, file3, file4}
|
||||||
|
for _, expectedFile := range expectedFiles {
|
||||||
|
suite.Contains(files, expectedFile, "Should include file %s", expectedFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestWalkDirectoryIncludesHiddenFiles() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create directory with hidden files and directories
|
||||||
|
testDir := filepath.Join(suite.tempDir, "test-hidden")
|
||||||
|
err = os.MkdirAll(testDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
hiddenDir := filepath.Join(testDir, ".hidden")
|
||||||
|
err = os.MkdirAll(hiddenDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create regular and hidden files
|
||||||
|
regularFile := filepath.Join(testDir, "regular.txt")
|
||||||
|
hiddenFile := filepath.Join(testDir, ".hidden-file")
|
||||||
|
hiddenDirFile := filepath.Join(hiddenDir, "file-in-hidden.txt")
|
||||||
|
|
||||||
|
suite.Require().NoError(os.WriteFile(regularFile, []byte("regular"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(hiddenFile, []byte("hidden"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(hiddenDirFile, []byte("in hidden dir"), 0644))
|
||||||
|
|
||||||
|
// Call walkDirectory method
|
||||||
|
files, err := suite.lnk.walkDirectory(testDir)
|
||||||
|
suite.Require().NoError(err, "walkDirectory should succeed with hidden files")
|
||||||
|
|
||||||
|
// Should find all files including hidden ones
|
||||||
|
suite.Len(files, 3, "Should find all files including hidden ones")
|
||||||
|
suite.Contains(files, regularFile, "Should include regular file")
|
||||||
|
suite.Contains(files, hiddenFile, "Should include hidden file")
|
||||||
|
suite.Contains(files, hiddenDirFile, "Should include file in hidden directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestWalkDirectorySymlinkHandling() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create directory structure
|
||||||
|
testDir := filepath.Join(suite.tempDir, "test-symlinks")
|
||||||
|
err = os.MkdirAll(testDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create a regular file
|
||||||
|
regularFile := filepath.Join(testDir, "regular.txt")
|
||||||
|
suite.Require().NoError(os.WriteFile(regularFile, []byte("regular"), 0644))
|
||||||
|
|
||||||
|
// Create a symlink to the regular file
|
||||||
|
symlinkFile := filepath.Join(testDir, "link-to-regular.txt")
|
||||||
|
err = os.Symlink(regularFile, symlinkFile)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Call walkDirectory method
|
||||||
|
files, err := suite.lnk.walkDirectory(testDir)
|
||||||
|
suite.Require().NoError(err, "walkDirectory should handle symlinks")
|
||||||
|
|
||||||
|
// Should include both regular file and properly handle symlink
|
||||||
|
// (exact behavior depends on implementation - could include symlink as file)
|
||||||
|
suite.GreaterOrEqual(len(files), 1, "Should find at least the regular file")
|
||||||
|
suite.Contains(files, regularFile, "Should include regular file")
|
||||||
|
|
||||||
|
// The symlink handling behavior will be defined in implementation
|
||||||
|
// For now, we just ensure no errors occur
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestWalkDirectoryEmptyDirs() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create directory structure with empty directories
|
||||||
|
testDir := filepath.Join(suite.tempDir, "test-empty")
|
||||||
|
err = os.MkdirAll(testDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create empty subdirectories
|
||||||
|
emptyDir1 := filepath.Join(testDir, "empty1")
|
||||||
|
emptyDir2 := filepath.Join(testDir, "empty2")
|
||||||
|
err = os.MkdirAll(emptyDir1, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
err = os.MkdirAll(emptyDir2, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create one file in a non-empty directory
|
||||||
|
nonEmptyDir := filepath.Join(testDir, "non-empty")
|
||||||
|
err = os.MkdirAll(nonEmptyDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
testFile := filepath.Join(nonEmptyDir, "test.txt")
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile, []byte("content"), 0644))
|
||||||
|
|
||||||
|
// Call walkDirectory method
|
||||||
|
files, err := suite.lnk.walkDirectory(testDir)
|
||||||
|
suite.Require().NoError(err, "walkDirectory should skip empty directories")
|
||||||
|
|
||||||
|
// Should only find the one file, not empty directories
|
||||||
|
suite.Len(files, 1, "Should only find files, not empty directories")
|
||||||
|
suite.Contains(files, testFile, "Should include the actual file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 2.3: Progress Indication System Tests
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestProgressReporting() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create directory with multiple files to test progress reporting
|
||||||
|
testDir := filepath.Join(suite.tempDir, "progress-test")
|
||||||
|
err = os.MkdirAll(testDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create 15 files to exceed threshold
|
||||||
|
expectedFiles := 15
|
||||||
|
for i := 0; i < expectedFiles; i++ {
|
||||||
|
file := filepath.Join(testDir, fmt.Sprintf("file%d.txt", i))
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track progress calls
|
||||||
|
var progressCalls []struct {
|
||||||
|
Current int
|
||||||
|
Total int
|
||||||
|
CurrentFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback := func(current, total int, currentFile string) {
|
||||||
|
progressCalls = append(progressCalls, struct {
|
||||||
|
Current int
|
||||||
|
Total int
|
||||||
|
CurrentFile string
|
||||||
|
}{
|
||||||
|
Current: current,
|
||||||
|
Total: total,
|
||||||
|
CurrentFile: currentFile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call AddRecursiveWithProgress method (which doesn't exist yet)
|
||||||
|
err = suite.lnk.AddRecursiveWithProgress([]string{testDir}, progressCallback)
|
||||||
|
suite.Require().NoError(err, "AddRecursiveWithProgress should succeed")
|
||||||
|
|
||||||
|
// Verify progress was reported
|
||||||
|
suite.Greater(len(progressCalls), 0, "Progress callback should be called")
|
||||||
|
suite.Equal(expectedFiles, len(progressCalls), "Should have progress calls for each file")
|
||||||
|
|
||||||
|
// Verify progress order and totals
|
||||||
|
for i, call := range progressCalls {
|
||||||
|
suite.Equal(i+1, call.Current, "Current count should increment")
|
||||||
|
suite.Equal(expectedFiles, call.Total, "Total should be consistent")
|
||||||
|
suite.NotEmpty(call.CurrentFile, "CurrentFile should be provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestProgressThreshold() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test with few files (under threshold)
|
||||||
|
smallDir := filepath.Join(suite.tempDir, "small-test")
|
||||||
|
err = os.MkdirAll(smallDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create only 5 files (under 10 threshold)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
file := filepath.Join(smallDir, fmt.Sprintf("small%d.txt", i))
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track progress calls for small operation
|
||||||
|
smallProgressCalls := 0
|
||||||
|
smallCallback := func(current, total int, currentFile string) {
|
||||||
|
smallProgressCalls++
|
||||||
|
}
|
||||||
|
|
||||||
|
err = suite.lnk.AddRecursiveWithProgress([]string{smallDir}, smallCallback)
|
||||||
|
suite.Require().NoError(err, "AddRecursiveWithProgress should succeed for small operation")
|
||||||
|
|
||||||
|
// Should NOT call progress for small operations
|
||||||
|
suite.Equal(0, smallProgressCalls, "Progress should not be called for operations under threshold")
|
||||||
|
|
||||||
|
// Test with many files (over threshold)
|
||||||
|
largeDir := filepath.Join(suite.tempDir, "large-test")
|
||||||
|
err = os.MkdirAll(largeDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create 15 files (over 10 threshold)
|
||||||
|
for i := 0; i < 15; i++ {
|
||||||
|
file := filepath.Join(largeDir, fmt.Sprintf("large%d.txt", i))
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track progress calls for large operation
|
||||||
|
largeProgressCalls := 0
|
||||||
|
largeCallback := func(current, total int, currentFile string) {
|
||||||
|
largeProgressCalls++
|
||||||
|
}
|
||||||
|
|
||||||
|
err = suite.lnk.AddRecursiveWithProgress([]string{largeDir}, largeCallback)
|
||||||
|
suite.Require().NoError(err, "AddRecursiveWithProgress should succeed for large operation")
|
||||||
|
|
||||||
|
// Should call progress for large operations
|
||||||
|
suite.Equal(15, largeProgressCalls, "Progress should be called for operations over threshold")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreSuite(t *testing.T) {
|
func TestCoreSuite(t *testing.T) {
|
||||||
suite.Run(t, new(CoreTestSuite))
|
suite.Run(t, new(CoreTestSuite))
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user