Skip to content

Commit f764c5c

Browse files
authored
fix: trim trailing slash in Directory.GetDirectories (#851)
This PR fixes the `Directory.GetDirectories` method to trim trailing slashes from directory paths in the returned results. This ensures consistent behavior where directory paths returned by enumeration methods don't include trailing directory separators. ### Key changes: - Adds helper method `TrimTrailingDirectorySeparator` for consistent path normalization - Updates directory enumeration logic to remove trailing slashes from results - Includes comprehensive test coverage for both `GetDirectories` and `EnumerateDirectories` methods
1 parent f66fee3 commit f764c5c

File tree

6 files changed

+76
-20
lines changed

6 files changed

+76
-20
lines changed

Source/Testably.Abstractions.Testing/FileSystem/DirectoryMock.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,8 @@ private IEnumerable<string> EnumerateInternal(FileSystemTypes fileSystemTypes,
712712
adjustedLocation.SearchPattern,
713713
enumerationOptions)
714714
.Select(x => _fileSystem
715-
.GetSubdirectoryPath(x.FullPath, x.FriendlyName, adjustedLocation.GivenPath));
715+
.GetSubdirectoryPath(x.FullPath, x.FriendlyName, adjustedLocation.GivenPath)
716+
.TrimTrailingDirectorySeparator(_fileSystem));
716717
}
717718

718719
private IDirectoryInfo LoadDirectoryInfoOrThrowNotFoundException(

Source/Testably.Abstractions.Testing/Helpers/PathHelper.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,17 @@ internal static string TrimOnWindows(this string path, MockFileSystem fileSystem
175175
return path;
176176
}
177177

178+
internal static string TrimTrailingDirectorySeparator(this string path,
179+
MockFileSystem fileSystem)
180+
{
181+
if (path.Length == 1 && path[0] == fileSystem.Path.DirectorySeparatorChar)
182+
{
183+
return path;
184+
}
185+
186+
return path.TrimEnd(fileSystem.Path.DirectorySeparatorChar);
187+
}
188+
178189
private static void CheckPathArgument(Execute execute, [NotNull] string? path, string paramName,
179190
bool includeIsEmptyCheck)
180191
{

Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,14 @@ public IEnumerable<IStorageLocation> EnumerateLocations(
260260
foreach (KeyValuePair<IStorageLocation, IStorageContainer> item in _containers
261261
.Where(x => x.Key.FullPath.StartsWith(fullPath,
262262
_fileSystem.Execute.StringComparisonMode) &&
263-
!x.Key.Equals(location)))
263+
!x.Key.Equals(location))
264+
.OrderBy(x => x.Key.FullPath))
264265
{
265266
if (type.HasFlag(item.Value.Type) &&
266267
IncludeItemInEnumeration(item, fullPathWithoutTrailingSlash,
267268
enumerationOptions))
268269
{
269-
string? itemPath = item.Key.FullPath;
270+
string itemPath = item.Key.FullPath;
270271
if (itemPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar))
271272
{
272273
itemPath = itemPath.TrimEnd(_fileSystem.Path.DirectorySeparatorChar);
@@ -353,8 +354,8 @@ public IEnumerable<IStorageDrive> GetDrives()
353354

354355
string fullPath;
355356
if (path.IsUncPath(_fileSystem) &&
356-
_fileSystem.Execute is { IsNetFramework: true } or {IsWindows: false } &&
357-
path.LastIndexOf(_fileSystem.Path.DirectorySeparatorChar) <= 2)
357+
_fileSystem.Execute is { IsNetFramework: true } or { IsWindows: false } &&
358+
path.LastIndexOf(_fileSystem.Path.DirectorySeparatorChar) <= 2)
358359
{
359360
fullPath = path;
360361
}
@@ -790,7 +791,8 @@ private void CheckAndAdjustParentDirectoryTimes(IStorageLocation location)
790791
throw ExceptionFactory.AccessDenied(location.FullPath);
791792
}
792793
#else
793-
using (parentContainer.RequestAccess(FileAccess.Write, FileShare.ReadWrite, onBehalfOfLocation: location))
794+
using (parentContainer.RequestAccess(FileAccess.Write, FileShare.ReadWrite,
795+
onBehalfOfLocation: location))
794796
{
795797
TimeAdjustments timeAdjustment = TimeAdjustments.LastWriteTime;
796798
if (_fileSystem.Execute.IsWindows)
@@ -821,7 +823,7 @@ private void CreateParents(MockFileSystem fileSystem, IStorageLocation location)
821823
List<IStorageAccessHandle> accessHandles = [];
822824
try
823825
{
824-
foreach (string? parentPath in parents)
826+
foreach (string parentPath in parents)
825827
{
826828
ChangeDescription? fileSystemChange = null;
827829
IStorageLocation parentLocation =

Tests/Testably.Abstractions.Tests/FileSystem/Directory/CreateDirectoryTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,8 @@ public async Task CreateDirectory_TrailingDirectorySeparator_ShouldNotBeTrimmed(
348348
await That(result.Name).IsEqualTo(expectedName.TrimEnd(
349349
FileSystem.Path.DirectorySeparatorChar,
350350
FileSystem.Path.AltDirectorySeparatorChar));
351-
await That(result.FullName).IsEqualTo($"{BasePath}{FileSystem.Path.DirectorySeparatorChar}{expectedName}"
351+
await That(result.FullName).IsEqualTo(
352+
$"{BasePath}{FileSystem.Path.DirectorySeparatorChar}{expectedName}"
352353
.Replace(FileSystem.Path.AltDirectorySeparatorChar,
353354
FileSystem.Path.DirectorySeparatorChar));
354355
await That(FileSystem.Directory.Exists(nameWithSuffix)).IsTrue();

Tests/Testably.Abstractions.Tests/FileSystem/Directory/EnumerateDirectoriesTests.cs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ await That(result).HasSingle().Which.EndsWith(extension)
185185
public async Task EnumerateDirectories_ShouldIncludeEmptyDirectoriesWithTrailingSlash()
186186
{
187187
string rootDirectory = "RootDir";
188-
string emptyDirectory = FileSystem.Path.Combine(rootDirectory, "EmptyDir") + FileSystem.Path.DirectorySeparatorChar;
188+
string emptyDirectory = FileSystem.Path.Combine(rootDirectory, "EmptyDir") +
189+
FileSystem.Path.DirectorySeparatorChar;
189190

190191
FileSystem.Directory.CreateDirectory(emptyDirectory);
191192

@@ -225,11 +226,29 @@ await That(result)
225226
.InAnyOrder();
226227
}
227228

229+
[Theory]
230+
[InlineData('/')]
231+
[InlineData('\\')]
232+
public async Task EnumerateDirectories_TrailingDirectorySeparator_ShouldBeTrimmed(char suffix)
233+
{
234+
Skip.IfNot(Test.RunsOnWindows ||
235+
suffix == FileSystem.Path.DirectorySeparatorChar ||
236+
suffix == FileSystem.Path.AltDirectorySeparatorChar);
237+
238+
string path = $"foo{suffix}";
239+
240+
FileSystem.Directory.CreateDirectory(path);
241+
IEnumerable<string> result = FileSystem.Directory.EnumerateDirectories(".");
242+
243+
await That(result).HasSingle()
244+
.Which.DoesNotEndWith(suffix);
245+
}
246+
228247
#if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS
229248
[Theory]
230249
[AutoData]
231250
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderAttributesToSkip(
232-
string path)
251+
string path)
233252
{
234253
EnumerationOptions enumerationOptions = new()
235254
{
@@ -254,7 +273,7 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderAttr
254273
[InlineData(true)]
255274
[InlineData(false)]
256275
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderIgnoreInaccessible(
257-
bool ignoreInaccessible)
276+
bool ignoreInaccessible)
258277
{
259278
Skip.IfNot(Test.RunsOnWindows);
260279

@@ -299,8 +318,8 @@ await That(Act).Throws<UnauthorizedAccessException>()
299318
[InlineAutoData(MatchCasing.CaseInsensitive)]
300319
[InlineAutoData(MatchCasing.CaseSensitive)]
301320
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatchCasing(
302-
MatchCasing matchCasing,
303-
string path)
321+
MatchCasing matchCasing,
322+
string path)
304323
{
305324
EnumerationOptions enumerationOptions = new()
306325
{
@@ -329,8 +348,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatc
329348
[InlineAutoData(MatchType.Simple)]
330349
[InlineAutoData(MatchType.Win32)]
331350
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatchType(
332-
MatchType matchType,
333-
string path)
351+
MatchType matchType,
352+
string path)
334353
{
335354
EnumerationOptions enumerationOptions = new()
336355
{
@@ -360,7 +379,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMatc
360379
[InlineAutoData(true, 2)]
361380
[InlineAutoData(true, 3)]
362381
[InlineAutoData(false, 2)]
363-
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMaxRecursionDepthWhenRecurseSubdirectoriesIsSet(
382+
public async Task
383+
EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMaxRecursionDepthWhenRecurseSubdirectoriesIsSet(
364384
bool recurseSubdirectories,
365385
int maxRecursionDepth,
366386
string path)
@@ -409,7 +429,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderMaxR
409429
[Theory]
410430
[InlineAutoData(true)]
411431
[InlineAutoData(false)]
412-
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRecurseSubdirectories(
432+
public async Task
433+
EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRecurseSubdirectories(
413434
bool recurseSubdirectories,
414435
string path)
415436
{
@@ -440,7 +461,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRecu
440461
[Theory]
441462
[InlineAutoData(true)]
442463
[InlineAutoData(false)]
443-
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectories(
464+
public async Task
465+
EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectories(
444466
bool returnSpecialDirectories,
445467
string path)
446468
{
@@ -469,7 +491,8 @@ public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderRetu
469491

470492
#if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS
471493
[Fact]
472-
public async Task EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectoriesCorrectlyForPathRoots()
494+
public async Task
495+
EnumerateDirectories_WithEnumerationOptions_ShouldConsiderReturnSpecialDirectoriesCorrectlyForPathRoots()
473496
{
474497
string root = FileSystem.Path.GetPathRoot(FileSystem.Directory.GetCurrentDirectory())!;
475498
EnumerationOptions enumerationOptions = new()

Tests/Testably.Abstractions.Tests/FileSystem/Directory/GetDirectoriesTests.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,29 @@ await That(result).IsEmpty()
101101
}
102102
}
103103

104+
[Theory]
105+
[InlineData('/')]
106+
[InlineData('\\')]
107+
public async Task GetDirectories_TrailingDirectorySeparator_ShouldBeTrimmed(char suffix)
108+
{
109+
Skip.IfNot(Test.RunsOnWindows ||
110+
suffix == FileSystem.Path.DirectorySeparatorChar ||
111+
suffix == FileSystem.Path.AltDirectorySeparatorChar);
112+
113+
string path = $"foo{suffix}";
114+
115+
FileSystem.Directory.CreateDirectory(path);
116+
string[] result = FileSystem.Directory.GetDirectories(".");
117+
118+
await That(result).HasSingle()
119+
.Which.DoesNotEndWith(suffix);
120+
}
121+
104122
#if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS
105123
[Theory]
106124
[AutoData]
107125
public async Task GetDirectories_WithEnumerationOptions_ShouldConsiderSetOptions(
108-
string path)
126+
string path)
109127
{
110128
IDirectoryInfo baseDirectory =
111129
FileSystem.Directory.CreateDirectory(path);

0 commit comments

Comments
 (0)