Skip to content

Commit 928bc38

Browse files
authored
fix: handle case-sensitive file overwrite on Windows and add tests (#1259)
This PR fixes an issue (#1140) where copying a file with the same name but different casing on Windows resulted in an "already in use" error. It aligns the behavior with the native file system. ## Changes - Fixed file overwrite logic to properly handle case-insensitive paths on Windows. - Added Windows-specific checks to mimic `System.IO.File.Copy` `System.IO.File.Move` and `System.IO.File.Replace` behavior. - Implemented test cases to verify expected behavior for case-sensitive and case-insensitive file systems. - Ensured that the fix does not affect Linux behavior. ## Testing - Verified that the fix works correctly on Windows by checking file existence before and after copying,moving and replacing file with different cases - Ran test cases on both Windows and Linux to ensure no unintended side effects.
1 parent 44fb537 commit 928bc38

File tree

4 files changed

+83
-18
lines changed

4 files changed

+83
-18
lines changed

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public override void AppendAllBytes(string path, ReadOnlySpan<byte> bytes)
4848
AppendAllBytes(path, bytes.ToArray());
4949
}
5050
#endif
51-
51+
5252
/// <inheritdoc />
5353
public override void AppendAllLines(string path, IEnumerable<string> contents)
5454
{
@@ -163,6 +163,11 @@ public override void Copy(string sourceFileName, string destFileName, bool overw
163163
throw CommonExceptions.FileAlreadyExists(destFileName);
164164
}
165165

166+
if (string.Equals(sourceFileName, destFileName, StringComparison.OrdinalIgnoreCase) && XFS.IsWindowsPlatform())
167+
{
168+
throw CommonExceptions.ProcessCannotAccessFileInUse(destFileName);
169+
}
170+
166171
mockFileDataAccessor.RemoveFile(destFileName);
167172
}
168173

@@ -522,30 +527,37 @@ public override void Move(string sourceFileName, string destFileName)
522527
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(sourceFileName, nameof(sourceFileName));
523528
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(destFileName, nameof(destFileName));
524529

525-
if (mockFileDataAccessor.GetFile(destFileName) != null)
526-
{
527-
if (mockFileDataAccessor.StringOperations.Equals(destFileName, sourceFileName))
528-
{
529-
return;
530-
}
531-
else
532-
{
533-
throw new IOException("A file can not be created if it already exists.");
534-
}
535-
}
536-
537530
var sourceFile = mockFileDataAccessor.GetFile(sourceFileName);
538531

539532
if (sourceFile == null)
540533
{
541534
throw CommonExceptions.FileNotFound(sourceFileName);
542535
}
536+
543537
if (!sourceFile.AllowedFileShare.HasFlag(FileShare.Delete))
544538
{
545539
throw CommonExceptions.ProcessCannotAccessFileInUse();
546540
}
541+
547542
VerifyDirectoryExists(destFileName);
548543

544+
if (mockFileDataAccessor.GetFile(destFileName) != null)
545+
{
546+
if (mockFileDataAccessor.StringOperations.Equals(destFileName, sourceFileName))
547+
{
548+
if (XFS.IsWindowsPlatform())
549+
{
550+
mockFileDataAccessor.RemoveFile(sourceFileName);
551+
mockFileDataAccessor.AddFile(destFileName, mockFileDataAccessor.AdjustTimes(new MockFileData(sourceFile), TimeAdjustments.LastAccessTime), false);
552+
}
553+
return;
554+
}
555+
else
556+
{
557+
throw new IOException("A file can not be created if it already exists.");
558+
}
559+
}
560+
549561
mockFileDataAccessor.RemoveFile(sourceFileName, false);
550562
mockFileDataAccessor.AddFile(destFileName, mockFileDataAccessor.AdjustTimes(new MockFileData(sourceFile), TimeAdjustments.LastAccessTime), false);
551563
}
@@ -822,6 +834,11 @@ public override void Replace(string sourceFileName, string destinationFileName,
822834
throw CommonExceptions.FileNotFound(destinationFileName);
823835
}
824836

837+
if (mockFileDataAccessor.StringOperations.Equals(sourceFileName, destinationFileName) && XFS.IsWindowsPlatform())
838+
{
839+
throw CommonExceptions.ProcessCannotAccessFileInUse();
840+
}
841+
825842
if (destinationBackupFileName != null)
826843
{
827844
Copy(destinationFileName, destinationBackupFileName, overwrite: true);
@@ -1066,7 +1083,7 @@ public override void WriteAllBytes(string path, byte[] bytes)
10661083

10671084
mockFileDataAccessor.AddFile(path, mockFileDataAccessor.AdjustTimes(new MockFileData(bytes.ToArray()), TimeAdjustments.All));
10681085
}
1069-
1086+
10701087
#if FEATURE_FILE_SPAN
10711088
/// <inheritdoc cref="IFile.WriteAllBytes(string,ReadOnlySpan{byte})"/>
10721089
public override void WriteAllBytes(string path, ReadOnlySpan<byte> bytes)

tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileCopyTests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,22 @@ public async Task MockFile_Copy_ShouldThrowNotSupportedExceptionWhenSourcePathCo
246246
await That(action).Throws<NotSupportedException>();
247247
}
248248

249+
[Test]
250+
[WindowsOnly(WindowsSpecifics.Drives)]
251+
public async Task MockFile_Copy_ShouldThrowIOExceptionWhenOverwritingWithSameNameDifferentCase()
252+
{
253+
var fileSystem = new MockFileSystem();
254+
string path = @"C:\Temp\file.txt";
255+
string pathUpper = @"C:\Temp\FILE.TXT";
256+
257+
fileSystem.File.WriteAllText(path, "Hello");
258+
259+
void Act() => fileSystem.File.Copy(path, pathUpper, true);
260+
261+
await That(Act).Throws<IOException>()
262+
.WithMessage($"The process cannot access the file '{pathUpper}' because it is being used by another process.");
263+
}
264+
249265
[Test]
250266
[WindowsOnly(WindowsSpecifics.Drives)]
251267
public async Task MockFile_Copy_ShouldThrowNotSupportedExceptionWhenSourcePathContainsInvalidDriveLetter()
@@ -417,4 +433,4 @@ public async Task MockFile_Copy_ShouldThrowIOExceptionForInvalidFileShare()
417433

418434
await That(action).Throws<IOException>();
419435
}
420-
}
436+
}

tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileMoveTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,22 @@ public async Task MockFile_Move_ShouldThrowNotSupportedExceptionWhenDestinationP
249249
await That(action).Throws<NotSupportedException>();
250250
}
251251

252+
[Test]
253+
[WindowsOnly(WindowsSpecifics.Drives)]
254+
public async Task MockFile_Move_CaseOnlyRename_ShouldChangeCase()
255+
{
256+
var fileSystem = new MockFileSystem();
257+
string sourceFilePath = @"c:\temp\demo.txt";
258+
string destFilePath = @"c:\temp\DEMO.TXT";
259+
string sourceFileContent = "content";
260+
fileSystem.File.WriteAllText(sourceFilePath, sourceFileContent);
261+
262+
fileSystem.File.Move(sourceFilePath, destFilePath);
263+
264+
await That(fileSystem.File.Exists(destFilePath)).IsTrue();
265+
await That(fileSystem.File.ReadAllText(destFilePath)).IsEqualTo(sourceFileContent);
266+
}
267+
252268
[Test]
253269
public async Task MockFile_Move_ShouldThrowArgumentExceptionWhenSourceIsEmpty_Message()
254270
{

tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileTests.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ public async Task MockFile_AppendText_CreatesNewFileForAppendToNonExistingFile()
547547
await That(file.TextContents).IsEqualTo("New too!");
548548
await That(filesystem.FileExists(filepath)).IsTrue();
549549
}
550-
550+
551551
#if !NET9_0_OR_GREATER
552552
[Test]
553553
public void Serializable_works()
@@ -567,7 +567,7 @@ public void Serializable_works()
567567
Assert.Pass();
568568
}
569569
#endif
570-
570+
571571
#if !NET9_0_OR_GREATER
572572
[Test]
573573
public async Task Serializable_can_deserialize()
@@ -670,7 +670,7 @@ public async Task MockFile_Replace_ShouldCreateBackup()
670670
fileSystem.File.Replace(path1, path2, path3);
671671

672672
await That(fileSystem.File.ReadAllText(path3)).IsEqualTo("2");
673-
}
673+
}
674674

675675
[Test]
676676
public async Task MockFile_Replace_ShouldThrowIfDirectoryOfBackupPathDoesNotExist()
@@ -730,4 +730,20 @@ public async Task MockFile_OpenRead_ShouldReturnReadOnlyStream()
730730
await That(stream.CanWrite).IsFalse();
731731
await That(() => stream.WriteByte(0)).Throws<NotSupportedException>();
732732
}
733+
734+
[Test]
735+
[WindowsOnly(WindowsSpecifics.Drives)]
736+
public async Task MockFile_Replace_SourceAndDestinationDifferOnlyInCasing_ShouldThrowIOException()
737+
{
738+
var fileSystem = new MockFileSystem();
739+
string sourceFilePath = @"c:\temp\demo.txt";
740+
string destFilePath = @"c:\temp\DEMO.txt";
741+
string fileContent = "content";
742+
fileSystem.File.WriteAllText(sourceFilePath, fileContent);
743+
744+
void Act() => fileSystem.File.Replace(sourceFilePath, destFilePath, null, true);
745+
746+
await That(Act).Throws<IOException>()
747+
.HasMessage("The process cannot access the file because it is being used by another process.");
748+
}
733749
}

0 commit comments

Comments
 (0)