Skip to content

Commit c66bf87

Browse files
authored
fix: correct MockFileInfo and MockDirectoryInfo for non existing files/directories (#834)
Fixes #829 Fixes #830 BREAKING CHANGE: `MockFileData.NullObject` is now `internal`. Users need to create new `MockFileData` instances for representing arbitrary data. Detailed changes: * make MockFileData.NullObject internal The `MockFileData.NullObject` was introduced in commit 0d85da3 as a way to represent the data of a non-existing file. Nevertheless, the issue #830 shows that at least one person used it as "some arbitrary MockFileData of an existing file I don't care about". Reason: As we are going to really make this the data representing a non-existing file by fixing/changing the `Attribute` property in one of the next commits, make this property internal and thereby breaking to force all consumers to really think about their tests and if they really ment a non-existing file or simply some arbirary data. Otherwise, the upcoming change would have nasty side effects if the NullObject is used for arbitrary data of an existing file. An alternative considered would be to rename this static property (which is also breaking) to any users, but decided against it as I couldn't think of a usecase where explicitly creating a file or directory which doesn't exist instead of relying on what the library will for you when being asked for a non-existing file. E.g. it is the difference of var fs = new MockFileSystem(); var nonExistingPath = XFS.Path(@"c:\some\non\existing\file.txt"); fs.AddFile(nonExistingPath, MockFileData.NullObject); var fi = fs.FileInfo.FromFilename(nonExistingPath); vs simply relying on the library to return the correct data for the non-existing file: var fs = new MockFileSystem(); var nonExistingPath = XFS.Path(@"c:\some\non\existing\file.txt"); var fi = fs.FileInfo.FromFilename(nonExistingPath); All tests are adapted to no longer use the MockFileData.NullObject. * fix: return -1 in MockFileInfo.Attributes and MockDirectoryInfo According to the Microsoft documentation[1] the `Attributes` property of FileSystemInfo (base of FileInfo and DirectoryInfo) returns (FileAttributes)(-1) for a file/directory which doesn't exist when the object instance is created. The on disk content is not looked at when calling the `Attributes` property, only a pre-cached value is returned. Only calling `Refresh` will read from disk. To be on the safe side, I checked the documented behaviour with the real implemenation with the below code snipped and it indeed shows the documented behaviour. var fs = new FileSystem(); var expected = (FileAttributes)(-1); var fi = fs.FileInfo.FromFileName(@"x:\not-existing\path"); Assert.That(fi.Attributes, Is.EqualTo(expected)); This lead to quite some needed code adaptions, especially in the area of the `GetMockFileDataForRead` methods, as those now no longer throw an exception. Furthermore, as cachedMockFileData is always a non-null instance, get rid of the logic checking for null in `GetMockFileDataForRead` and `MockFileInfo.CopyTo(string destFileName)` method. Also ensure that the used MockFileData is always `Clone`ed (and therefore even the MockFileData.NullObject) so that we operate on our own copy and don't get any updates done in the FileSystem. With this new semantics the `MockDiretory.Exists` method requires special treatment, as we now must check for a valid MockFileData by checking if Attributes != -1. [1]: https://docs.microsoft.com/en-us/dotnet/api/system.io.filesysteminfo.attributes?view=net-6.0#remarks * MockFileInfo: add test setting Attributes on non-existing file Ensure that when setting the Attributes property on a non existing file the appropriate FileNotFoundException is thrown, as documented in [1]. [1]: https://docs.microsoft.com/en-us/dotnet/api/system.io.filesysteminfo.attributes?view=net-6.0#remarks * MockDirectory: add tests for Time property getters on non existing file Add tests that when trying to retrieve the time properties of a non existing file returns 12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC) as documented in [1] [1]: https://docs.microsoft.com/en-us/dotnet/api/system.io.filesysteminfo.lastaccesstime?view=net-6.0#remarks * MockFileInfo: add tests for Time property getters on non existing file Add tests that when trying to retrieve the time properties of a non existing file returns 12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC) as documented in [1] [1]: https://docs.microsoft.com/en-us/dotnet/api/system.io.filesysteminfo.lastaccesstime?view=net-6.0#remarks * Fix MockDirectoryInfo: throw correct exception on non-existing directory When setting one of the Time properties or the `Attributes` property on a non-existing directory, a DirectoryNotFoundException should be thrown. Fix the code by throwing the correct exception (it did throw a FileNotFoundExecption before) and add the missing tests. * Increase major version to 17.0 MockFileData.NullObject was made internal, which is a breaking change, therefore increase the major version.
1 parent 94ba753 commit c66bf87

File tree

10 files changed

+249
-47
lines changed

10 files changed

+249
-47
lines changed

src/System.IO.Abstractions.TestingHelpers/MockDirectoryInfo.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public override void Delete()
4747
/// <inheritdoc />
4848
public override void Refresh()
4949
{
50-
cachedMockFileData = mockFileDataAccessor.GetFile(directoryPath)?.Clone();
50+
var mockFileData = mockFileDataAccessor.GetFile(directoryPath) ?? MockFileData.NullObject;
51+
cachedMockFileData = mockFileData.Clone();
5152
}
5253

5354
/// <inheritdoc />
@@ -74,7 +75,10 @@ public override DateTime CreationTimeUtc
7475
/// <inheritdoc />
7576
public override bool Exists
7677
{
77-
get { return GetMockFileDataForRead() != MockFileData.NullObject; }
78+
get {
79+
var mockFileData = GetMockFileDataForRead();
80+
return (int)mockFileData.Attributes != -1 && mockFileData.IsDirectory;
81+
}
7882
}
7983

8084
/// <inheritdoc />
@@ -396,14 +400,14 @@ private MockFileData GetMockFileDataForRead()
396400
Refresh();
397401
refreshOnNextRead = false;
398402
}
399-
return cachedMockFileData ?? MockFileData.NullObject;
403+
return cachedMockFileData;
400404
}
401405

402406
private MockFileData GetMockFileDataForWrite()
403407
{
404408
refreshOnNextRead = true;
405409
return mockFileDataAccessor.GetFile(directoryPath)
406-
?? throw CommonExceptions.FileNotFound(directoryPath);
410+
?? throw CommonExceptions.CouldNotFindPartOfPath(directoryPath);
407411
}
408412

409413
/// <inheritdoc />

src/System.IO.Abstractions.TestingHelpers/MockFileData.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ public class MockFileData
1717
public static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true);
1818

1919
/// <summary>
20-
/// The null object.
20+
/// The null object. It represents the data of a non-existing file or directory.
2121
/// </summary>
22-
public static readonly MockFileData NullObject = new MockFileData(string.Empty)
22+
internal static readonly MockFileData NullObject = new MockFileData(string.Empty)
2323
{
2424
LastWriteTime = new DateTime(1601, 01, 01, 00, 00, 00, DateTimeKind.Utc),
2525
LastAccessTime = new DateTime(1601, 01, 01, 00, 00, 00, DateTimeKind.Utc),
2626
CreationTime = new DateTime(1601, 01, 01, 00, 00, 00, DateTimeKind.Utc),
27-
Attributes = FileAttributes.Normal,
27+
Attributes = (FileAttributes)(-1),
2828
};
2929

3030
/// <summary>

src/System.IO.Abstractions.TestingHelpers/MockFileInfo.cs

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public override void Delete()
3535
/// <inheritdoc />
3636
public override void Refresh()
3737
{
38-
cachedMockFileData = mockFileSystem.GetFile(path)?.Clone();
38+
var mockFileData = mockFileSystem.GetFile(path)?.Clone();
39+
cachedMockFileData = mockFileData ?? MockFileData.NullObject.Clone();
3940
}
4041

4142
/// <inheritdoc />
@@ -88,8 +89,8 @@ public override bool Exists
8889
{
8990
get
9091
{
91-
var mockFileData = GetMockFileDataForRead(throwIfNotExisting: false);
92-
return mockFileData != null && !mockFileData.IsDirectory;
92+
var mockFileData = GetMockFileDataForRead();
93+
return (int)mockFileData.Attributes != -1 && !mockFileData.IsDirectory;
9394
}
9495
}
9596

@@ -203,14 +204,6 @@ public override IFileInfo CopyTo(string destFileName)
203204
/// <inheritdoc />
204205
public override IFileInfo CopyTo(string destFileName, bool overwrite)
205206
{
206-
if (!Exists)
207-
{
208-
var mockFileData = GetMockFileDataForRead(throwIfNotExisting: false);
209-
if (mockFileData == null)
210-
{
211-
throw CommonExceptions.FileNotFound(FullName);
212-
}
213-
}
214207
if (destFileName == FullName)
215208
{
216209
return this;
@@ -373,7 +366,7 @@ public override long Length
373366
{
374367
get
375368
{
376-
var mockFileData = GetMockFileDataForRead(throwIfNotExisting: false);
369+
var mockFileData = GetMockFileDataForRead();
377370
if (mockFileData == null || mockFileData.IsDirectory)
378371
{
379372
throw CommonExceptions.FileNotFound(path);
@@ -388,26 +381,14 @@ public override string ToString()
388381
return originalPath;
389382
}
390383

391-
private MockFileData GetMockFileDataForRead(bool throwIfNotExisting = true)
384+
private MockFileData GetMockFileDataForRead()
392385
{
393386
if (refreshOnNextRead)
394387
{
395388
Refresh();
396389
refreshOnNextRead = false;
397390
}
398-
var mockFileData = cachedMockFileData;
399-
if (mockFileData == null)
400-
{
401-
if (throwIfNotExisting)
402-
{
403-
throw CommonExceptions.FileNotFound(path);
404-
}
405-
else
406-
{
407-
return null;
408-
}
409-
}
410-
return mockFileData;
391+
return cachedMockFileData;
411392
}
412393

413394
private MockFileData GetMockFileDataForWrite()

tests/System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryInfoTests.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ public void MockDirectoryInfo_Exists(string path, bool expected)
5555
Assert.That(result, Is.EqualTo(expected));
5656
}
5757

58+
[Test]
59+
public void MockDirectoryInfo_Attributes_ShouldReturnMinusOneForNonExistingFile()
60+
{
61+
var fileSystem = new MockFileSystem();
62+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing\file.txt"));
63+
FileAttributes expected = (FileAttributes)(-1);
64+
65+
Assert.That(directoryInfo.Attributes, Is.EqualTo(expected));
66+
}
67+
68+
[Test]
69+
public void MockDirectoryInfo_Attributes_SetterShouldThrowDirectoryNotFoundEceptionOnNonExistingFileOrDirectory()
70+
{
71+
var fileSystem = new MockFileSystem();
72+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
73+
74+
Assert.That(() => directoryInfo.Attributes = FileAttributes.Hidden, Throws.TypeOf<DirectoryNotFoundException>());
75+
}
76+
5877
[Test]
5978
[WindowsOnly(WindowsSpecifics.UNCPaths)]
6079
public void MockDirectoryInfo_GetFiles_ShouldWorkWithUNCPath()
@@ -443,5 +462,126 @@ public void MockDirectoryInfo_LastAccessTime_ShouldReflectChangedValue()
443462
// Assert
444463
Assert.AreEqual(lastAccessTime, directoryInfo.LastAccessTime);
445464
}
465+
466+
[Test]
467+
public void MockDirectoryInfo_CreationTime_ShouldReturnDefaultTimeForNonExistingFile()
468+
{
469+
var fileSystem = new MockFileSystem();
470+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
471+
472+
var result = directoryInfo.CreationTime;
473+
474+
Assert.That(result, Is.EqualTo(MockFileData.DefaultDateTimeOffset.UtcDateTime));
475+
}
476+
477+
[Test]
478+
public void MockDirectoryInfo_LastAccessTime_ShouldReturnDefaultTimeForNonExistingFile()
479+
{
480+
var fileSystem = new MockFileSystem();
481+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
482+
483+
var result = directoryInfo.LastAccessTimeUtc;
484+
485+
Assert.That(result, Is.EqualTo(MockFileData.DefaultDateTimeOffset.UtcDateTime));
486+
}
487+
488+
[Test]
489+
public void MockDirectoryInfo_LastWriteTime_ShouldReturnDefaultTimeForNonExistingFile()
490+
{
491+
var fileSystem = new MockFileSystem();
492+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
493+
494+
var result = directoryInfo.LastWriteTimeUtc;
495+
496+
Assert.That(result, Is.EqualTo(MockFileData.DefaultDateTimeOffset.UtcDateTime));
497+
}
498+
499+
[Test]
500+
public void MockDirectoryInfo_CreationTimeUtc_ShouldReturnDefaultTimeForNonExistingFile()
501+
{
502+
var fileSystem = new MockFileSystem();
503+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
504+
505+
var result = directoryInfo.CreationTimeUtc;
506+
507+
Assert.That(result, Is.EqualTo(MockFileData.DefaultDateTimeOffset.UtcDateTime));
508+
}
509+
510+
[Test]
511+
public void MockDirectoryInfo_LastAccessTimeUtc_ShouldReturnDefaultTimeForNonExistingFile()
512+
{
513+
var fileSystem = new MockFileSystem();
514+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
515+
516+
var result = directoryInfo.LastAccessTimeUtc;
517+
518+
Assert.That(result, Is.EqualTo(MockFileData.DefaultDateTimeOffset.UtcDateTime));
519+
}
520+
521+
[Test]
522+
public void MockDirectoryInfo_LastWriteTimeUtc_ShouldReturnDefaultTimeForNonExistingFile()
523+
{
524+
var fileSystem = new MockFileSystem();
525+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
526+
527+
var result = directoryInfo.LastWriteTimeUtc;
528+
529+
Assert.That(result, Is.EqualTo(MockFileData.DefaultDateTimeOffset.UtcDateTime));
530+
}
531+
532+
public void MockDirectoryInfo_CreationTime_SetterShouldThrowDirectoryNotFoundExceptionForNonExistingDirectory()
533+
{
534+
var newTime = new DateTime(2022, 04, 06);
535+
var fileSystem = new MockFileSystem();
536+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
537+
538+
Assert.That(() => directoryInfo.CreationTime = newTime, Throws.TypeOf<DirectoryNotFoundException>());
539+
}
540+
541+
public void MockDirectoryInfo_LastAccessTime_SetterShouldThrowDirectoryNotFoundExceptionForNonExistingDirectory()
542+
{
543+
var newTime = new DateTime(2022, 04, 06);
544+
var fileSystem = new MockFileSystem();
545+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
546+
547+
Assert.That(() => directoryInfo.LastAccessTime = newTime, Throws.TypeOf<DirectoryNotFoundException>());
548+
}
549+
550+
public void MockDirectoryInfo_LastWriteTime_SetterShouldThrowDirectoryNotFoundExceptionForNonExistingDirectory()
551+
{
552+
var newTime = new DateTime(2022, 04, 06);
553+
var fileSystem = new MockFileSystem();
554+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
555+
556+
Assert.That(() => directoryInfo.LastWriteTime = newTime, Throws.TypeOf<DirectoryNotFoundException>());
557+
}
558+
559+
public void MockDirectoryInfo_CreationTimeUtc_SetterShouldThrowDirectoryNotFoundExceptionForNonExistingDirectory()
560+
{
561+
var newTime = new DateTime(2022, 04, 06);
562+
var fileSystem = new MockFileSystem();
563+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
564+
565+
Assert.That(() => directoryInfo.CreationTimeUtc = newTime, Throws.TypeOf<DirectoryNotFoundException>());
566+
}
567+
568+
public void MockDirectoryInfo_LastAccessTimeUtc_SetterShouldThrowDirectoryNotFoundExceptionForNonExistingDirectory()
569+
{
570+
var newTime = new DateTime(2022, 04, 06);
571+
var fileSystem = new MockFileSystem();
572+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
573+
574+
Assert.That(() => directoryInfo.LastAccessTimeUtc = newTime, Throws.TypeOf<DirectoryNotFoundException>());
575+
}
576+
577+
public void MockDirectoryInfo_LastWriteTimeUtc_SetterShouldThrowDirectoryNotFoundExceptionForNonExistingDirectory()
578+
{
579+
var newTime = new DateTime(2022, 04, 06);
580+
var fileSystem = new MockFileSystem();
581+
var directoryInfo = new MockDirectoryInfo(fileSystem, XFS.Path(@"c:\non\existing"));
582+
583+
Assert.That(() => directoryInfo.LastWriteTime = newTime, Throws.TypeOf<DirectoryNotFoundException>());
584+
}
585+
446586
}
447587
}

tests/System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,12 +1812,13 @@ public void MockDirectory_EnumerateFiles_ShouldFilterByExtensionBasedSearchPatte
18121812
public void MockDirectory_EnumerateFiles_WhenFilterIsUnRooted_ShouldFindFilesInCurrentDirectory()
18131813
{
18141814
// Arrange
1815+
var someContent = new MockFileData(String.Empty);
18151816
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
18161817
{
1817-
{ XFS.Path(@"c:\a.txt"), MockFileData.NullObject },
1818-
{ XFS.Path(@"c:\a\a.txt"), MockFileData.NullObject },
1819-
{ XFS.Path(@"c:\a\b\b.txt"), MockFileData.NullObject },
1820-
{ XFS.Path(@"c:\a\c\c.txt"), MockFileData.NullObject },
1818+
{ XFS.Path(@"c:\a.txt"), someContent },
1819+
{ XFS.Path(@"c:\a\a.txt"), someContent },
1820+
{ XFS.Path(@"c:\a\b\b.txt"), someContent },
1821+
{ XFS.Path(@"c:\a\c\c.txt"), someContent },
18211822
});
18221823

18231824
var expected = new[]
@@ -1838,11 +1839,12 @@ public void MockDirectory_EnumerateFiles_WhenFilterIsUnRooted_ShouldFindFilesInC
18381839
public void MockDirectory_EnumerateFiles_WhenFilterIsUnRooted_ShouldNotFindFilesInPathOutsideCurrentDirectory()
18391840
{
18401841
// Arrange
1842+
var someContent = new MockFileData(String.Empty);
18411843
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
18421844
{
1843-
{ XFS.Path(@"c:\a.txt"), MockFileData.NullObject },
1844-
{ XFS.Path(@"c:\a\b\b.txt"), MockFileData.NullObject },
1845-
{ XFS.Path(@"c:\c\b\b.txt"), MockFileData.NullObject },
1845+
{ XFS.Path(@"c:\a.txt"), someContent },
1846+
{ XFS.Path(@"c:\a\b\b.txt"), someContent },
1847+
{ XFS.Path(@"c:\c\b\b.txt"), someContent },
18461848
});
18471849

18481850
var expected = new[]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public void MockFile_Copy_ShouldThrowExceptionWhenFolderInDestinationDoesNotExis
110110
string destFileName = XFS.Path(destFilePath);
111111
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
112112
{
113-
{sourceFileName, MockFileData.NullObject}
113+
{sourceFileName, string.Empty}
114114
});
115115

116116
Assert.Throws<DirectoryNotFoundException>(() => fileSystem.File.Copy(sourceFileName, destFileName), string.Format(CultureInfo.InvariantCulture, @"Could not find a part of the path '{0}'.", destFilePath));

0 commit comments

Comments
 (0)