Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Collections.Concurrent;

namespace System.IO.Abstractions.TestingHelpers;

Expand Down Expand Up @@ -32,16 +33,19 @@ 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<string, byte> _fileShareNoneStreams = [];

/// <inheritdoc />
public MockFileStream(
IMockFileDataAccessor mockFileDataAccessor,
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),
Expand All @@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to add some normalization before using the path as the dictionary key. Relative paths passed to the ctor could resolve to the same file system object. The mockFileDataAccessor does like 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;
}

Copy link
Contributor Author

@HarrisonTCodes HarrisonTCodes Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comment and comprehensive snippets! This is a great point.

I added a private NormalizePath method to the MockFileSystem class largely based on the above, which should handle relative paths passed to the ctor pointing to the same object.

I recognise there is a slight duplication of logic here, and maybe a more rigorous refactor with a general-use, shared FixPath method somewhere (or potentially just making FixPath public?), instead of having these two private methods, would be preferred - this could be worth some discussion.

Please let me know your thoughts, and if you think the normalization implemented is sufficient!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to de-duplicate this logic into an internal method that can be used whenever we need to normalize a path to ensure it is done the same everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to de-duplicate this logic into an internal method that can be used whenever we need to normalize a path to ensure it is done the same everywhere.

Agreed. I have been digging around the codebase, and it seems to me a good approach to this would be to move the FixPath method (and associated other methods) logic from the MockFileSystem class into the PathVerifier class, updating both MockFileSystem and MockFileStream accordingly?

As usual, its possible I'm missing something here or overlooking a more apt approach that those with more experience in this codebase may be able to see, so please do let me know if you think there's a better approach!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be covered in eabc64e (explicit API test runs in following commit), let me know if there are any other desired changes here!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now works only for FileShare.None, but not for read and write access:

Access Share Result
Read Read ok
Read ReadWrite ok
Read Write throw
Read None throw
Write Read throw
Write ReadWrite ok
Write Write ok
Write None throw
ReadWrite Read throw
ReadWrite ReadWrite ok
ReadWrite Write throw
ReadWrite None throw

I think it would be a great idea to also handle these cases correctly. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I briefly mentioned an approach to this in a previous comment, where instead of just using the ConcurrentDictionary as effectively a concurrent hashmap, we actually use the value of entries to store the share they were opened with, and check against it where we currently just check for existence.

This is the approach I am planning to go for and start working on, but let me know if you think there's a better idea I'm overlooking or something I'm missing!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fear the solution is not so easy, as you can have multiple streams with e.g. read access and read share open at the same time and only when all streams got disposed should you be able to open the file with write access. So you would need to create a disposable access reference for each stream and remove this instance upon disposal of the stream from some kind of storage.

For reference: in Testably.Abstractions I used a ConcurrentDictionary<IStorageLocation, ConcurrentDictionary<Guid, FileHandle>> with a custom FileHandle class (see here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, multiple streams of the same file open at once...
Thanks for the reference, I'll checkout how its handled in Testably.Abstractions, maybe some of it could be directly ported over?

{
throw CommonExceptions.ProcessCannotAccessFileInUse(path);
}

if (mockFileDataAccessor.FileExists(path))
{
if (mode.Equals(FileMode.CreateNew))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,25 @@ public FileSystemStream New(string path, FileMode mode, FileAccess access)

/// <inheritdoc />
public FileSystemStream New(string path, FileMode mode, FileAccess access, FileShare share)
=> new MockFileStream(mockFileSystem, path, mode, access);
=> new MockFileStream(mockFileSystem, path, mode, access, share);

/// <inheritdoc />
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);

/// <inheritdoc />
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);

/// <inheritdoc />
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
/// <inheritdoc />
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

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,6 @@ public MockFileSystem MockTime(Func<DateTime> 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)
{
Expand Down Expand Up @@ -194,7 +181,7 @@ public MockFileData AdjustTimes(MockFileData fileData, TimeAdjustments timeAdjus
/// <inheritdoc />
public MockFileData GetFile(string path)
{
path = FixPath(path).TrimSlashes();
path = pathVerifier.FixPath(path).TrimSlashes();
return GetFileWithoutFixingPath(path);
}

Expand All @@ -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)
{
Expand All @@ -232,7 +221,9 @@ private void SetEntry(string path, MockFileData mockFile)
/// <inheritdoc />
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);
Expand Down Expand Up @@ -319,7 +310,9 @@ public MockFileData GetFile(IFileInfo path)
/// <inheritdoc />
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))
Expand Down Expand Up @@ -408,8 +401,8 @@ public void AddDrive(string name, MockDriveData mockDrive)
/// <inheritdoc />
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);

Expand Down Expand Up @@ -452,7 +445,7 @@ bool PathStartsWith(string path, string[] minMatch)
/// <inheritdoc />
public void RemoveFile(string path, bool verifyAccess = true)
{
path = FixPath(path);
path = pathVerifier.FixPath(path);

lock (files)
{
Expand All @@ -473,7 +466,7 @@ public bool FileExists(string path)
return false;
}

path = FixPath(path).TrimSlashes();
path = pathVerifier.FixPath(path).TrimSlashes();

lock (files)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,23 @@ public bool TryNormalizeDriveName(string name, out string result)
result = name;
return true;
}

/// <summary>
/// Resolves and normalizes a path.
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,4 +390,26 @@ async Task Act() =>
await source.CopyToAsync(destination);
await That(Act).Throws<NotSupportedException>();
}

[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<IOException>();
}
}

Comment on lines +402 to +404
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could combine these two tests into one and verify that after you dispose of the stream it no longer 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();
}
}