diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs index 3c43a27e0..a21c282b3 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs @@ -468,8 +468,66 @@ public bool TryJoin(ReadOnlySpan path1, #endregion - private static string CombineInternal(string[] paths) - => System.IO.Path.Combine(paths); + private string CombineInternal(string[] paths) + { + string NormalizePath(string path, bool ignoreStartingSeparator) + { + if (!ignoreStartingSeparator && ( + path[0] == DirectorySeparatorChar || + path[0] == AltDirectorySeparatorChar)) + { + path = path.Substring(1); + } + + if (path[path.Length - 1] == DirectorySeparatorChar || + path[path.Length - 1] == AltDirectorySeparatorChar) + { + path = path.Substring(0, path.Length - 1); + } + + return NormalizeDirectorySeparators(path); + } + + if (paths == null) + { + throw new ArgumentNullException(nameof(paths)); + } + + StringBuilder sb = new(); + + bool isFirst = true; + bool endsWithDirectorySeparator = false; + foreach (string path in paths) + { + if (path == null) + { + throw new ArgumentNullException(nameof(paths)); + } + + if (string.IsNullOrEmpty(path)) + { + continue; + } + + if (IsPathRooted(path)) + { + sb.Clear(); + isFirst = true; + } + + sb.Append(NormalizePath(path, isFirst)); + sb.Append(DirectorySeparatorChar); + endsWithDirectorySeparator = path.EndsWith(DirectorySeparatorChar) || + path.EndsWith(AltDirectorySeparatorChar); + } + + if (!endsWithDirectorySeparator) + { + return sb.ToString(0, sb.Length - 1); + } + + return sb.ToString(); + } protected abstract int GetRootLength(string path); protected abstract bool IsDirectorySeparator(char c); diff --git a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs index 65e155895..13d82cabd 100644 --- a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs +++ b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs @@ -232,6 +232,7 @@ private bool IncludeSimulatedTests(ClassModel @class) string[] supportedPathTests = [ "ChangeExtensionTests", + "CombineTests", "EndsInDirectorySeparatorTests", "GetDirectoryNameTests", "GetExtensionTests", diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/Path/CombineTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/Path/CombineTests.cs index 27c4771cb..d6dfea500 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/Path/CombineTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/Path/CombineTests.cs @@ -45,6 +45,25 @@ public void Combine_2Paths_Rooted_ShouldReturnLastRootedPath( result.Should().Be(path2); } + [SkippableTheory] + [InlineAutoData("/foo/", "/bar/", "/bar/")] + [InlineAutoData("foo/", "/bar", "/bar")] + [InlineAutoData("foo/", "bar", "foo/bar")] + [InlineAutoData("foo", "/bar", "/bar")] + [InlineAutoData("foo", "bar", "foo/bar")] + [InlineAutoData("/foo", "bar/", "/foo/bar/")] + public void Combine_2Paths_ShouldReturnExpectedResult( + string path1, string path2, string expectedResult) + { + path1 = path1.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path2 = path2.Replace('/', FileSystem.Path.DirectorySeparatorChar); + expectedResult = expectedResult.Replace('/', FileSystem.Path.DirectorySeparatorChar); + + string result = FileSystem.Path.Combine(path1, path2); + + result.Should().Be(expectedResult); + } + [SkippableTheory] [InlineAutoData] [InlineAutoData(" ")] @@ -109,6 +128,27 @@ public void Combine_3Paths_Rooted_ShouldReturnLastRootedPath( result.Should().Be(path3); } + [SkippableTheory] + [InlineAutoData("/foo/", "/bar/", "/baz/", "/baz/")] + [InlineAutoData("foo/", "/bar/", "/baz", "/baz")] + [InlineAutoData("foo/", "bar", "/baz", "/baz")] + [InlineAutoData("foo", "/bar", "/baz", "/baz")] + [InlineAutoData("foo", "/bar/", "baz", "/bar/baz")] + [InlineAutoData("foo", "bar", "baz", "foo/bar/baz")] + [InlineAutoData("/foo", "bar", "baz/", "/foo/bar/baz/")] + public void Combine_3Paths_ShouldReturnExpectedResult( + string path1, string path2, string path3, string expectedResult) + { + path1 = path1.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path2 = path2.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path3 = path3.Replace('/', FileSystem.Path.DirectorySeparatorChar); + expectedResult = expectedResult.Replace('/', FileSystem.Path.DirectorySeparatorChar); + + string result = FileSystem.Path.Combine(path1, path2, path3); + + result.Should().Be(expectedResult); + } + [SkippableTheory] [InlineAutoData] [InlineAutoData(" ")] @@ -183,6 +223,28 @@ public void Combine_4Paths_Rooted_ShouldReturnLastRootedPath( result.Should().Be(path4); } + [SkippableTheory] + [InlineAutoData("/foo/", "/bar/", "/baz/", "/muh/", "/muh/")] + [InlineAutoData("foo/", "/bar/", "/baz/", "/muh", "/muh")] + [InlineAutoData("foo/", "bar", "/baz", "/muh", "/muh")] + [InlineAutoData("foo", "/bar", "/baz", "/muh", "/muh")] + [InlineAutoData("foo", "/bar/", "baz/", "muh", "/bar/baz/muh")] + [InlineAutoData("foo", "bar", "baz", "muh", "foo/bar/baz/muh")] + [InlineAutoData("/foo", "bar", "baz", "muh/", "/foo/bar/baz/muh/")] + public void Combine_4Paths_ShouldReturnExpectedResult( + string path1, string path2, string path3, string path4, string expectedResult) + { + path1 = path1.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path2 = path2.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path3 = path3.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path4 = path4.Replace('/', FileSystem.Path.DirectorySeparatorChar); + expectedResult = expectedResult.Replace('/', FileSystem.Path.DirectorySeparatorChar); + + string result = FileSystem.Path.Combine(path1, path2, path3, path4); + + result.Should().Be(expectedResult); + } + [SkippableTheory] [InlineAutoData] [InlineAutoData(" ")] @@ -281,6 +343,29 @@ public void Combine_ParamPaths_Rooted_ShouldReturnLastRootedPath( result.Should().Be(path5); } + [SkippableTheory] + [InlineAutoData("/foo/", "/bar/", "/baz/", "/muh/", "/maeh/", "/maeh/")] + [InlineAutoData("foo/", "/bar/", "/baz/", "/muh", "/maeh", "/maeh")] + [InlineAutoData("foo/", "bar", "/baz", "/muh", "/maeh", "/maeh")] + [InlineAutoData("foo", "/bar", "/baz", "/muh", "/maeh", "/maeh")] + [InlineAutoData("foo", "/bar/", "baz/", "muh/", "maeh", "/bar/baz/muh/maeh")] + [InlineAutoData("foo", "bar", "baz", "muh", "maeh", "foo/bar/baz/muh/maeh")] + [InlineAutoData("/foo", "bar", "baz", "muh", "maeh/", "/foo/bar/baz/muh/maeh/")] + public void Combine_ParamPaths_ShouldReturnExpectedResult( + string path1, string path2, string path3, string path4, string path5, string expectedResult) + { + path1 = path1.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path2 = path2.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path3 = path3.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path4 = path4.Replace('/', FileSystem.Path.DirectorySeparatorChar); + path5 = path5.Replace('/', FileSystem.Path.DirectorySeparatorChar); + expectedResult = expectedResult.Replace('/', FileSystem.Path.DirectorySeparatorChar); + + string result = FileSystem.Path.Combine(path1, path2, path3, path4, path5); + + result.Should().Be(expectedResult); + } + [SkippableTheory] [InlineAutoData] [InlineAutoData(" ")]