diff --git a/Source/Testably.Abstractions.Testing/FileSystem/DirectoryInfoMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/DirectoryInfoMock.cs index 7433bc832..35b13131b 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/DirectoryInfoMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/DirectoryInfoMock.cs @@ -35,7 +35,7 @@ public override bool Exists .DirectoryInfo.RegisterPathProperty(Location.FullPath, nameof(Exists), PropertyAccess.Get); - return base.Exists && FileSystemType == FileSystemTypes.Directory; + return base.Exists && State.Type == FileSystemTypes.Directory; } } @@ -158,7 +158,7 @@ public IEnumerable EnumerateDirectories( FullName, searchPattern, EnumerationOptionsHelper.FromSearchOption(searchOption)) - .Select(location => New(location, _fileSystem)); + .Select(location => WithInitializedState(New(location, _fileSystem))); } #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS @@ -175,7 +175,7 @@ public IEnumerable EnumerateDirectories( FullName, searchPattern, enumerationOptions) - .Select(location => New(location, _fileSystem)); + .Select(location => WithInitializedState(New(location, _fileSystem))); } #endif @@ -212,7 +212,7 @@ public IEnumerable EnumerateFiles( FullName, searchPattern, EnumerationOptionsHelper.FromSearchOption(searchOption)) - .Select(location => FileInfoMock.New(location, _fileSystem)); + .Select(location => WithInitializedState(FileInfoMock.New(location, _fileSystem))); } #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS @@ -228,7 +228,7 @@ public IEnumerable EnumerateFiles( FullName, searchPattern, enumerationOptions) - .Select(location => FileInfoMock.New(location, _fileSystem)); + .Select(location => WithInitializedState(FileInfoMock.New(location, _fileSystem))); } #endif @@ -266,7 +266,7 @@ public IEnumerable EnumerateFileSystemInfos( FullName, searchPattern, EnumerationOptionsHelper.FromSearchOption(searchOption)) - .Select(location => FileSystemInfoMock.New(location, _fileSystem)); + .Select(location => WithInitializedState(FileSystemInfoMock.New(location, _fileSystem))); } #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS @@ -283,7 +283,7 @@ public IEnumerable EnumerateFileSystemInfos( FullName, searchPattern, enumerationOptions) - .Select(location => FileSystemInfoMock.New(location, _fileSystem)); + .Select(location => WithInitializedState(FileSystemInfoMock.New(location, _fileSystem))); } #endif @@ -431,6 +431,7 @@ public void MoveTo(string destDirName) .EnsureValidFormat(_fileSystem, nameof(destDirName))), recursive: true) ?? throw ExceptionFactory.DirectoryNotFound(FullName); + ResetCache(true); } #endregion @@ -447,6 +448,13 @@ public void MoveTo(string destDirName) return new DirectoryInfoMock(location, fileSystem); } + private static T WithInitializedState(T fileSystemInfo) + where T : FileSystemInfoMock + { + fileSystemInfo.InitializeState(); + return fileSystemInfo; + } + private IEnumerable EnumerateInternal( FileSystemTypes fileSystemTypes, string path, diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileInfoMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileInfoMock.cs index 41da90dc7..3df4cded1 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileInfoMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileInfoMock.cs @@ -60,7 +60,7 @@ public override bool Exists .FileInfo.RegisterPathProperty(Location.FullPath, nameof(Exists), PropertyAccess.Get); - return base.Exists && FileSystemType == FileSystemTypes.File; + return base.Exists && State.Type == FileSystemTypes.File; } } @@ -101,8 +101,7 @@ public long Length .FileInfo.RegisterPathProperty(Location.FullPath, nameof(Length), PropertyAccess.Get); - if (Container is NullContainer || - Container.Type != FileSystemTypes.File) + if (!State.Exists || State.Type != FileSystemTypes.File) { throw ExceptionFactory.FileNotFound( _fileSystem.Execute.IsNetFramework @@ -110,7 +109,7 @@ public long Length : Location.FullPath); } - return Container.GetBytes().Length; + return State.Length; } } @@ -138,7 +137,11 @@ public StreamWriter AppendText() using IDisposable registration = _fileSystem.StatisticsRegistration .FileInfo.RegisterPathMethod(Location.FullPath, nameof(AppendText)); - return new StreamWriter(Open(FileMode.Append, FileAccess.Write)); + StreamWriter streamWriter = new(Open(FileMode.Append, FileAccess.Write)); +#if NET8_0_OR_GREATER + ResetCache(true); +#endif + return streamWriter; } /// @@ -179,12 +182,9 @@ public FileSystemStream Create() using IDisposable registration = _fileSystem.StatisticsRegistration .FileInfo.RegisterPathMethod(Location.FullPath, nameof(Create)); - if (!_fileSystem.Execute.IsNetFramework) - { - Refresh(); - } - - return _fileSystem.File.Create(FullName); + FileSystemStream stream = _fileSystem.File.Create(FullName); + ResetCache(!_fileSystem.Execute.IsNetFramework); + return stream; } /// @@ -195,7 +195,7 @@ public StreamWriter CreateText() StreamWriter streamWriter = new(_fileSystem.File.Create(FullName)); #if NET8_0_OR_GREATER - Refresh(); + ResetCache(true); #endif return streamWriter; } @@ -232,6 +232,7 @@ public void MoveTo(string destFileName) _fileSystem.Storage.GetLocation(destFileName .EnsureValidArgument(_fileSystem, nameof(destFileName)))) ?? throw ExceptionFactory.FileNotFound(FullName); + ResetCache(true); } #if FEATURE_FILE_MOVETO_OVERWRITE @@ -248,6 +249,7 @@ public void MoveTo(string destFileName, bool overwrite) .EnsureValidArgument(_fileSystem, nameof(destFileName))), overwrite) ?? throw ExceptionFactory.FileNotFound(FullName); + ResetCache(true); } #endif diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemInfoMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemInfoMock.cs index a3beea2c2..91af43b91 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemInfoMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemInfoMock.cs @@ -28,9 +28,13 @@ protected IStorageContainer Container set => _container = value; } - private bool? _exists; - private bool _isInitialized; private IStorageContainer _container; + private bool _isInitialized; +#if FEATURE_FILESYSTEM_LINK + private bool _isLinkTargetCached; + private string? _linkTarget; +#endif + private CachedState? _state; protected FileSystemInfoMock(MockFileSystem fileSystem, IStorageLocation location, FileSystemTypes fileSystemType) @@ -53,14 +57,15 @@ public FileAttributes Attributes using IDisposable registration = RegisterPathProperty(nameof(Attributes), PropertyAccess.Get); - return Container.Attributes; + return State.Attributes; } set { using IDisposable registration = RegisterPathProperty(nameof(Attributes), PropertyAccess.Set); - Container.Attributes = value; + _fileSystem.Storage.GetContainer(Location).Attributes = value; + ResetCache(true); } } @@ -86,6 +91,7 @@ public void CreateAsSymbolicLink(string pathToTarget) { Container = container; container.LinkTarget = pathToTarget; + ResetCache(true); } else { @@ -104,14 +110,15 @@ public DateTime CreationTime using IDisposable registration = RegisterPathProperty(nameof(CreationTime), PropertyAccess.Get); - return Container.CreationTime.Get(DateTimeKind.Local); + return State.CreationTimeLocal; } set { using IDisposable registration = RegisterPathProperty(nameof(CreationTime), PropertyAccess.Set); - Container.CreationTime.Set(value, DateTimeKind.Local); + _fileSystem.Storage.GetContainer(Location).CreationTime.Set(value, DateTimeKind.Local); + ResetCache(true); } } @@ -123,14 +130,15 @@ public DateTime CreationTimeUtc using IDisposable registration = RegisterPathProperty(nameof(CreationTimeUtc), PropertyAccess.Get); - return Container.CreationTime.Get(DateTimeKind.Utc); + return State.CreationTimeUtc; } set { using IDisposable registration = RegisterPathProperty(nameof(CreationTimeUtc), PropertyAccess.Set); - Container.CreationTime.Set(value, DateTimeKind.Utc); + _fileSystem.Storage.GetContainer(Location).CreationTime.Set(value, DateTimeKind.Utc); + ResetCache(true); } } @@ -145,15 +153,7 @@ public virtual void Delete() /// public virtual bool Exists - { - get - { - RefreshInternal(); - _exists ??= !string.IsNullOrWhiteSpace(Location.FriendlyName) && - Container is not NullContainer; - return _exists.Value; - } - } + => State.Exists; /// public string Extension @@ -197,14 +197,15 @@ public DateTime LastAccessTime using IDisposable registration = RegisterPathProperty(nameof(LastAccessTime), PropertyAccess.Get); - return Container.LastAccessTime.Get(DateTimeKind.Local); + return State.LastAccessTimeLocal; } set { using IDisposable registration = RegisterPathProperty(nameof(LastAccessTime), PropertyAccess.Set); - Container.LastAccessTime.Set(value, DateTimeKind.Local); + _fileSystem.Storage.GetContainer(Location).LastAccessTime.Set(value, DateTimeKind.Local); + ResetCache(true); } } @@ -216,14 +217,15 @@ public DateTime LastAccessTimeUtc using IDisposable registration = RegisterPathProperty(nameof(LastAccessTimeUtc), PropertyAccess.Get); - return Container.LastAccessTime.Get(DateTimeKind.Utc); + return State.LastAccessTimeUtc; } set { using IDisposable registration = RegisterPathProperty(nameof(LastAccessTimeUtc), PropertyAccess.Set); - Container.LastAccessTime.Set(value, DateTimeKind.Utc); + _fileSystem.Storage.GetContainer(Location).LastAccessTime.Set(value, DateTimeKind.Utc); + ResetCache(true); } } @@ -235,14 +237,15 @@ public DateTime LastWriteTime using IDisposable registration = RegisterPathProperty(nameof(LastWriteTime), PropertyAccess.Get); - return Container.LastWriteTime.Get(DateTimeKind.Local); + return State.LastWriteTimeLocal; } set { using IDisposable registration = RegisterPathProperty(nameof(LastWriteTime), PropertyAccess.Set); - Container.LastWriteTime.Set(value, DateTimeKind.Local); + _fileSystem.Storage.GetContainer(Location).LastWriteTime.Set(value, DateTimeKind.Local); + ResetCache(true); } } @@ -254,14 +257,15 @@ public DateTime LastWriteTimeUtc using IDisposable registration = RegisterPathProperty(nameof(LastWriteTimeUtc), PropertyAccess.Get); - return Container.LastWriteTime.Get(DateTimeKind.Utc); + return State.LastWriteTimeUtc; } set { using IDisposable registration = RegisterPathProperty(nameof(LastWriteTimeUtc), PropertyAccess.Set); - Container.LastWriteTime.Set(value, DateTimeKind.Utc); + _fileSystem.Storage.GetContainer(Location).LastWriteTime.Set(value, DateTimeKind.Utc); + ResetCache(true); } } @@ -274,7 +278,13 @@ public string? LinkTarget using IDisposable registration = RegisterPathProperty(nameof(LinkTarget), PropertyAccess.Get); - return Container.LinkTarget; + if (!_isLinkTargetCached) + { + _linkTarget = _fileSystem.Storage.GetContainer(Location).LinkTarget; + _isLinkTargetCached = true; + } + + return _linkTarget; } } #endif @@ -306,7 +316,7 @@ public UnixFileMode UnixFileMode using IDisposable registration = RegisterPathProperty(nameof(UnixFileMode), PropertyAccess.Get); - return Container.UnixFileMode; + return State.UnixFileMode; } [UnsupportedOSPlatform("windows")] set @@ -319,7 +329,8 @@ public UnixFileMode UnixFileMode throw ExceptionFactory.UnixFileModeNotSupportedOnThisPlatform(); } - Container.UnixFileMode = value; + _fileSystem.Storage.GetContainer(Location).UnixFileMode = value; + ResetCache(true); } } #endif @@ -330,6 +341,7 @@ public void Refresh() using IDisposable registration = RegisterPathMethod(nameof(Refresh)); ResetCache(true); + _state = CaptureState(); } #if FEATURE_FILESYSTEM_LINK @@ -389,16 +401,42 @@ internal static FileSystemInfoMock New(IStorageLocation location, FileSystemTypes.DirectoryOrFile); } - protected void ResetCache(bool resetExistsCache) + /// + /// The state of the file or directory, captured on first access and kept until + /// or an operation that invalidates it, like does. + /// + protected CachedState State + => _state ??= CaptureState(); + + /// + /// Captures the state now, as instances returned from an enumeration + /// already carry the state found during the enumeration. + /// + internal void InitializeState() + => _state = CaptureState(); + + protected void ResetCache(bool resetState) { - if (resetExistsCache) + if (resetState) { - _exists = null; + _state = null; +#if FEATURE_FILESYSTEM_LINK + _isLinkTargetCached = false; + _linkTarget = null; +#endif } _isInitialized = false; } + private CachedState CaptureState() + { + RefreshInternal(); + return new CachedState(Container, + !string.IsNullOrWhiteSpace(Location.FriendlyName) && + Container is not NullContainer); + } + private void RefreshInternal() { if (_isInitialized) @@ -418,4 +456,38 @@ protected virtual IDisposable RegisterPathMethod(string name) protected virtual IDisposable RegisterPathMethod(string name, T1 parameter1) => new NoOpDisposable(); + + protected sealed class CachedState + { + public FileAttributes Attributes { get; } + public DateTime CreationTimeLocal { get; } + public DateTime CreationTimeUtc { get; } + public bool Exists { get; } + public DateTime LastAccessTimeLocal { get; } + public DateTime LastAccessTimeUtc { get; } + public DateTime LastWriteTimeLocal { get; } + public DateTime LastWriteTimeUtc { get; } + public long Length { get; } + public FileSystemTypes Type { get; } +#if FEATURE_FILESYSTEM_UNIXFILEMODE + public UnixFileMode UnixFileMode { get; } +#endif + + public CachedState(IStorageContainer container, bool exists) + { + Exists = exists; + Type = container.Type; + Attributes = container.Attributes; + CreationTimeLocal = container.CreationTime.Get(DateTimeKind.Local); + CreationTimeUtc = container.CreationTime.Get(DateTimeKind.Utc); + LastAccessTimeLocal = container.LastAccessTime.Get(DateTimeKind.Local); + LastAccessTimeUtc = container.LastAccessTime.Get(DateTimeKind.Utc); + LastWriteTimeLocal = container.LastWriteTime.Get(DateTimeKind.Local); + LastWriteTimeUtc = container.LastWriteTime.Get(DateTimeKind.Utc); + Length = container.GetBytes().Length; +#if FEATURE_FILESYSTEM_UNIXFILEMODE + UnixFileMode = container.UnixFileMode; +#endif + } + } } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateDirectoriesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateDirectoriesTests.cs index a1d7b9814..f0c61a9b8 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateDirectoriesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateDirectoriesTests.cs @@ -67,6 +67,22 @@ await That(result).IsEmpty() } } + [Test] + [AutoArguments] + public async Task EnumerateDirectories_ShouldReturnInstancesWithStateFromEnumeration( + string path, string subdirectoryName) + { + string subdirectoryPath = FileSystem.Path.Combine(path, subdirectoryName); + FileSystem.Directory.CreateDirectory(subdirectoryPath); + + IDirectoryInfo result = FileSystem.DirectoryInfo.New(path).EnumerateDirectories().Single(); + FileSystem.Directory.Delete(subdirectoryPath); + + await That(result.Exists).IsTrue(); + result.Refresh(); + await That(result.Exists).IsFalse(); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Test] [AutoArguments] diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFileSystemInfosTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFileSystemInfosTests.cs index 317b49ed3..d5cef1762 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFileSystemInfosTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFileSystemInfosTests.cs @@ -101,6 +101,22 @@ await That(result).Contains(d d is IFileInfo); } + [Test] + [AutoArguments] + public async Task EnumerateFileSystemInfos_ShouldReturnInstancesWithStateFromEnumeration( + string path, string subdirectoryName) + { + string subdirectoryPath = FileSystem.Path.Combine(path, subdirectoryName); + FileSystem.Directory.CreateDirectory(subdirectoryPath); + + IFileSystemInfo result = FileSystem.DirectoryInfo.New(path).EnumerateFileSystemInfos().Single(); + FileSystem.Directory.Delete(subdirectoryPath); + + await That(result.Exists).IsTrue(); + result.Refresh(); + await That(result.Exists).IsFalse(); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Test] public async Task EnumerateFileSystemInfos_WithEnumerationOptions_ShouldConsiderSetOptions() diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFilesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFilesTests.cs index cde70d353..827f12c17 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFilesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/EnumerateFilesTests.cs @@ -73,6 +73,25 @@ await That(result).HasSingle() } } + [Test] + [AutoArguments] + public async Task EnumerateFiles_ShouldReturnInstancesWithStateFromEnumeration( + string path, string fileName) + { + string filePath = FileSystem.Path.Combine(path, fileName); + FileSystem.Directory.CreateDirectory(path); + FileSystem.File.WriteAllText(filePath, "abc"); + + IFileInfo result = FileSystem.DirectoryInfo.New(path).EnumerateFiles().Single(); + FileSystem.File.WriteAllText(filePath, "abcdefghij"); + + await That(result.Length).IsEqualTo(3); + FileSystem.File.Delete(filePath); + await That(result.Exists).IsTrue(); + result.Refresh(); + await That(result.Exists).IsFalse(); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Test] public async Task EnumerateFiles_WithEnumerationOptions_ShouldConsiderSetOptions() diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetDirectoriesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetDirectoriesTests.cs index 710a575fe..d43d122db 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetDirectoriesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetDirectoriesTests.cs @@ -67,6 +67,22 @@ await That(result).IsEmpty() } } + [Test] + [AutoArguments] + public async Task GetDirectories_ShouldReturnInstancesWithStateFromEnumeration( + string path, string subdirectoryName) + { + string subdirectoryPath = FileSystem.Path.Combine(path, subdirectoryName); + FileSystem.Directory.CreateDirectory(subdirectoryPath); + + IDirectoryInfo result = FileSystem.DirectoryInfo.New(path).GetDirectories().Single(); + FileSystem.Directory.Delete(subdirectoryPath); + + await That(result.Exists).IsTrue(); + result.Refresh(); + await That(result.Exists).IsFalse(); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Test] [AutoArguments] diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFileSystemInfosTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFileSystemInfosTests.cs index a63551f12..026f6f16a 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFileSystemInfosTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFileSystemInfosTests.cs @@ -101,6 +101,23 @@ await That(result).Contains(d d is IFileInfo); } + [Test] + [AutoArguments] + public async Task GetFileSystemInfos_ShouldReturnInstancesWithStateFromEnumeration( + string path, string fileName) + { + string filePath = FileSystem.Path.Combine(path, fileName); + FileSystem.Directory.CreateDirectory(path); + FileSystem.File.WriteAllText(filePath, "abc"); + + IFileSystemInfo result = FileSystem.DirectoryInfo.New(path).GetFileSystemInfos().Single(); + FileSystem.File.Delete(filePath); + + await That(result.Exists).IsTrue(); + result.Refresh(); + await That(result.Exists).IsFalse(); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Test] public async Task GetFileSystemInfos_WithEnumerationOptions_ShouldConsiderSetOptions() diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFilesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFilesTests.cs index a16789f9f..4ad1d50e4 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFilesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/GetFilesTests.cs @@ -73,6 +73,25 @@ await That(result).HasSingle() } } + [Test] + [AutoArguments] + public async Task GetFiles_ShouldReturnInstancesWithStateFromEnumeration( + string path, string fileName) + { + string filePath = FileSystem.Path.Combine(path, fileName); + FileSystem.Directory.CreateDirectory(path); + FileSystem.File.WriteAllText(filePath, "abc"); + + IFileInfo result = FileSystem.DirectoryInfo.New(path).GetFiles().Single(); + FileSystem.File.WriteAllText(filePath, "abcdefghij"); + + await That(result.Length).IsEqualTo(3); + FileSystem.File.Delete(filePath); + await That(result.Exists).IsTrue(); + result.Refresh(); + await That(result.Exists).IsFalse(); + } + #if FEATURE_FILESYSTEM_ENUMERATION_OPTIONS [Test] public async Task GetFiles_WithEnumerationOptions_ShouldConsiderSetOptions() diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/RefreshTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/RefreshTests.cs new file mode 100644 index 000000000..17a32792b --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/FileSystem/DirectoryInfo/RefreshTests.cs @@ -0,0 +1,76 @@ +namespace Testably.Abstractions.Tests.FileSystem.DirectoryInfo; + +[FileSystemTests] +public class RefreshTests(FileSystemTestData testData) : FileSystemTestBase(testData) +{ + private static readonly DateTime OtherTime = new(2002, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + [Test] + [AutoArguments] + public async Task CreateSubdirectory_ShouldNotResetCachedState(string path, string subdirectory) + { + FileSystem.Directory.CreateDirectory(path); + FileSystem.Directory.SetLastWriteTimeUtc(path, OtherTime); + IDirectoryInfo sut = FileSystem.DirectoryInfo.New(path); + await That(sut.Exists).IsTrue(); + + sut.CreateSubdirectory(subdirectory); + + await That(sut.LastWriteTimeUtc).IsEqualTo(OtherTime); + } + + [Test] + [AutoArguments] + public async Task Exists_ShouldReturnCachedValueFromOtherProperty(string path) + { + FileSystem.Directory.CreateDirectory(path); + IDirectoryInfo sut = FileSystem.DirectoryInfo.New(path); + _ = sut.Attributes; + + FileSystem.Directory.Delete(path); + + await That(sut.Exists).IsTrue(); + } + + [Test] + [AutoArguments] + public async Task LastWriteTime_Set_ShouldResetCachedState(string path) + { + FileSystem.Directory.CreateDirectory(path); + IDirectoryInfo sut = FileSystem.DirectoryInfo.New(path); + await That(sut.LastWriteTimeUtc).IsNotEqualTo(OtherTime); + + sut.LastWriteTimeUtc = OtherTime; + + await That(sut.LastWriteTimeUtc).IsEqualTo(OtherTime); + } + + [Test] + [AutoArguments] + public async Task LastWriteTime_ShouldReturnCachedValueUntilRefresh(string path) + { + FileSystem.Directory.CreateDirectory(path); + IDirectoryInfo sut = FileSystem.DirectoryInfo.New(path); + await That(sut.Exists).IsTrue(); + + FileSystem.Directory.SetLastWriteTimeUtc(path, OtherTime); + + await That(sut.LastWriteTimeUtc).IsNotEqualTo(OtherTime); + sut.Refresh(); + await That(sut.LastWriteTimeUtc).IsEqualTo(OtherTime); + } + + [Test] + [AutoArguments] + public async Task MoveTo_ShouldResetCachedState(string path, string destination) + { + FileSystem.Directory.CreateDirectory(path); + IDirectoryInfo sut = FileSystem.DirectoryInfo.New(path); + await That(sut.Exists).IsTrue(); + FileSystem.Directory.SetLastWriteTimeUtc(path, OtherTime); + + sut.MoveTo(destination); + + await That(sut.LastWriteTimeUtc).IsEqualTo(OtherTime); + } +} diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/AppendTextTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/AppendTextTests.cs index c2dd4ee46..6d7c7976b 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/AppendTextTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/AppendTextTests.cs @@ -35,4 +35,31 @@ public async Task AppendText_ShouldAddTextToExistingFile( await That(FileSystem).HasFile(path).WithContent(contents + appendText); } + +#if NET8_0_OR_GREATER + [Test] + [AutoArguments] + public async Task AppendText_ShouldRefreshExistsCache(string path) + { + IFileInfo fileInfo = FileSystem.FileInfo.New(path); + await That(fileInfo.Exists).IsFalse(); + + fileInfo.AppendText().Dispose(); + + await That(fileInfo.Exists).IsTrue(); + } +#else + [Test] + [AutoArguments] + public async Task AppendText_ShouldNotRefreshExistsCache(string path) + { + IFileInfo fileInfo = FileSystem.FileInfo.New(path); + await That(fileInfo.Exists).IsFalse(); + + fileInfo.AppendText().Dispose(); + + await That(fileInfo.Exists).IsFalse(); + await That(FileSystem.File.Exists(path)).IsTrue(); + } +#endif } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/CreateAsSymbolicLinkTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/CreateAsSymbolicLinkTests.cs index cfa73604c..76d877b1c 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/CreateAsSymbolicLinkTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/CreateAsSymbolicLinkTests.cs @@ -19,6 +19,20 @@ await That(FileSystem.File.GetAttributes(path)) .HasFlag(FileAttributes.ReparsePoint); } + [Test] + [AutoArguments] + public async Task CreateAsSymbolicLink_ShouldRefreshExistsCache( + string path, string pathToTarget) + { + FileSystem.File.WriteAllText(pathToTarget, null); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsFalse(); + + sut.CreateAsSymbolicLink(pathToTarget); + + await That(sut.Exists).IsTrue(); + } + [Test] [AutoArguments] public async Task CreateAsSymbolicLink_SourceFileAlreadyExists_ShouldThrowIOException( diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/DeleteTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/DeleteTests.cs index b58eaa268..570b5f82f 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/DeleteTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/DeleteTests.cs @@ -55,6 +55,31 @@ public async Task Delete_ShouldRefreshExistsCache_ExceptOnNetFramework(string pa await That(FileSystem.File.Exists(path)).IsFalse(); } + [Test] + [AutoArguments] + public async Task Delete_ShouldResetCachedState_ExceptOnNetFramework(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Length).IsEqualTo(3); + + sut.Delete(); + + if (Test.IsNetFramework) + { + await That(sut.Length).IsEqualTo(3); + } + else + { + void Act() + { + _ = sut.Length; + } + + await That(Act).Throws(); + } + } + [Test] [AutoArguments] public async Task Delete_WithOpenFile_ShouldThrowIOException_OnWindows(string filename) diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/EncryptDecryptTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/EncryptDecryptTests.cs index 04e285bb8..87d1fcada 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/EncryptDecryptTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/EncryptDecryptTests.cs @@ -56,6 +56,7 @@ public async Task Encrypt_Decrypt_ShouldChangeEncryptedFileAttribute( sut.Encrypt(); await That(sut).HasAttribute(FileAttributes.Encrypted); sut.Decrypt(); + sut.Refresh(); await That(sut).DoesNotHaveAttribute(FileAttributes.Encrypted); } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/MoveToTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/MoveToTests.cs index 43e672b90..011ee087c 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/MoveToTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/MoveToTests.cs @@ -181,6 +181,20 @@ await That(FileSystem.File.GetLastWriteTime(destinationName)) .IsEqualTo(sourceLastWriteTime); } + [Test] + [AutoArguments] + public async Task MoveTo_ShouldResetCachedState(string sourceName, string destinationName) + { + FileSystem.File.WriteAllText(sourceName, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(sourceName); + await That(sut.Length).IsEqualTo(3); + FileSystem.File.WriteAllText(sourceName, "abcdefghij"); + + sut.MoveTo(destinationName); + + await That(sut.Length).IsEqualTo(10); + } + [Test] [AutoArguments] public async Task MoveTo_ShouldMoveFileWithContent( diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/OpenTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/OpenTests.cs index a3eefbb1a..98e6b4a2e 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/OpenTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/OpenTests.cs @@ -170,6 +170,19 @@ void Act() await That(FileSystem.File.Exists(path)).IsTrue(); } + [Test] + [AutoArguments] + public async Task Open_ShouldNotResetCachedState(string path) + { + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsFalse(); + + sut.Open(FileMode.Create).Dispose(); + + await That(sut.Exists).IsFalse(); + await That(FileSystem.File.Exists(path)).IsTrue(); + } + [Test] [AutoArguments] public async Task Open_ShouldOpenWithReadAndWriteAccess( diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/RefreshTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/RefreshTests.cs new file mode 100644 index 000000000..9f4fd09ed --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileInfo/RefreshTests.cs @@ -0,0 +1,236 @@ +using System.IO; + +namespace Testably.Abstractions.Tests.FileSystem.FileInfo; + +[FileSystemTests] +public class RefreshTests(FileSystemTestData testData) : FileSystemTestBase(testData) +{ + private static readonly DateTime OtherTime = new(2002, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + [Test] + [AutoArguments] + public async Task Attributes_ShouldReturnCachedValueUntilRefresh(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Attributes).DoesNotHaveFlag(FileAttributes.ReadOnly); + + FileSystem.File.SetAttributes(path, FileAttributes.ReadOnly); + + await That(sut.Attributes).DoesNotHaveFlag(FileAttributes.ReadOnly); + await That(sut.IsReadOnly).IsFalse(); + sut.Refresh(); + await That(sut.Attributes).HasFlag(FileAttributes.ReadOnly); + await That(sut.IsReadOnly).IsTrue(); + FileSystem.File.SetAttributes(path, FileAttributes.Normal); + } + + [Test] + [AutoArguments] + public async Task Attributes_Set_ShouldResetCachedState(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Length).IsEqualTo(3); + + sut.Attributes = FileAttributes.Normal; + FileSystem.File.WriteAllText(path, "abcdefghij"); + + await That(sut.Length).IsEqualTo(10); + } + + [Test] + [AutoArguments] + public async Task CreationTime_Set_ShouldResetCachedState(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Length).IsEqualTo(3); + + sut.CreationTimeUtc = OtherTime; + FileSystem.File.WriteAllText(path, "abcdefghij"); + + await That(sut.Length).IsEqualTo(10); + } + + [Test] + [AutoArguments] + public async Task Exists_ShouldReturnCachedValueFromOtherProperty(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Length).IsEqualTo(3); + + FileSystem.File.Delete(path); + + await That(sut.Exists).IsTrue(); + } + + [Test] + [AutoArguments] + public async Task IsReadOnly_Set_ShouldResetCachedState(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.IsReadOnly).IsFalse(); + + sut.IsReadOnly = true; + + await That(sut.IsReadOnly).IsTrue(); + sut.IsReadOnly = false; + await That(sut.IsReadOnly).IsFalse(); + } + + [Test] + [AutoArguments] + public async Task LastAccessTime_Set_ShouldResetCachedState(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Length).IsEqualTo(3); + + sut.LastAccessTimeUtc = OtherTime; + FileSystem.File.WriteAllText(path, "abcdefghij"); + + await That(sut.Length).IsEqualTo(10); + } + + [Test] + [AutoArguments] + public async Task LastWriteTime_Set_ShouldResetCachedState(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.LastWriteTimeUtc).IsNotEqualTo(OtherTime); + + sut.LastWriteTimeUtc = OtherTime; + + await That(sut.LastWriteTimeUtc).IsEqualTo(OtherTime); + await That(sut.LastWriteTime).IsEqualTo(OtherTime.ToLocalTime()); + } + + [Test] + [AutoArguments] + public async Task Length_Missing_ShouldReturnCachedStateUntilRefresh(string path) + { + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsFalse(); + + FileSystem.File.WriteAllText(path, "abc"); + + void Act() + { + _ = sut.Length; + } + + await That(Act).Throws(); + sut.Refresh(); + await That(sut.Length).IsEqualTo(3); + } + + [Test] + [AutoArguments] + public async Task Length_ShouldReturnCachedValueFromExists(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsTrue(); + + FileSystem.File.WriteAllText(path, "abcdefghij"); + + await That(sut.Length).IsEqualTo(3); + } + + [Test] + [AutoArguments] + public async Task Length_ShouldReturnCachedValueUntilRefresh(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Length).IsEqualTo(3); + + FileSystem.File.WriteAllText(path, "abcdefghij"); + + await That(sut.Length).IsEqualTo(3); + sut.Refresh(); + await That(sut.Length).IsEqualTo(10); + } + + [Test] + [AutoArguments] + public async Task Refresh_ShouldCaptureStateImmediately(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + + sut.Refresh(); + FileSystem.File.WriteAllText(path, "abcdefghij"); + + await That(sut.Length).IsEqualTo(3); + FileSystem.File.Delete(path); + await That(sut.Exists).IsTrue(); + } + + [Test] + [AutoArguments] + public async Task Times_ShouldReturnCachedValuesUntilRefresh(string path) + { + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsTrue(); + + FileSystem.File.SetCreationTimeUtc(path, OtherTime); + FileSystem.File.SetLastAccessTimeUtc(path, OtherTime); + FileSystem.File.SetLastWriteTimeUtc(path, OtherTime); + + await That(sut.CreationTimeUtc).IsNotEqualTo(OtherTime); + await That(sut.CreationTime).IsNotEqualTo(OtherTime.ToLocalTime()); + await That(sut.LastAccessTimeUtc).IsNotEqualTo(OtherTime); + await That(sut.LastAccessTime).IsNotEqualTo(OtherTime.ToLocalTime()); + await That(sut.LastWriteTimeUtc).IsNotEqualTo(OtherTime); + await That(sut.LastWriteTime).IsNotEqualTo(OtherTime.ToLocalTime()); + sut.Refresh(); + await That(sut.LastAccessTimeUtc).IsEqualTo(OtherTime); + await That(sut.LastWriteTimeUtc).IsEqualTo(OtherTime); + } + +#if FEATURE_FILESYSTEM_UNIXFILEMODE + [Test] + [AutoArguments] + public async Task UnixFileMode_ShouldReturnCachedValueUntilRefresh(string path) + { + Skip.If(Test.RunsOnWindows); + + UnixFileMode unixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsTrue(); + + #pragma warning disable CA1416 + FileSystem.File.SetUnixFileMode(path, unixFileMode); + #pragma warning restore CA1416 + + await That(sut.UnixFileMode).IsNotEqualTo(unixFileMode); + sut.Refresh(); + await That(sut.UnixFileMode).IsEqualTo(unixFileMode); + } + + [Test] + [AutoArguments] + public async Task UnixFileMode_Set_ShouldResetCachedState(string path) + { + Skip.If(Test.RunsOnWindows); + + UnixFileMode unixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + FileSystem.File.WriteAllText(path, "abc"); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.UnixFileMode).IsNotEqualTo(unixFileMode); + + #pragma warning disable CA1416 + sut.UnixFileMode = unixFileMode; + #pragma warning restore CA1416 + + await That(sut.UnixFileMode).IsEqualTo(unixFileMode); + } +#endif +} diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemInfo/Tests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemInfo/Tests.cs index e19a9347e..a08618f59 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemInfo/Tests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemInfo/Tests.cs @@ -45,6 +45,27 @@ public async Task LinkTarget_ShouldBeSetByCreateAsSymbolicLink( await That(sut.LinkTarget).IsEqualTo(pathToTarget); } + + [Test] + [AutoArguments] + public async Task LinkTarget_ShouldReturnCachedValueUntilRefresh( + string path, string pathToTarget1, string pathToTarget2) + { + FileSystem.File.WriteAllText(pathToTarget1, null); + FileSystem.File.WriteAllText(pathToTarget2, null); + FileSystem.File.CreateSymbolicLink(path, pathToTarget1); + IFileInfo sut = FileSystem.FileInfo.New(path); + await That(sut.Exists).IsTrue(); + FileSystem.File.Delete(path); + FileSystem.File.CreateSymbolicLink(path, pathToTarget2); + + await That(sut.LinkTarget).IsEqualTo(pathToTarget2); + FileSystem.File.Delete(path); + FileSystem.File.CreateSymbolicLink(path, pathToTarget1); + await That(sut.LinkTarget).IsEqualTo(pathToTarget2); + sut.Refresh(); + await That(sut.LinkTarget).IsEqualTo(pathToTarget1); + } #endif [Test]