diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs index e6de487b7..94f92a40b 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/CheckoutTests.cs @@ -248,7 +248,7 @@ public void CheckoutBranchAfterReadingFileAndVerifyContentsCorrect() this.FilesShouldMatchCheckoutOfSourceBranch(); // Verify modified paths contents - GVFSHelpers.ModifiedPathsContentsShouldEqual(this.Enlistment, this.FileSystem, "A .gitattributes" + GVFSHelpers.ModifiedPathsNewLine); + GVFSHelpers.ModifiedPathsShouldOnlyContain(this.Enlistment, this.FileSystem, ".gitattributes"); } [TestCase] @@ -266,7 +266,7 @@ public void CheckoutBranchAfterReadingAllFilesAndVerifyContentsCorrect() .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, compareContent: true, withinPrefixes: this.pathPrefixes); // Verify modified paths contents - GVFSHelpers.ModifiedPathsContentsShouldEqual(this.Enlistment, this.FileSystem, "A .gitattributes" + GVFSHelpers.ModifiedPathsNewLine); + GVFSHelpers.ModifiedPathsShouldOnlyContain(this.Enlistment, this.FileSystem, ".gitattributes"); } [TestCase] diff --git a/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs index d943035fb..8fdf7fe7f 100644 --- a/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs +++ b/GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs @@ -165,34 +165,35 @@ public static string ReadAllTextFromWriteLockedFile(string filename) } } - public static void ModifiedPathsContentsShouldEqual(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, string contents) + /// + /// Asserts that the modified-paths set, after replaying the on-disk + /// A/D log, contains exactly -- no more, + /// no fewer. Use this when a test wants to prove that some sequence + /// of operations produced no spurious modified-paths entries. + /// + public static void ModifiedPathsShouldOnlyContain(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, params string[] gitPaths) { - string modifedPathsContents = GetModifiedPathsContents(enlistment, fileSystem); - modifedPathsContents.ShouldEqual(contents); + HashSet currentPaths = GetCurrentModifiedPaths(enlistment, fileSystem); + HashSet expectedPaths = new HashSet(gitPaths, FileSystemHelpers.PathComparer); + currentPaths.SetEquals(expectedPaths).ShouldBeTrue( + $"Expected modified paths {{{string.Join(",", expectedPaths)}}} but got {{{string.Join(",", currentPaths)}}}"); } public static void ModifiedPathsShouldContain(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, params string[] gitPaths) { - string modifedPathsContents = GetModifiedPathsContents(enlistment, fileSystem); - string[] modifedPathLines = modifedPathsContents.Split(new[] { ModifiedPathsNewLine }, StringSplitOptions.None); + HashSet currentPaths = GetCurrentModifiedPaths(enlistment, fileSystem); foreach (string gitPath in gitPaths) { - modifedPathLines.ShouldContain(path => path.Equals(ModifedPathsLineAddPrefix + gitPath, FileSystemHelpers.PathComparison)); + currentPaths.ShouldContain(path => path.Equals(gitPath, FileSystemHelpers.PathComparison)); } } public static void ModifiedPathsShouldNotContain(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, params string[] gitPaths) { - string modifedPathsContents = GetModifiedPathsContents(enlistment, fileSystem); - string[] modifedPathLines = modifedPathsContents.Split(new[] { ModifiedPathsNewLine }, StringSplitOptions.None); + HashSet currentPaths = GetCurrentModifiedPaths(enlistment, fileSystem); foreach (string gitPath in gitPaths) { - modifedPathLines.ShouldNotContain( - path => - { - return path.Equals(ModifedPathsLineAddPrefix + gitPath, FileSystemHelpers.PathComparison) || - path.Equals(ModifedPathsLineDeletePrefix + gitPath, FileSystemHelpers.PathComparison); - }); + currentPaths.ShouldNotContain(path => path.Equals(gitPath, FileSystemHelpers.PathComparison)); } } @@ -230,6 +231,36 @@ private static string GetModifiedPathsContents(GVFSFunctionalTestEnlistment enli return GVFSHelpers.ReadAllTextFromWriteLockedFile(modifiedPathsDatabase); } + /// + /// Returns the set of currently-modified paths by replaying the on-disk + /// modified paths log. The file is append-only between background-op + /// batches; + /// compacts it after each batch finishes. Because + /// + /// can return after the last task is dequeued but before that compaction + /// completes, callers must replay the A/D log entries the same way + /// does on mount to + /// observe a consistent state. + /// + private static HashSet GetCurrentModifiedPaths(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem) + { + string contents = GetModifiedPathsContents(enlistment, fileSystem); + HashSet paths = new HashSet(FileSystemHelpers.PathComparer); + foreach (string line in contents.Split(new[] { ModifiedPathsNewLine }, StringSplitOptions.RemoveEmptyEntries)) + { + if (line.StartsWith(ModifedPathsLineAddPrefix, StringComparison.Ordinal)) + { + paths.Add(line.Substring(ModifedPathsLineAddPrefix.Length)); + } + else if (line.StartsWith(ModifedPathsLineDeletePrefix, StringComparison.Ordinal)) + { + paths.Remove(line.Substring(ModifedPathsLineDeletePrefix.Length)); + } + } + + return paths; + } + private static T RunSqliteCommand(string sqliteDbPath, Func runCommand) { string connectionString = $"data source={sqliteDbPath}"; diff --git a/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs b/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs index efe65bb0a..c8a28fd42 100644 --- a/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs +++ b/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs @@ -47,8 +47,15 @@ public static T ShouldContainSingle(this IEnumerable group, Func public static void ShouldNotContain(this IEnumerable group, Func predicate) { - T item = group.SingleOrDefault(predicate); - item.ShouldEqual(default(T), "Unexpected matching entry found in {" + string.Join(",", group) + "}"); + List matches = group.Where(predicate).ToList(); + if (matches.Count != 0) + { + Assert.Fail("Unexpected matching {0} {1}: {2} found in {{{3}}}", + matches.Count, + matches.Count == 1 ? "entry" : "entries", + string.Join(",", matches), + string.Join(",", group)); + } } public static IEnumerable ShouldNotContain(this IEnumerable group, IEnumerable unexpectedValues, Func predicate) diff --git a/GVFS/GVFS.UnitTests/Common/ModifiedPathsDatabaseTests.cs b/GVFS/GVFS.UnitTests/Common/ModifiedPathsDatabaseTests.cs index 0f7682830..9037592f0 100644 --- a/GVFS/GVFS.UnitTests/Common/ModifiedPathsDatabaseTests.cs +++ b/GVFS/GVFS.UnitTests/Common/ModifiedPathsDatabaseTests.cs @@ -135,6 +135,24 @@ public void EntryNotAddedIfParentDirectoryExists() modifiedPathsDatabase.Contains("dir2/dir", isFolder: true).ShouldBeTrue(); } + [TestCase] + public void AddFollowedByDeleteIsRecoveredOnLoad() + { + // Simulates the on-disk state during the window between a background + // operation completing and PostBackgroundOperation calling + // WriteAllEntriesAndFlush. The append log contains both the add and + // delete entries; a subsequent load must replay them and end with + // the path NOT in the modified-paths set. + const string AddThenDelete = "A temp.txt\r\nD temp.txt\r\n"; + + ModifiedPathsDatabase modifiedPathsDatabase = CreateModifiedPathsDatabase(AddThenDelete); + + // Only the auto-added .gitattributes default entry should remain. + modifiedPathsDatabase.Count.ShouldEqual(1); + modifiedPathsDatabase.Contains(DefaultEntry, isFolder: false).ShouldBeTrue(); + modifiedPathsDatabase.Contains("temp.txt", isFolder: false).ShouldBeFalse(); + } + [TestCase] public void RemoveEntriesWithParentFolderEntry() {