diff --git a/src/Verify.Tests/Converters/ExtensionConverterTests.InvalidFileCharactersInName#name/a/b-c.verified.txt b/src/Verify.Tests/Converters/ExtensionConverterTests.InvalidFileCharactersInName#name/a/b-c.verified.txt deleted file mode 100644 index 3c877c57a0..0000000000 --- a/src/Verify.Tests/Converters/ExtensionConverterTests.InvalidFileCharactersInName#name/a/b-c.verified.txt +++ /dev/null @@ -1 +0,0 @@ -value \ No newline at end of file diff --git a/src/Verify.Tests/Naming/MatchingFileFinderTests.cs b/src/Verify.Tests/Naming/MatchingFileFinderTests.cs index 056fa9a7d6..28b38caf8a 100644 --- a/src/Verify.Tests/Naming/MatchingFileFinderTests.cs +++ b/src/Verify.Tests/Naming/MatchingFileFinderTests.cs @@ -44,4 +44,49 @@ public void StrayFileWithNoExtensionIsIgnored() Assert.Single(found); Assert.EndsWith("SomeTest.verified.txt", found[0]); } + + // A target name can contain a directory separator, which puts the file below a + // `{prefix}#` directory where a flat scan cannot see it + [Fact] + public void FindVerifiedIncludesNested() + { + using var directory = new TempDirectory(); + File.WriteAllText(directory.BuildPath("SomeTest.verified.txt"), "a"); + var nested = WriteNested(directory, "SomeTest#sub", "inner", "file.verified.txt"); + // not a file Verify writes, and the `.*` in the search pattern matches it + WriteNested(directory, "SomeTest#sub", "extensionless.verified"); + // a different test that happens to start with the same text + var otherNested = WriteNested(directory, "SomeTestOther#sub", "file.verified.txt"); + + var found = MatchingFileFinder.FindVerified("SomeTest", directory) + .ToList(); + + Assert.Equal(2, found.Count); + Assert.Contains(nested, found); + Assert.DoesNotContain(otherNested, found); + } + + [Fact] + public void DeleteReceivedIncludesNested() + { + using var directory = new TempDirectory(); + var nestedReceived = WriteNested(directory, "SomeTest#sub", "inner", "file.received.txt"); + var nestedVerified = WriteNested(directory, "SomeTest#sub", "inner", "file.verified.txt"); + var otherReceived = WriteNested(directory, "SomeTestOther#sub", "file.received.txt"); + + MatchingFileFinder.DeleteReceived("SomeTest", directory); + + Assert.False(File.Exists(nestedReceived)); + // only received files are swept, and only for this test + Assert.True(File.Exists(nestedVerified)); + Assert.True(File.Exists(otherReceived)); + } + + static string WriteNested(TempDirectory directory, params string[] segments) + { + var path = Path.Combine([directory.Path, ..segments]); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, "content"); + return path; + } } diff --git a/src/Verify/Naming/MatchingFileFinder.cs b/src/Verify/Naming/MatchingFileFinder.cs index a7169fc64a..22a4e117a3 100644 --- a/src/Verify/Naming/MatchingFileFinder.cs +++ b/src/Verify/Naming/MatchingFileFinder.cs @@ -1,25 +1,17 @@ -static class MatchingFileFinder +static class MatchingFileFinder { public static void DeleteReceived(string fileNamePrefix, string directory) { - foreach (var file in Find( - directory, - searchPattern: $"{fileNamePrefix}*.received.*", - nonIndexedPattern: $"{fileNamePrefix}.received.", - indexedPattern: $"{fileNamePrefix}#")) + foreach (var file in Find(directory, fileNamePrefix, "received")) { IoHelpers.DeleteFile(file); } } public static IEnumerable FindVerified(string fileNamePrefix, string directory) => - Find( - directory, - searchPattern: $"{fileNamePrefix}*.verified.*", - nonIndexedPattern: $"{fileNamePrefix}.verified.", - indexedPattern: $"{fileNamePrefix}#"); + Find(directory, fileNamePrefix, "verified"); - static List Find(string directory, string searchPattern, string nonIndexedPattern, string indexedPattern) + static List Find(string directory, string fileNamePrefix, string marker) { // Directory.EnumerateFiles inserts a separator only when the directory // does not already end with one, so the file-name offset depends on @@ -32,10 +24,13 @@ static List Find(string directory, string searchPattern, string nonIndex startIndex++; } + var indexedPattern = $"{fileNamePrefix}#"; + var nonIndexedPattern = $"{fileNamePrefix}.{marker}."; + var list = new List(); var nonIndexedPatternSpan = nonIndexedPattern.AsSpan(); var indexedPatternSpan = indexedPattern.AsSpan(); - foreach (var file in Directory.EnumerateFiles(directory, searchPattern)) + foreach (var file in Directory.EnumerateFiles(directory, $"{fileNamePrefix}*.{marker}.*")) { var fileSpan = file.AsSpan(); if (fileSpan.SubStringEquals(nonIndexedPatternSpan, startIndex) || @@ -45,9 +40,39 @@ static List Find(string directory, string searchPattern, string nonIndex } } + AddNested(directory, indexedPattern, marker, list); + return list; } + /// + /// A target name can contain a directory separator: VerifyDirectory names each target + /// after its path within the tree, and SanitizeFilePath deliberately keeps separators. + /// The file for such a target lives under a `{prefix}#` directory, which the flat scan + /// cannot see, so without this its verified file is never tracked and a stale one + /// survives every run. + /// + static void AddNested(string directory, string indexedPattern, string marker, List list) + { + var markerSegment = $".{marker}."; + // The directory name carries the prefix, so everything below it belongs to this + // test. A neighbouring test that merely starts with the same text has its own + // `{itsPrefix}#` directory, which this pattern excludes. + foreach (var subDirectory in Directory.EnumerateDirectories(directory, $"{indexedPattern}*")) + { + foreach (var file in Directory.EnumerateFiles(subDirectory, $"*{markerSegment}*", SearchOption.AllDirectories)) + { + // The Win32 pattern also matches a name ending in `.verified` with no + // extension, which is not a file Verify writes. + if (Path.GetFileName(file) + .Contains(markerSegment)) + { + list.Add(file); + } + } + } + } + static bool SubStringEquals(this CharSpan value, CharSpan match, int start) { // The Win32 search pattern `{prefix}*.received.*` also matches a stray file named @@ -61,4 +86,4 @@ static bool SubStringEquals(this CharSpan value, CharSpan match, int start) var slice = value.Slice(start, match.Length); return slice.SequenceEqual(match); } -} \ No newline at end of file +}