Skip to content

Commit 1277a9c

Browse files
committed
fix: handle case-insensitive file overwrite on Windows
test: add tests for Windows file copy behavior
1 parent 44fb537 commit 1277a9c

File tree

4 files changed

+83
-15
lines changed

4 files changed

+83
-15
lines changed

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

Lines changed: 31 additions & 12 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,10 +527,29 @@ public override void Move(string sourceFileName, string destFileName)
522527
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(sourceFileName, nameof(sourceFileName));
523528
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(destFileName, nameof(destFileName));
524529

530+
var sourceFile = mockFileDataAccessor.GetFile(sourceFileName);
531+
532+
if (sourceFile == null)
533+
{
534+
throw CommonExceptions.FileNotFound(sourceFileName);
535+
}
536+
537+
if (!sourceFile.AllowedFileShare.HasFlag(FileShare.Delete))
538+
{
539+
throw CommonExceptions.ProcessCannotAccessFileInUse();
540+
}
541+
542+
VerifyDirectoryExists(destFileName);
543+
525544
if (mockFileDataAccessor.GetFile(destFileName) != null)
526545
{
527546
if (mockFileDataAccessor.StringOperations.Equals(destFileName, sourceFileName))
528547
{
548+
if (XFS.IsWindowsPlatform())
549+
{
550+
mockFileDataAccessor.RemoveFile(sourceFileName);
551+
mockFileDataAccessor.AddFile(destFileName, mockFileDataAccessor.AdjustTimes(new MockFileData(sourceFile), TimeAdjustments.LastAccessTime), false);
552+
}
529553
return;
530554
}
531555
else
@@ -534,16 +558,6 @@ public override void Move(string sourceFileName, string destFileName)
534558
}
535559
}
536560

537-
var sourceFile = mockFileDataAccessor.GetFile(sourceFileName);
538-
539-
if (sourceFile == null)
540-
{
541-
throw CommonExceptions.FileNotFound(sourceFileName);
542-
}
543-
if (!sourceFile.AllowedFileShare.HasFlag(FileShare.Delete))
544-
{
545-
throw CommonExceptions.ProcessCannotAccessFileInUse();
546-
}
547561
VerifyDirectoryExists(destFileName);
548562

549563
mockFileDataAccessor.RemoveFile(sourceFileName, false);
@@ -822,6 +836,11 @@ public override void Replace(string sourceFileName, string destinationFileName,
822836
throw CommonExceptions.FileNotFound(destinationFileName);
823837
}
824838

839+
if (mockFileDataAccessor.StringOperations.Equals(sourceFileName, destinationFileName) && XFS.IsWindowsPlatform())
840+
{
841+
throw CommonExceptions.ProcessCannotAccessFileInUse();
842+
}
843+
825844
if (destinationBackupFileName != null)
826845
{
827846
Copy(destinationFileName, destinationBackupFileName, overwrite: true);
@@ -1066,7 +1085,7 @@ public override void WriteAllBytes(string path, byte[] bytes)
10661085

10671086
mockFileDataAccessor.AddFile(path, mockFileDataAccessor.AdjustTimes(new MockFileData(bytes.ToArray()), TimeAdjustments.All));
10681087
}
1069-
1088+
10701089
#if FEATURE_FILE_SPAN
10711090
/// <inheritdoc cref="IFile.WriteAllBytes(string,ReadOnlySpan{byte})"/>
10721091
public override void WriteAllBytes(string path, ReadOnlySpan<byte> bytes)

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

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

249+
[Test]
250+
[WindowsOnly(WindowsSpecifics.Drives)]
251+
public async Task Copy_Should_ThrowIOException_When_OverwritingWithSameNameDifferentCase()
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+
await That(() => fileSystem.File.Copy(path, pathUpper, true)).Throws<IOException>().HasMessage($"The process cannot access the file '{pathUpper}' because it is being used by another process.");
260+
}
261+
249262
[Test]
250263
[WindowsOnly(WindowsSpecifics.Drives)]
251264
public async Task MockFile_Copy_ShouldThrowNotSupportedExceptionWhenSourcePathContainsInvalidDriveLetter()

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,26 @@ 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+
string sourceFilePath = @"c:\something\demo.txt";
257+
string destFilePath = @"c:\something\DEMO.TXT";
258+
string sourceFileContent = "content";
259+
260+
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
261+
{
262+
{
263+
sourceFilePath, new MockFileData(sourceFileContent)}
264+
});
265+
266+
fileSystem.File.Move(sourceFilePath, destFilePath);
267+
268+
await That(fileSystem.FileExists(destFilePath)).IsTrue();
269+
await That(fileSystem.GetFile(destFilePath).TextContents).IsEqualTo(sourceFileContent);
270+
}
271+
252272
[Test]
253273
public async Task MockFile_Move_ShouldThrowArgumentExceptionWhenSourceIsEmpty_Message()
254274
{

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_SameSourceAndDestination_ShouldThrowIOException()
737+
{
738+
string sourceFilePath = @"c:\something\demo.txt";
739+
string destFilePath = @"c:\something\Demo.txt";
740+
string fileContent = "content";
741+
742+
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
743+
{
744+
{ sourceFilePath, new MockFileData(fileContent) }
745+
});
746+
747+
await That(() => fileSystem.File.Replace(sourceFilePath, destFilePath, null, true)).Throws<IOException>().HasMessage("The process cannot access the file because it is being used by another process.");
748+
}
733749
}

0 commit comments

Comments
 (0)