From 5e768d19da6cfcc1d5588f198cdfd62c99ad491c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Mon, 12 Aug 2024 11:52:52 +0200 Subject: [PATCH] fix: Throw correct exception when using `File.Replace` with case-only changes on MacOS (#638) * Add tests for case-only changes in File.Copy, File.Move and File.Replace * Ensure correct casing in `Directory.Move` tests * Throw correct exception on MacOS --- .../Helpers/ExceptionFactory.cs | 4 +++ .../Storage/InMemoryStorage.cs | 6 ++++ .../FileSystem/Directory/MoveTests.cs | 2 +- .../FileSystem/File/CopyTests.cs | 27 ++++++++++++++ .../FileSystem/File/MoveTests.cs | 2 ++ .../FileSystem/File/ReplaceTests.cs | 36 +++++++++++++++++++ 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs index eabb8a43c..8f7eed0e1 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs @@ -203,6 +203,10 @@ internal static IOException ProcessCannotAccessTheFile(string path, int hResult) $"The process cannot access the file '{path}' because it is being used by another process.", hResult); + internal static IOException ReplaceSourceMustBeDifferentThanDestination( + string sourcePath, string destinationPath) + => new($"The source '{sourcePath}' and destination '{destinationPath}' are the same file.", -2146232800); + #pragma warning disable MA0015 // Specify the parameter name internal static ArgumentException SearchPatternCannotContainTwoDots() => new( diff --git a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs index 273bc0eb6..1726a9ae6 100644 --- a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs +++ b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs @@ -441,6 +441,12 @@ public IStorageContainer GetOrCreateContainer( throw ExceptionFactory.AccessToPathDenied(source.FullPath); } + if (_fileSystem.Execute.IsMac && + source.FullPath.Equals(destination.FullPath, StringComparison.OrdinalIgnoreCase)) + { + throw ExceptionFactory.ReplaceSourceMustBeDifferentThanDestination(source.FullPath, destination.FullPath); + } + using (_ = sourceContainer.RequestAccess( FileAccess.ReadWrite, FileShare.None, diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/MoveTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/MoveTests.cs index 4dde33fc2..4c9232fa6 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/Directory/MoveTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/Directory/MoveTests.cs @@ -29,7 +29,7 @@ public void Move_CaseOnlyChange_ShouldMoveDirectoryWithContent(string path) FileSystem.Directory.Exists(source).Should().Be(!Test.RunsOnLinux); FileSystem.Should().HaveDirectory(destination); FileSystem.Directory.GetDirectories(".").Should() - .ContainSingle(d => d.Contains(destination)); + .ContainSingle(d => d.Contains(destination, StringComparison.Ordinal)); FileSystem.Directory.GetFiles(destination, initialized[1].Name) .Should().ContainSingle(); FileSystem.Directory.GetDirectories(destination, initialized[2].Name) diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/CopyTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/CopyTests.cs index 272a0306c..e33297339 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/CopyTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/CopyTests.cs @@ -7,6 +7,33 @@ public abstract partial class CopyTests : FileSystemTestBase where TFileSystem : IFileSystem { + [SkippableTheory] + [AutoData] + public void Copy_CaseOnlyChange_ShouldThrowIOException_ExceptOnLinux( + string name, string contents) + { + string sourceName = name.ToLowerInvariant(); + string destinationName = name.ToUpperInvariant(); + FileSystem.File.WriteAllText(sourceName, contents); + + Exception? exception = Record.Exception(() => + { + FileSystem.File.Copy(sourceName, destinationName); + }); + + if (Test.RunsOnLinux) + { + exception.Should().BeNull(); + FileSystem.File.Exists(sourceName).Should().BeTrue(); + FileSystem.File.Exists(destinationName).Should().BeTrue(); + } + else + { + exception.Should() + .BeException(hResult: Test.RunsOnWindows ? -2147024816 : 17); + } + } + [SkippableTheory] [AutoData] public void diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/MoveTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/MoveTests.cs index af7b2acf6..9dd733a27 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/MoveTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/MoveTests.cs @@ -26,6 +26,8 @@ public void Move_CaseOnlyChange_ShouldMoveFileWithContent( FileSystem.Should().HaveFile(destinationName) .Which.HasContent(contents); + FileSystem.Directory.GetFiles(".").Should() + .ContainSingle(d => d.Contains(destinationName, StringComparison.Ordinal)); } [SkippableTheory] diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/ReplaceTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/ReplaceTests.cs index f28ee80ad..d0ca89906 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/ReplaceTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/ReplaceTests.cs @@ -7,6 +7,42 @@ public abstract partial class ReplaceTests : FileSystemTestBase where TFileSystem : IFileSystem { + [SkippableTheory] + [AutoData] + public void Replace_CaseOnlyChange_ShouldThrowIOException( + string name, string contents) + { + string sourceName = name.ToLowerInvariant(); + string destinationName = name.ToUpperInvariant(); + FileSystem.File.WriteAllText(sourceName, contents); + FileSystem.File.WriteAllText(destinationName, "other-content"); + + Exception? exception = Record.Exception(() => + { + FileSystem.File.Replace(sourceName, destinationName, null); + }); + + + if (Test.RunsOnLinux) + { + exception.Should().BeNull(); + FileSystem.File.Exists(sourceName).Should().BeFalse(); + FileSystem.File.Exists(destinationName).Should().BeTrue(); + } + else if (Test.RunsOnMac) + { + exception.Should().BeException( + hResult: -2146232800, + messageContains: $"The source '{FileSystem.Path.GetFullPath(sourceName)}' and destination '{FileSystem.Path.GetFullPath(destinationName)}' are the same file"); + } + else + { + exception.Should().BeException( + hResult: -2147024864, + messageContains: "The process cannot access the file"); + } + } + [SkippableTheory] [AutoData] public void