Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .azure-pipelines/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ parameters:

variables:
- name: 'GVFSMajorAndMinorVersion'
value: '2.0'
value: '2.1'
- name: 'GVFSRevision'
value: $(Build.BuildNumber)
- name: 'GVFSVersion'
Expand Down
24 changes: 23 additions & 1 deletion GVFS/GVFS.CommandLine.Tests/GvfsMainCliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,28 @@ public void Dehydrate_FullCommandLine_ParsesCorrectly()
Assert.That(parseResult.Errors, Is.Empty, "Full dehydrate command with --confirm --folders should parse without errors");
}

[Test]
public void Dehydrate_DiscardBackup_ParsesCorrectly()
{
var parseResult = rootCommand.Parse(new[] { "dehydrate", "--confirm", "--full", "--discard-backup" });
Assert.That(parseResult.Errors, Is.Empty, "dehydrate --confirm --full --discard-backup should parse without errors");
}

[Test]
public void Dehydrate_PruneBackups_IsSubcommand()
{
var dehydrate = FindSubcommand("dehydrate");
var pruneBackups = dehydrate.Subcommands.FirstOrDefault(c => c.Name == "prune-backups");
Assert.That(pruneBackups, Is.Not.Null, "dehydrate should have a 'prune-backups' subcommand");
}

[Test]
public void Dehydrate_PruneBackups_ParsesCorrectly()
{
var parseResult = rootCommand.Parse(new[] { "dehydrate", "prune-backups" });
Assert.That(parseResult.Errors, Is.Empty, "dehydrate prune-backups should parse without errors");
}

[Test]
public void Service_FullCommandLine_ParsesCorrectly()
{
Expand Down Expand Up @@ -353,7 +375,7 @@ public void Clone_HasAllExpectedOptions()
[Test]
public void Dehydrate_HasAllExpectedOptions()
{
var expected = new[] { "--confirm", "--no-status", "--folders" };
var expected = new[] { "--confirm", "--no-status", "--folders", "--full", "--discard-backup" };
foreach (var optName in expected)
{
Assert.That(FindOptionOnCommand("dehydrate", optName), Is.Not.Null,
Expand Down
15 changes: 14 additions & 1 deletion GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,11 @@ public Request()
{
}

public Request(string backupFolderPath, string folders)
public Request(string backupFolderPath, string folders, bool discardBackup = false)
{
this.Folders = folders;
this.BackupFolderPath = backupFolderPath;
this.DiscardBackup = discardBackup;
}

public static Request FromMessage(Message message)
Expand All @@ -228,6 +229,12 @@ public static Request FromMessage(Message message)

public string BackupFolderPath { get; set; }

/// <summary>
/// When true, the mount deletes the backup folder during its unmounted window
/// (the moved ProjFS placeholders can only be deleted while the repo is unmounted).
/// </summary>
public bool DiscardBackup { get; set; }

public Message CreateMessage()
{
return new Message(Dehydrate, GVFSJsonOptions.Serialize(this));
Expand All @@ -253,6 +260,12 @@ public Response(string result)
public List<string> SuccessfulFolders { get; set; }
public List<string> FailedFolders { get; set; }

/// <summary>
/// True if the backup folder was requested to be discarded and was successfully
/// deleted during the unmounted window.
/// </summary>
public bool BackupDiscarded { get; set; }

public static Response FromMessage(Message message)
{
return GVFSJsonOptions.Deserialize<Response>(message.Body);
Expand Down
135 changes: 130 additions & 5 deletions GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DehydrateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture
{
[TestFixture]
[SkipInCI("Atrophied: folder dehydrate behavior changed, expectations need updating")]
public class DehydrateTests : TestsWithEnlistmentPerFixture
{
private const string FolderDehydrateSuccessfulMessage = "folder dehydrate successful.";
Expand Down Expand Up @@ -58,12 +57,14 @@ public void DehydrateShouldSucceedInCommonCase()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FullDehydrateShouldExitWithoutConfirm()
{
this.DehydrateShouldSucceed(new[] { "To actually execute the dehydrate, run 'gvfs dehydrate --confirm --full'" }, confirm: false, noStatus: false, full: true);
}

[TestCase]
[SkipInCI("Shared EnlistmentPerFixture cannot run consecutive full dehydrates reliably (only the first succeeds; later backups fail). Needs per-test-case enlistment; triage in follow-up.")]
public void FullDehydrateShouldSucceedInCommonCase()
{
this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false, full: true);
Expand All @@ -77,6 +78,7 @@ public void DehydrateShouldFailOnUnmountedRepoWithStatus()
}

[TestCase]
[SkipInCI("Shared EnlistmentPerFixture cannot run consecutive full dehydrates reliably (only the first succeeds; later backups fail). Needs per-test-case enlistment; triage in follow-up.")]
public void DehydrateShouldSucceedEvenIfObjectCacheIsDeleted()
{
this.Enlistment.UnmountGVFS();
Expand All @@ -85,6 +87,7 @@ public void DehydrateShouldSucceedEvenIfObjectCacheIsDeleted()
}

[TestCase]
[SkipInCI("Atrophied: backup src now includes an extra 'databases' folder; expected layout is out of date. Fix in follow-up.")]
public void DehydrateShouldBackupFiles()
{
this.DehydrateShouldSucceed(new[] { "The repo was successfully dehydrated and remounted" }, confirm: true, noStatus: false, full: true);
Expand All @@ -106,6 +109,72 @@ public void DehydrateShouldBackupFiles()
this.DirectoryShouldContain(gvfsDatabasesFolder, "BackgroundGitOperations.dat", "ModifiedPaths.dat", "VFSForGit.sqlite");
}

[TestCase]
public void FolderDehydrateWithDiscardBackupShouldDeleteBackup()
{
// Hydrate files under the folder first so the dehydrate moves real ProjFS placeholders
// into the backup. Those placeholders can only be deleted while the repo is unmounted,
// which is exactly what --discard-backup does (it deletes during the dehydrate's
// unmounted window). Without hydration the backup has no placeholders and would not
// exercise this path.
this.HydrateFolder("GVFS");

this.DehydrateShouldSucceed(
new[] { "folder dehydrate successful", "(--discard-backup)" },
confirm: true,
noStatus: false,
full: false,
discardBackup: true,
foldersToDehydrate: "GVFS");

string backupFolder = Path.Combine(this.Enlistment.EnlistmentRoot, "dehydrate_backup");
backupFolder.ShouldNotExistOnDisk(this.fileSystem);
}

[TestCase]
public void DehydratePruneBackupsShouldDeleteExistingBackupsWhenUnmounted()
{
this.HydrateFolder("GVFS");

this.DehydrateShouldSucceed(new[] { "folder dehydrate successful" }, confirm: true, noStatus: false, full: false, foldersToDehydrate: "GVFS");

string backupFolder = Path.Combine(this.Enlistment.EnlistmentRoot, "dehydrate_backup");
backupFolder.ShouldBeADirectory(this.fileSystem);

// A backup with moved placeholders can only be pruned while unmounted.
this.Enlistment.UnmountGVFS();

ProcessResult result = this.RunDehydratePruneBackupsProcess();
result.ExitCode.ShouldEqual(0, $"prune-backups exit code was {result.ExitCode}. Output: {result.Output}");
result.Output.ShouldContain(new[] { "Pruned" });

backupFolder.ShouldNotExistOnDisk(this.fileSystem);

this.Enlistment.MountGVFS();
}

[TestCase]
public void DehydratePruneBackupsWhileMountedReportsUnmountGuidance()
{
this.HydrateFolder("GVFS");

this.DehydrateShouldSucceed(new[] { "folder dehydrate successful" }, confirm: true, noStatus: false, full: false, foldersToDehydrate: "GVFS");

string backupFolder = Path.Combine(this.Enlistment.EnlistmentRoot, "dehydrate_backup");
backupFolder.ShouldBeADirectory(this.fileSystem);

// Pruning while mounted cannot delete the placeholder backup; the verb should fail and
// tell the user to unmount first.
ProcessResult result = this.RunDehydratePruneBackupsProcess();
result.ExitCode.ShouldEqual(GVFSGenericError, $"prune-backups should fail while mounted. Output: {result.Output}");
result.Output.ShouldContain(new[] { "Run 'gvfs unmount'" });

// Clean up the leftover backup while unmounted so it does not leak into later tests.
this.Enlistment.UnmountGVFS();
RepositoryHelpers.DeleteTestDirectory(backupFolder);
this.Enlistment.MountGVFS();
}

[TestCase]
public void DehydrateShouldFailIfLocalCacheNotInMetadata()
{
Expand Down Expand Up @@ -182,6 +251,7 @@ public void DehydrateShouldFailOnWrongDiskLayoutVersion()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateFolderThatWasEnumerated()
{
string folderToDehydrate = "GVFS";
Expand All @@ -200,6 +270,7 @@ public void FolderDehydrateFolderThatWasEnumerated()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateFolderWithFilesThatWerePlaceholders()
{
string folderToDehydrate = "GVFS";
Expand All @@ -220,6 +291,7 @@ public void FolderDehydrateFolderWithFilesThatWerePlaceholders()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateFolderWithFilesThatWereRead()
{
string folderToDehydrate = "GVFS";
Expand All @@ -238,6 +310,7 @@ public void FolderDehydrateFolderWithFilesThatWereRead()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateFolderWithFilesThatWereWrittenTo()
{
string folderToDehydrate = "GVFS";
Expand All @@ -257,6 +330,7 @@ public void FolderDehydrateFolderWithFilesThatWereWrittenTo()
}

[TestCase]
[SkipInCI("Hangs locally (ProjFS 'provider temporarily unavailable'). Triage in follow-up.")]
public void FolderDehydrateFolderThatWasDeleted()
{
string folderToDehydrate = "Scripts";
Expand All @@ -274,6 +348,7 @@ public void FolderDehydrateFolderThatWasDeleted()
}

[TestCase]
[SkipInCI("Atrophied/flaky: locked-folder dehydrate returns exit 7 with ProjFS teardown errors. Fix in follow-up.")]
public void FolderDehydrateFolderThatIsLocked()
{
const string folderToDehydrate = "GVFS";
Expand Down Expand Up @@ -330,6 +405,7 @@ public void FolderDehydrateFolderThatIsSubstringOfExistingFolder()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateNestedFoldersChildBeforeParent()
{
string parentFolderToDehydrate = "GVFS";
Expand All @@ -354,6 +430,7 @@ public void FolderDehydrateNestedFoldersChildBeforeParent()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateNestedFoldersParentBeforeChild()
{
string parentFolderToDehydrate = "GVFS";
Expand All @@ -378,6 +455,7 @@ public void FolderDehydrateNestedFoldersParentBeforeChild()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateParentFolderInModifiedPathsShouldOutputMessage()
{
string folderToDehydrateParentFolder = "GitCommandsTests";
Expand All @@ -404,6 +482,7 @@ public void FolderDehydrateDirtyStatusShouldFail()
}

[TestCase]
[SkipInCI("Atrophied: dehydrate with --no-status now succeeds (exit 0) instead of failing (exit 3). Fix in follow-up.")]
public void FolderDehydrateDirtyStatusWithNoStatusShouldFail()
{
string folderToDehydrate = "GVFS";
Expand All @@ -423,6 +502,7 @@ public void FolderDehydrateCannotDehydrateDotGitFolder()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydratePreviouslyDeletedFolders()
{
string folderToDehydrate = "TrailingSlashTests";
Expand Down Expand Up @@ -457,6 +537,7 @@ public void FolderDehydratePreviouslyDeletedFolders()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateTombstone()
{
string folderToDehydrate = "TrailingSlashTests";
Expand All @@ -472,6 +553,7 @@ public void FolderDehydrateTombstone()
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateRelativePaths()
{
string[] foldersToDehydrate = new[]
Expand All @@ -496,13 +578,15 @@ public void FolderDehydrateRelativePaths()
}

[TestCase]
[SkipInCI("Atrophied: dehydrating a nonexistent folder now reports success instead of an error. Fix in follow-up.")]
public void FolderDehydrateFolderThatDoesNotExist()
{
string folderToDehydrate = "DoesNotExist";
this.DehydrateShouldSucceed(new[] { $"Cannot dehydrate folder '{folderToDehydrate}': '{folderToDehydrate}' does not exist." }, confirm: true, noStatus: false, foldersToDehydrate: folderToDehydrate);
}

[TestCase]
[SkipInCI("Unverified: fixture hangs on ProjFS before this test runs. Triage in follow-up.")]
public void FolderDehydrateNewlyCreatedFolderAndFile()
{
string folderToDehydrate = "NewFolder";
Expand Down Expand Up @@ -570,9 +654,9 @@ private void CheckDehydratedFolderAfterUnmount(string path)
}
}

private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, bool noStatus, bool full = false, params string[] foldersToDehydrate)
private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, bool noStatus, bool full = false, bool discardBackup = false, params string[] foldersToDehydrate)
{
ProcessResult result = this.RunDehydrateProcess(confirm, noStatus, full, foldersToDehydrate);
ProcessResult result = this.RunDehydrateProcess(confirm, noStatus, full, discardBackup, foldersToDehydrate);
result.ExitCode.ShouldEqual(0, $"mount exit code was {result.ExitCode}. Output: {result.Output}");

if (result.Output.Contains("Failed to move the src folder: Access to the path"))
Expand All @@ -586,12 +670,12 @@ private void DehydrateShouldSucceed(string[] expectedInOutput, bool confirm, boo

private void DehydrateShouldFail(string[] expectedErrorMessages, bool noStatus, bool full = false, params string[] foldersToDehydrate)
{
ProcessResult result = this.RunDehydrateProcess(confirm: true, noStatus: noStatus, full: full, foldersToDehydrate: foldersToDehydrate);
ProcessResult result = this.RunDehydrateProcess(confirm: true, noStatus: noStatus, full: full, discardBackup: false, foldersToDehydrate: foldersToDehydrate);
result.ExitCode.ShouldEqual(GVFSGenericError, $"mount exit code was not {GVFSGenericError}");
result.Output.ShouldContain(expectedErrorMessages);
}

private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, bool full = false, params string[] foldersToDehydrate)
private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, bool full = false, bool discardBackup = false, params string[] foldersToDehydrate)
{
string dehydrateFlags = string.Empty;
if (confirm)
Expand All @@ -609,6 +693,11 @@ private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, bool full
dehydrateFlags += " --full ";
}

if (discardBackup)
{
dehydrateFlags += " --discard-backup ";
}

if (foldersToDehydrate.Length > 0)
{
dehydrateFlags += $" --folders {string.Join(";", foldersToDehydrate)}";
Expand All @@ -626,6 +715,42 @@ private ProcessResult RunDehydrateProcess(bool confirm, bool noStatus, bool full
return ProcessHelper.Run(processInfo);
}

private ProcessResult RunDehydratePruneBackupsProcess()
{
ProcessStartInfo processInfo = new ProcessStartInfo(GVFSTestConfig.PathToGVFS);
processInfo.Arguments = "dehydrate prune-backups " + TestConstants.InternalUseOnlyFlag + " " + GVFSHelpers.GetInternalParameter();
processInfo.WindowStyle = ProcessWindowStyle.Hidden;
processInfo.WorkingDirectory = this.Enlistment.EnlistmentRoot;
processInfo.UseShellExecute = false;
processInfo.RedirectStandardOutput = true;

return ProcessHelper.Run(processInfo);
}

// Hydrate some files under the given root-level folder so that a subsequent folder
// dehydrate moves real ProjFS placeholders into the backup.
private void HydrateFolder(string folder)
{
string folderPath = Path.Combine(this.Enlistment.RepoRoot, folder);
int hydrated = 0;
foreach (string file in Directory.EnumerateFiles(folderPath, "*", SearchOption.AllDirectories))
{
try
{
File.ReadAllBytes(file);
hydrated++;
}
catch
{
}

if (hydrated >= 25)
{
break;
}
}
}

private SafeFileHandle OpenFolderHandle(string path)
{
return NativeMethods.CreateFile(
Expand Down
Loading
Loading