diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs index 0f9a97a48..e4d739704 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFile.cs @@ -677,7 +677,7 @@ private FileSystemStream OpenInternal( } mockFileDataAccessor.AdjustTimes(mockFileData, timeAdjustments); - return new MockFileStream(mockFileDataAccessor, path, mode, access, options); + return new MockFileStream(mockFileDataAccessor, path, mode, access, FileShare.Read, options); } /// diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs index 1be2241af..05ab473f5 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Runtime.Versioning; using System.Security.AccessControl; +using System.Collections.Concurrent; namespace System.IO.Abstractions.TestingHelpers; @@ -32,9 +33,11 @@ public NullFileSystemStream() : base(Null, ".", true) private readonly IMockFileDataAccessor mockFileDataAccessor; private readonly string path; private readonly FileAccess access = FileAccess.ReadWrite; + private readonly FileShare share = FileShare.Read; private readonly FileOptions options; private readonly MockFileData fileData; private bool disposed; + private static ConcurrentDictionary _fileShareNoneStreams = []; /// public MockFileStream( @@ -42,6 +45,7 @@ public MockFileStream( string path, FileMode mode, FileAccess access = FileAccess.ReadWrite, + FileShare share = FileShare.Read, FileOptions options = FileOptions.None) : base(new MemoryStream(), path == null ? null : Path.GetFullPath(path), @@ -51,9 +55,15 @@ public MockFileStream( ThrowIfInvalidModeAccess(mode, access); this.mockFileDataAccessor = mockFileDataAccessor ?? throw new ArgumentNullException(nameof(mockFileDataAccessor)); + path = mockFileDataAccessor.PathVerifier.FixPath(path); this.path = path; this.options = options; + if (_fileShareNoneStreams.ContainsKey(path)) + { + throw CommonExceptions.ProcessCannotAccessFileInUse(path); + } + if (mockFileDataAccessor.FileExists(path)) { if (mode.Equals(FileMode.CreateNew)) @@ -97,7 +107,15 @@ public MockFileStream( mockFileDataAccessor.AddFile(path, fileData); } + if (share is FileShare.None) + { + if (!_fileShareNoneStreams.TryAdd(path, 0)) + { + throw CommonExceptions.ProcessCannotAccessFileInUse(path); + } + } this.access = access; + this.share = share; } private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access) @@ -144,6 +162,10 @@ protected override void Dispose(bool disposing) { return; } + if (share is FileShare.None) + { + _fileShareNoneStreams.TryRemove(path, out _); + } InternalFlush(); base.Dispose(disposing); OnClose(); diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStreamFactory.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStreamFactory.cs index 9cd34f7ae..1dc39759c 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStreamFactory.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStreamFactory.cs @@ -41,25 +41,25 @@ public FileSystemStream New(string path, FileMode mode, FileAccess access) /// public FileSystemStream New(string path, FileMode mode, FileAccess access, FileShare share) - => new MockFileStream(mockFileSystem, path, mode, access); + => new MockFileStream(mockFileSystem, path, mode, access, share); /// public FileSystemStream New(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize) - => new MockFileStream(mockFileSystem, path, mode, access); + => new MockFileStream(mockFileSystem, path, mode, access, share); /// public FileSystemStream New(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, bool useAsync) - => new MockFileStream(mockFileSystem, path, mode, access); + => new MockFileStream(mockFileSystem, path, mode, access, share); /// public FileSystemStream New(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new MockFileStream(mockFileSystem, path, mode, access, options); + => new MockFileStream(mockFileSystem, path, mode, access, share, options); #if FEATURE_FILESTREAM_OPTIONS /// public FileSystemStream New(string path, FileStreamOptions options) - => new MockFileStream(mockFileSystem, path, options.Mode, options.Access, options.Options); + => new MockFileStream(mockFileSystem, path, options.Mode, options.Access, options.Share, options.Options); #endif /// diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs index 3f71206cb..34b809c21 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs @@ -128,19 +128,6 @@ public MockFileSystem MockTime(Func dateTimeProvider) return this; } - private string FixPath(string path, bool checkCaps = false) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path), StringResources.Manager.GetString("VALUE_CANNOT_BE_NULL")); - } - - var pathSeparatorFixed = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - var fullPath = Path.GetFullPath(pathSeparatorFixed); - - return checkCaps ? GetPathWithCorrectDirectoryCapitalization(fullPath) : fullPath; - } - //If C:\foo exists, ensures that trying to save a file to "C:\FOO\file.txt" instead saves it to "C:\foo\file.txt". private string GetPathWithCorrectDirectoryCapitalization(string fullPath) { @@ -194,7 +181,7 @@ public MockFileData AdjustTimes(MockFileData fileData, TimeAdjustments timeAdjus /// public MockFileData GetFile(string path) { - path = FixPath(path).TrimSlashes(); + path = pathVerifier.FixPath(path).TrimSlashes(); return GetFileWithoutFixingPath(path); } @@ -210,7 +197,9 @@ public MockDriveData GetDrive(string name) private void SetEntry(string path, MockFileData mockFile) { - path = FixPath(path, true).TrimSlashes(); + path = GetPathWithCorrectDirectoryCapitalization( + pathVerifier.FixPath(path) + ).TrimSlashes(); lock (files) { @@ -232,7 +221,9 @@ private void SetEntry(string path, MockFileData mockFile) /// public void AddFile(string path, MockFileData mockFile, bool verifyAccess = true) { - var fixedPath = FixPath(path, true); + var fixedPath = GetPathWithCorrectDirectoryCapitalization( + pathVerifier.FixPath(path) + ); mockFile ??= new MockFileData(string.Empty); var file = GetFile(fixedPath); @@ -319,7 +310,9 @@ public MockFileData GetFile(IFileInfo path) /// public void AddDirectory(string path) { - var fixedPath = FixPath(path, true); + var fixedPath = GetPathWithCorrectDirectoryCapitalization( + pathVerifier.FixPath(path) + ); var separator = Path.DirectorySeparatorChar.ToString(); if (FileExists(fixedPath) && FileIsReadOnly(fixedPath)) @@ -408,8 +401,8 @@ public void AddDrive(string name, MockDriveData mockDrive) /// public void MoveDirectory(string sourcePath, string destPath) { - sourcePath = FixPath(sourcePath); - destPath = FixPath(destPath); + sourcePath = pathVerifier.FixPath(sourcePath); + destPath = pathVerifier.FixPath(destPath); var sourcePathSequence = sourcePath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); @@ -452,7 +445,7 @@ bool PathStartsWith(string path, string[] minMatch) /// public void RemoveFile(string path, bool verifyAccess = true) { - path = FixPath(path); + path = pathVerifier.FixPath(path); lock (files) { @@ -473,7 +466,7 @@ public bool FileExists(string path) return false; } - path = FixPath(path).TrimSlashes(); + path = pathVerifier.FixPath(path).TrimSlashes(); lock (files) { diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs index 2c426637c..93405eebb 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs @@ -183,4 +183,23 @@ public bool TryNormalizeDriveName(string name, out string result) result = name; return true; } + + /// + /// Resolves and normalizes a path. + /// + public string FixPath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path), StringResources.Manager.GetString("VALUE_CANNOT_BE_NULL")); + } + + var pathSeparatorFixed = path.Replace( + _mockFileDataAccessor.Path.AltDirectorySeparatorChar, + _mockFileDataAccessor.Path.DirectorySeparatorChar + ); + var fullPath = _mockFileDataAccessor.Path.GetFullPath(pathSeparatorFixed); + + return fullPath; + } } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt index 788ddb703..094f901ae 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt @@ -297,7 +297,7 @@ namespace System.IO.Abstractions.TestingHelpers [System.Serializable] public class MockFileStream : System.IO.Abstractions.FileSystemStream, System.IO.Abstractions.IFileSystemAclSupport { - public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } + public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileShare share = 1, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } @@ -468,6 +468,7 @@ namespace System.IO.Abstractions.TestingHelpers { public PathVerifier(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor) { } public void CheckInvalidPathChars(string path, bool checkAdditional = false) { } + public string FixPath(string path) { } public bool HasIllegalCharacters(string path, bool checkAdditional) { } public void IsLegalAbsoluteOrRelative(string path, string paramName) { } public string NormalizeDriveName(string name) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt index afd2ce3c4..d8dcd5d80 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt @@ -346,7 +346,7 @@ namespace System.IO.Abstractions.TestingHelpers [System.Serializable] public class MockFileStream : System.IO.Abstractions.FileSystemStream, System.IO.Abstractions.IFileSystemAclSupport { - public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } + public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileShare share = 1, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } @@ -524,6 +524,7 @@ namespace System.IO.Abstractions.TestingHelpers { public PathVerifier(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor) { } public void CheckInvalidPathChars(string path, bool checkAdditional = false) { } + public string FixPath(string path) { } public bool HasIllegalCharacters(string path, bool checkAdditional) { } public void IsLegalAbsoluteOrRelative(string path, string paramName) { } public string NormalizeDriveName(string name) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt index 1c581cab8..d5d7baa8e 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt @@ -370,7 +370,7 @@ namespace System.IO.Abstractions.TestingHelpers [System.Serializable] public class MockFileStream : System.IO.Abstractions.FileSystemStream, System.IO.Abstractions.IFileSystemAclSupport { - public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } + public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileShare share = 1, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } @@ -549,6 +549,7 @@ namespace System.IO.Abstractions.TestingHelpers { public PathVerifier(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor) { } public void CheckInvalidPathChars(string path, bool checkAdditional = false) { } + public string FixPath(string path) { } public bool HasIllegalCharacters(string path, bool checkAdditional) { } public void IsLegalAbsoluteOrRelative(string path, string paramName) { } public string NormalizeDriveName(string name) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt index 6c78cf677..b59eb5dbc 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt @@ -384,7 +384,7 @@ namespace System.IO.Abstractions.TestingHelpers [System.Serializable] public class MockFileStream : System.IO.Abstractions.FileSystemStream, System.IO.Abstractions.IFileSystemAclSupport { - public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } + public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileShare share = 1, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } @@ -563,6 +563,7 @@ namespace System.IO.Abstractions.TestingHelpers { public PathVerifier(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor) { } public void CheckInvalidPathChars(string path, bool checkAdditional = false) { } + public string FixPath(string path) { } public bool HasIllegalCharacters(string path, bool checkAdditional) { } public void IsLegalAbsoluteOrRelative(string path, string paramName) { } public string NormalizeDriveName(string name) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt index e0880b9f8..39357be50 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt @@ -297,7 +297,7 @@ namespace System.IO.Abstractions.TestingHelpers [System.Serializable] public class MockFileStream : System.IO.Abstractions.FileSystemStream, System.IO.Abstractions.IFileSystemAclSupport { - public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } + public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileShare share = 1, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } @@ -468,6 +468,7 @@ namespace System.IO.Abstractions.TestingHelpers { public PathVerifier(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor) { } public void CheckInvalidPathChars(string path, bool checkAdditional = false) { } + public string FixPath(string path) { } public bool HasIllegalCharacters(string path, bool checkAdditional) { } public void IsLegalAbsoluteOrRelative(string path, string paramName) { } public string NormalizeDriveName(string name) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt index 80f876c0c..df8c9c634 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt @@ -323,7 +323,7 @@ namespace System.IO.Abstractions.TestingHelpers [System.Serializable] public class MockFileStream : System.IO.Abstractions.FileSystemStream, System.IO.Abstractions.IFileSystemAclSupport { - public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } + public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileShare share = 1, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } @@ -497,6 +497,7 @@ namespace System.IO.Abstractions.TestingHelpers { public PathVerifier(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor) { } public void CheckInvalidPathChars(string path, bool checkAdditional = false) { } + public string FixPath(string path) { } public bool HasIllegalCharacters(string path, bool checkAdditional) { } public void IsLegalAbsoluteOrRelative(string path, string paramName) { } public string NormalizeDriveName(string name) { } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index b82566d9a..f973c1732 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -390,4 +390,26 @@ async Task Act() => await source.CopyToAsync(destination); await That(Act).Throws(); } + + [Test] + public async Task MockFileStream_WhenExclusiveStreamOpen_ShouldThrowIOException() + { + var fileSystem = new MockFileSystem(); + fileSystem.File.WriteAllText("foo.txt", ""); + using (new MockFileStream(fileSystem, "foo.txt", FileMode.Open, FileAccess.Read, FileShare.None)) + { + await That(() => new MockFileStream(fileSystem, "foo.txt", FileMode.Open, FileAccess.Read)).Throws(); + } + } + + [Test] + public async Task MockFileStream_WhenExclusiveStreamClosed_ShouldNotThrow() + { + var fileSystem = new MockFileSystem(); + fileSystem.File.WriteAllText("foo.txt", ""); + var stream = new MockFileStream(fileSystem, "foo.txt", FileMode.Open, FileAccess.Read, FileShare.None); + stream.Dispose(); + + await That(() => new MockFileStream(fileSystem, "foo.txt", FileMode.Open, FileAccess.Read)).DoesNotThrow(); + } } \ No newline at end of file