## NimPak Filesystem Management Tests ## ## This module contains unit tests for the filesystem management functionality, ## focusing on the GoboLinux-style directory structure and atomic symlink operations. import unittest, os, strutils, strformat import nimpak/[types, filesystem] # Create a temporary test directory let testDir = getTempDir() / "nimpak_test_filesystem" let programsRoot = testDir / "Programs" let indexRoot = testDir / "System/Index" let logFile = testDir / "filesystem.log" # Setup and teardown helpers proc setupTestDir() = if dirExists(testDir): removeDir(testDir) createDir(programsRoot) createDir(indexRoot) proc teardownTestDir() = if dirExists(testDir): removeDir(testDir) suite "FilesystemManager Tests": setup: setupTestDir() let fm = newFilesystemManager( programsRoot = programsRoot, indexRoot = indexRoot, logFile = logFile, dryRun = false ) teardown: teardownTestDir() test "Create FilesystemManager with custom paths": let customFm = newFilesystemManager( programsRoot = "/custom/Programs", indexRoot = "/custom/Index", logFile = "/custom/logs/fs.log", dryRun = true ) check customFm.programsRoot == "/custom/Programs" check customFm.indexRoot == "/custom/Index" check customFm.logFile == "/custom/logs/fs.log" check customFm.dryRun == true test "Create program directory structure": let pkg = PackageId(name: "test", version: "1.0.0", stream: Stable) let result = fm.createProgramDirectory(pkg) check result.isOk let programDir = result.get() check programDir == joinPath(programsRoot, "Test", "1.0.0") check dirExists(programDir) # Check standard subdirectories for subdir in ["bin", "lib", "share", "etc", "var"]: check dirExists(joinPath(programDir, subdir)) test "Remove program directory": let pkg = PackageId(name: "test", version: "1.0.0", stream: Stable) let createResult = fm.createProgramDirectory(pkg) check createResult.isOk let programDir = createResult.get() check dirExists(programDir) let removeResult = fm.removeProgramDirectory(pkg) check removeResult.isOk check not dirExists(programDir) test "Create and remove symlink": # Create a test file let sourceDir = joinPath(programsRoot, "Test", "1.0.0", "bin") createDir(sourceDir) let sourceFile = joinPath(sourceDir, "testapp") writeFile(sourceFile, "test content") let targetFile = joinPath(indexRoot, "bin", "testapp") # Create symlink let createResult = fm.createSymlink(sourceFile, targetFile) check createResult.isOk check symlinkExists(targetFile) check expandSymlink(targetFile) == sourceFile # Remove symlink let removeResult = fm.removeSymlink(targetFile) check removeResult.isOk check not symlinkExists(targetFile) test "Atomic symlink update with rollback": # Create test files let sourceDir = joinPath(programsRoot, "Test", "1.0.0", "bin") createDir(sourceDir) let sourceFile1 = joinPath(sourceDir, "app1") let sourceFile2 = joinPath(sourceDir, "app2") writeFile(sourceFile1, "app1 content") writeFile(sourceFile2, "app2 content") let targetFile1 = joinPath(indexRoot, "bin", "app1") let targetFile2 = joinPath(indexRoot, "bin", "app2") # Create symlink pairs let pairs = @[ SymlinkPair(source: sourceFile1, target: targetFile1), SymlinkPair(source: sourceFile2, target: targetFile2) ] # Perform atomic update let result = fm.atomicSymlinkUpdate(pairs) check result.isOk # Verify symlinks were created check symlinkExists(targetFile1) check symlinkExists(targetFile2) check expandSymlink(targetFile1) == sourceFile1 check expandSymlink(targetFile2) == sourceFile2 test "Generate symlinks for program directory": # Create test program directory with files let programDir = joinPath(programsRoot, "Test", "1.0.0") createDir(programDir) for dir in ["bin", "lib", "share", "etc"]: let dirPath = joinPath(programDir, dir) createDir(dirPath) # Create a test file in each directory writeFile(joinPath(dirPath, fmt"test_{dir}"), fmt"test content for {dir}") # Generate symlinks let result = fm.generateSymlinks(programDir) check result.isOk let symlinks = result.get() check symlinks.len == 4 # One for each directory # Verify symlink structure for pair in symlinks: check pair.source.startsWith(programDir) check pair.target.startsWith(indexRoot) test "Install and uninstall package": let pkg = PackageId(name: "testapp", version: "2.0.0", stream: Stable) # Install package let installResult = fm.installPackage(pkg) check installResult.isOk let location = installResult.get() check dirExists(location.programDir) # Create some test files to generate real symlinks let binDir = joinPath(location.programDir, "bin") let appPath = joinPath(binDir, "testapp") writeFile(appPath, "test app content") # Re-generate symlinks let symlinksResult = fm.generateSymlinks(location.programDir) check symlinksResult.isOk let symlinks = symlinksResult.get() # Apply symlinks let updateResult = fm.atomicSymlinkUpdate(symlinks) check updateResult.isOk # Verify symlink was created let indexBinPath = joinPath(indexRoot, "bin", "testapp") check symlinkExists(indexBinPath) # Uninstall package let uninstallResult = fm.uninstallPackage(pkg) check uninstallResult.isOk # Verify directory and symlinks were removed check not dirExists(location.programDir) check not symlinkExists(indexBinPath) test "Validate filesystem state": # Create a broken symlink let sourceDir = joinPath(programsRoot, "Test", "1.0.0", "bin") createDir(sourceDir) let sourceFile = joinPath(sourceDir, "testapp") writeFile(sourceFile, "test content") let targetDir = joinPath(indexRoot, "bin") createDir(targetDir) let targetFile = joinPath(targetDir, "testapp") # Create symlink discard fm.createSymlink(sourceFile, targetFile) # Remove source file to create broken symlink removeFile(sourceFile) # Validate filesystem state let result = fm.validateFilesystemState() check result.isOk let issues = result.get() if issues.len == 0: fail() else: check issues.len >= 1 # At least one issue (broken symlink) check issues[0].contains("Broken symlink") test "Repair filesystem": # Remove root directories removeDir(programsRoot) removeDir(indexRoot) # Create a broken symlink # createDir(indexRoot / "bin") # REMOVED THIS LINE createSymlink("/tmp/nonexistent_path_for_testing", indexRoot / "bin" / "broken") # Repair filesystem let result = fm.repairFilesystem() check result.isOk let repairs = result.get() check repairs.len >= 2 # At least created programs root and removed broken symlink # Verify repairs check dirExists(programsRoot) check dirExists(indexRoot) check not symlinkExists(indexRoot / "bin" / "broken") test "Dry run mode": let dryRunFm = newFilesystemManager( programsRoot = programsRoot, indexRoot = indexRoot, logFile = logFile, dryRun = true ) # Remove directories to test dry run removeDir(programsRoot) removeDir(indexRoot) let pkg = PackageId(name: "drytest", version: "1.0.0", stream: Stable) let result = dryRunFm.createProgramDirectory(pkg) # Should succeed in dry run mode check result.isOk # But directory should not actually be created check not dirExists(joinPath(programsRoot, "Drytest", "1.0.0")) test "Error handling for invalid paths": let invalidFm = newFilesystemManager( programsRoot = "/nonexistent/invalid/path", indexRoot = indexRoot, logFile = logFile, dryRun = false ) let pkg = PackageId(name: "test", version: "1.0.0", stream: Stable) let result = invalidFm.createProgramDirectory(pkg) check result.isErr let error = result.getError() check error.kind == DirectoryCreationFailed