From dffce2022bab339784aa74d9af839f160a603722 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 4 Feb 2026 09:57:57 -0800 Subject: [PATCH] Add functional test to repro delete/restore issue --- GVFS/GVFS.FunctionalTests/Program.cs | 5 +++ .../GitCommands/CorruptionScenarioTests.cs | 35 +++++++++++++++++++ .../Tests/GitCommands/GitRepoTests.cs | 17 +++++++++ .../Tools/ProcessHelper.cs | 9 +++++ 4 files changed, 66 insertions(+) create mode 100644 GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionScenarioTests.cs diff --git a/GVFS/GVFS.FunctionalTests/Program.cs b/GVFS/GVFS.FunctionalTests/Program.cs index f00d9496a8..56940a2b7a 100644 --- a/GVFS/GVFS.FunctionalTests/Program.cs +++ b/GVFS/GVFS.FunctionalTests/Program.cs @@ -21,6 +21,11 @@ public static void Main(string[] args) NUnitRunner runner = new NUnitRunner(args); runner.AddGlobalSetupIfNeeded("GVFS.FunctionalTests.GlobalSetup"); + if (runner.HasCustomArg("--debug")) + { + Debugger.Launch(); + } + if (runner.HasCustomArg("--no-shared-gvfs-cache")) { Console.WriteLine("Running without a shared git object cache"); diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionScenarioTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionScenarioTests.cs new file mode 100644 index 0000000000..e4a08d1237 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CorruptionScenarioTests.cs @@ -0,0 +1,35 @@ +using GVFS.FunctionalTests.Properties; +using NUnit.Framework; + +namespace GVFS.FunctionalTests.Tests.GitCommands +{ + /// + /// This class is used to reproduce corruption scenarios in the GVFS virtual projection. + /// + [Category(Categories.GitCommands)] + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + public class CorruptionReproTests : GitRepoTests + { + public CorruptionReproTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + /// + /// Reproduction of a reported issue: + /// Restoring a file after its parent directory was deleted fails with + /// "fatal: could not unlink 'path\to\': Directory not empty" + /// + [TestCase] + public void RestoreAfterDeleteNesteredDirectory() + { + // Delete a directory with nested subdirectories and files. + this.ValidateNonGitCommand("cmd.exe", "/c \"rmdir /s /q GVFlt_DeleteFileTest\""); + + // Restore the working directory. + this.ValidateGitCommand("restore ."); + + this.FilesShouldMatchCheckoutOfSourceBranch(); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs index 2b902117fb..d7a22fa280 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs @@ -269,6 +269,23 @@ protected void ValidateGitCommand(string command, params object[] args) args); } + protected void ValidateNonGitCommand(string command, string args = "", bool ignoreErrors = false, bool checkStatus = true) + { + string controlRepoRoot = this.ControlGitRepo.RootPath; + string gvfsRepoRoot = this.Enlistment.RepoRoot; + + ProcessResult expectedResult = ProcessHelper.Run(command, args, controlRepoRoot); + ProcessResult actualResult = ProcessHelper.Run(command, args, gvfsRepoRoot); + if (!ignoreErrors) + { + GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult); + } + if (checkStatus) + { + this.ValidateGitCommand("status"); + } + } + protected void ChangeMode(string filePath, ushort mode) { string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); diff --git a/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs b/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs index 664c1e2545..539c5cc82c 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs @@ -6,6 +6,11 @@ namespace GVFS.FunctionalTests.Tools public static class ProcessHelper { public static ProcessResult Run(string fileName, string arguments) + { + return Run(fileName, arguments, null); + } + + public static ProcessResult Run(string fileName, string arguments, string workingDirectory) { ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.UseShellExecute = false; @@ -14,6 +19,10 @@ public static ProcessResult Run(string fileName, string arguments) startInfo.CreateNoWindow = true; startInfo.FileName = fileName; startInfo.Arguments = arguments; + if (!string.IsNullOrEmpty(workingDirectory)) + { + startInfo.WorkingDirectory = workingDirectory; + } return Run(startInfo); }