From c31d9131a169dab015e81b9ad7c2ecf38eaa82fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Tue, 9 Sep 2025 16:01:39 +0200 Subject: [PATCH 1/2] fix: trim trailing slash in `Directory.GetDirectories` --- .../FileSystem/DirectoryMock.cs | 3 +- .../Helpers/PathHelper.cs | 12 +++++ .../Storage/InMemoryStorage.cs | 14 +++--- .../Directory/CreateDirectoryTests.cs | 3 +- .../Directory/EnumerateDirectoriesTests.cs | 45 ++++++++++++++----- .../Directory/GetDirectoriesTests.cs | 20 ++++++++- 6 files changed, 77 insertions(+), 20 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/FileSystem/DirectoryMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/DirectoryMock.cs index 816d41ae3..0f5cbad58 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/DirectoryMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/DirectoryMock.cs @@ -712,7 +712,8 @@ private IEnumerable EnumerateInternal(FileSystemTypes fileSystemTypes, adjustedLocation.SearchPattern, enumerationOptions) .Select(x => _fileSystem - .GetSubdirectoryPath(x.FullPath, x.FriendlyName, adjustedLocation.GivenPath)); + .GetSubdirectoryPath(x.FullPath, x.FriendlyName, adjustedLocation.GivenPath) + .TrimTrailingDirectorySeparator(_fileSystem)); } private IDirectoryInfo LoadDirectoryInfoOrThrowNotFoundException( diff --git a/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs b/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs index 525a67231..8f963f47e 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs @@ -175,6 +175,18 @@ internal static string TrimOnWindows(this string path, MockFileSystem fileSystem return path; } + internal static string TrimTrailingDirectorySeparator(this string path, + MockFileSystem fileSystem) + { + path = path.TrimEnd(fileSystem.Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(path)) + { + return $"{fileSystem.Path.DirectorySeparatorChar}"; + } + + return path; + } + private static void CheckPathArgument(Execute execute, [NotNull] string? path, string paramName, bool includeIsEmptyCheck) { diff --git a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs index 63a07f973..501d95627 100644 --- a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs +++ b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs @@ -260,13 +260,14 @@ public IEnumerable EnumerateLocations( foreach (KeyValuePair item in _containers .Where(x => x.Key.FullPath.StartsWith(fullPath, _fileSystem.Execute.StringComparisonMode) && - !x.Key.Equals(location))) + !x.Key.Equals(location)) + .OrderBy(x => x.Key.FullPath)) { if (type.HasFlag(item.Value.Type) && IncludeItemInEnumeration(item, fullPathWithoutTrailingSlash, enumerationOptions)) { - string? itemPath = item.Key.FullPath; + string itemPath = item.Key.FullPath; if (itemPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar)) { itemPath = itemPath.TrimEnd(_fileSystem.Path.DirectorySeparatorChar); @@ -353,8 +354,8 @@ public IEnumerable GetDrives() string fullPath; if (path.IsUncPath(_fileSystem) && - _fileSystem.Execute is { IsNetFramework: true } or {IsWindows: false } && - path.LastIndexOf(_fileSystem.Path.DirectorySeparatorChar) <= 2) + _fileSystem.Execute is { IsNetFramework: true } or { IsWindows: false } && + path.LastIndexOf(_fileSystem.Path.DirectorySeparatorChar) <= 2) { fullPath = path; } @@ -790,7 +791,8 @@ private void CheckAndAdjustParentDirectoryTimes(IStorageLocation location) throw ExceptionFactory.AccessDenied(location.FullPath); } #else - using (parentContainer.RequestAccess(FileAccess.Write, FileShare.ReadWrite, onBehalfOfLocation: location)) + using (parentContainer.RequestAccess(FileAccess.Write, FileShare.ReadWrite, + onBehalfOfLocation: location)) { TimeAdjustments timeAdjustment = TimeAdjustments.LastWriteTime; if (_fileSystem.Execute.IsWindows) @@ -821,7 +823,7 @@ private void CreateParents(MockFileSystem fileSystem, IStorageLocation location) List accessHandles = []; try { - foreach (string? parentPath in parents) + foreach (string parentPath in parents) { ChangeDescription? fileSystemChange = null; IStorageLocation parentLocation = diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/CreateDirectoryTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/CreateDirectoryTests.cs index a6156da72..d80679d3e 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/CreateDirectoryTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/CreateDirectoryTests.cs @@ -348,7 +348,8 @@ public async Task CreateDirectory_TrailingDirectorySeparator_ShouldNotBeTrimmed( await That(result.Name).IsEqualTo(expectedName.TrimEnd( FileSystem.Path.DirectorySeparatorChar, FileSystem.Path.AltDirectorySeparatorChar)); - await That(result.FullName).IsEqualTo($"{BasePath}{FileSystem.Path.DirectorySeparatorChar}{expectedName}" + await That(result.FullName).IsEqualTo( + $"{BasePath}{FileSystem.Path.DirectorySeparatorChar}{expectedName}" .Replace(FileSystem.Path.AltDirectorySeparatorChar, FileSystem.Path.DirectorySeparatorChar)); await That(FileSystem.Directory.Exists(nameWithSuffix)).IsTrue(); diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/EnumerateDirectoriesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/EnumerateDirectoriesTests.cs index 61306cd3e..eff650e4d 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/EnumerateDirectoriesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/EnumerateDirectoriesTests.cs @@ -185,7 +185,8 @@ await That(result).HasSingle().Which.EndsWith(extension) public async Task EnumerateDirectories_ShouldIncludeEmptyDirectoriesWithTrailingSlash() { string rootDirectory = "RootDir"; - string emptyDirectory = FileSystem.Path.Combine(rootDirectory, "EmptyDir") + FileSystem.Path.DirectorySeparatorChar; + string emptyDirectory = FileSystem.Path.Combine(rootDirectory, "EmptyDir") + + FileSystem.Path.DirectorySeparatorChar; FileSystem.Directory.CreateDirectory(emptyDirectory); @@ -225,11 +226,29 @@ await That(result) .InAnyOrder(); } + [Theory] + [InlineData('/')] + [InlineData('\\')] + public async Task EnumerateDirectories_TrailingDirectorySeparator_ShouldBeTrimmed(char suffix) + { + Skip.IfNot(Test.RunsOnWindows || + suffix == FileSystem.Path.DirectorySeparatorChar || + suffix == FileSystem.Path.AltDirectorySeparatorChar); + + string path = $"foo{suffix}"; + + FileSystem.Directory.CreateDirectory(path); + IEnumerable result = FileSystem.Directory.EnumerateDirectories("."); + + await That(result).HasSingle() + .Which.DoesNotEndWith(suffix); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Theory] [AutoData] public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderAttributesToSkip( - string path) + string path) { EnumerationOptions enumerationOptions = new() { @@ -254,7 +273,7 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderAttr [InlineData(true)] [InlineData(false)] public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderIgnoreInaccessible( - bool ignoreInaccessible) + bool ignoreInaccessible) { Skip.IfNot(Test.RunsOnWindows); @@ -299,8 +318,8 @@ await That(Act).Throws() [InlineAutoData(MatchCasing.CaseInsensitive)] [InlineAutoData(MatchCasing.CaseSensitive)] public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatchCasing( - MatchCasing matchCasing, - string path) + MatchCasing matchCasing, + string path) { EnumerationOptions enumerationOptions = new() { @@ -329,8 +348,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatc [InlineAutoData(MatchType.Simple)] [InlineAutoData(MatchType.Win32)] public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatchType( - MatchType matchType, - string path) + MatchType matchType, + string path) { EnumerationOptions enumerationOptions = new() { @@ -360,7 +379,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatc [InlineAutoData(true, 2)] [InlineAutoData(true, 3)] [InlineAutoData(false, 2)] - public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMaxRecursionDepthWhenRecurseSubdirectoriesIsSet( + public async Task + EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMaxRecursionDepthWhenRecurseSubdirectoriesIsSet( bool recurseSubdirectories, int maxRecursionDepth, string path) @@ -409,7 +429,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMaxR [Theory] [InlineAutoData(true)] [InlineAutoData(false)] - public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRecurseSubdirectories( + public async Task + EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRecurseSubdirectories( bool recurseSubdirectories, string path) { @@ -440,7 +461,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRecu [Theory] [InlineAutoData(true)] [InlineAutoData(false)] - public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectories( + public async Task + EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectories( bool returnSpecialDirectories, string path) { @@ -469,7 +491,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRetu #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Fact] - public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectoriesCorrectlyForPathRoots() + public async Task + EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectoriesCorrectlyForPathRoots() { string root = FileSystem.Path.GetPathRoot(FileSystem.Directory.GetCurrentDirectory())!; EnumerationOptions enumerationOptions = new() diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/GetDirectoriesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/GetDirectoriesTests.cs index 414a36f19..65bfc4687 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/GetDirectoriesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/GetDirectoriesTests.cs @@ -101,11 +101,29 @@ await That(result).IsEmpty() } } + [Theory] + [InlineData('/')] + [InlineData('\\')] + public async Task GetDirectories_TrailingDirectorySeparator_ShouldBeTrimmed(char suffix) + { + Skip.IfNot(Test.RunsOnWindows || + suffix == FileSystem.Path.DirectorySeparatorChar || + suffix == FileSystem.Path.AltDirectorySeparatorChar); + + string path = $"foo{suffix}"; + + FileSystem.Directory.CreateDirectory(path); + string[] result = FileSystem.Directory.GetDirectories("."); + + await That(result).HasSingle() + .Which.DoesNotEndWith(suffix); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Theory] [AutoData] public async Task GetDirectories_WithEnumerationOptions_ShouldConsiderSetOptions( - string path) + string path) { IDirectoryInfo baseDirectory = FileSystem.Directory.CreateDirectory(path); From e6313c4debf90404ad1287b377dbba1f2a89b4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Tue, 9 Sep 2025 16:06:26 +0200 Subject: [PATCH 2/2] Simplify `TrimTrailingDirectorySeparator` --- Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs b/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs index 8f963f47e..81f63b7c5 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs @@ -178,13 +178,12 @@ internal static string TrimOnWindows(this string path, MockFileSystem fileSystem internal static string TrimTrailingDirectorySeparator(this string path, MockFileSystem fileSystem) { - path = path.TrimEnd(fileSystem.Path.DirectorySeparatorChar); - if (string.IsNullOrEmpty(path)) + if (path.Length == 1 && path[0] == fileSystem.Path.DirectorySeparatorChar) { - return $"{fileSystem.Path.DirectorySeparatorChar}"; + return path; } - return path; + return path.TrimEnd(fileSystem.Path.DirectorySeparatorChar); } private static void CheckPathArgument(Execute execute, [NotNull] string? path, string paramName,