Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2739e85
fix: added correct handling of file share in file stream constructor/…
HarrisonTCodes Oct 18, 2025
ec701cf
fix: added stateful tracking of unshared file streams and prevented m…
HarrisonTCodes Oct 18, 2025
2a70ae1
refactor: changed fileshare none streams state to use concurrent dict…
HarrisonTCodes Oct 30, 2025
18716e4
refactor: used existing common exception for file-in-use error in fil…
HarrisonTCodes Oct 30, 2025
c660094
feat: added handling of failed addition of exclusive file stream to t…
HarrisonTCodes Oct 30, 2025
d648b5e
chore: explicit API acceptance test changes
HarrisonTCodes Oct 31, 2025
9952eab
test: added exclusive mock file stream handling unit tests
HarrisonTCodes Oct 31, 2025
4e724f5
feat: added path normalization to mock file stream
HarrisonTCodes Oct 31, 2025
c9e04a2
fix: improved path normalization in mock file stream for relative paths
HarrisonTCodes Oct 31, 2025
f241cc2
fix: added improved handling of file stream options in factory method
HarrisonTCodes Nov 9, 2025
eabc64e
refactor: de-duplicated normalize/fix path logic moving method to pat…
HarrisonTCodes Nov 9, 2025
2db45c1
chore: explicit API acceptance test changes to cover path verifier ch…
HarrisonTCodes Nov 9, 2025
611907b
feat: added more rigorous tracking of open file streams and shares/ac…
HarrisonTCodes Nov 13, 2025
cd2a151
feat: moved open file handles state to mock file system and ran API a…
HarrisonTCodes Nov 14, 2025
fd3062a
feat: added proper checking of access and share on file stream constr…
HarrisonTCodes Nov 14, 2025
543acdb
test: added unit tests to cover simultaneous file stream opening with…
HarrisonTCodes Nov 14, 2025
795891b
fix: used explicit GUID call instead of target-typed new for clarity …
HarrisonTCodes Nov 14, 2025
42a7402
Merge branch 'main' into fix/file-stream-sharing
HarrisonTCodes Nov 14, 2025
c689d74
chore: explicit API acceptance test changes for dotnet version 10
HarrisonTCodes Nov 14, 2025
8b23795
refactor: made path verifier fix path method internal
HarrisonTCodes Nov 16, 2025
a61e568
feat: added file handles class and updated mock file system/stream to…
HarrisonTCodes Nov 17, 2025
15ed8d6
refactor: renamed add handle method on file handles class
HarrisonTCodes Nov 20, 2025
9a2d277
Merge branch 'main' into fix/file-stream-sharing
vbreuss Nov 21, 2025
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
@@ -0,0 +1,56 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.IO.Abstractions.TestingHelpers;

public class FileHandles
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<Guid, (FileAccess access, FileShare share)>> handles = new();

public void TryAddHandle(string path, Guid guid, FileAccess access, FileShare share)
{
var pathHandles = handles.GetOrAdd(
path,
_ => new ConcurrentDictionary<Guid, (FileAccess, FileShare)>());

var requiredShare = AccessToShare(access);
foreach (var (existingAccess, existingShare) in pathHandles.Values)
{
var existingRequiredShare = AccessToShare(existingAccess);
var existingBlocksNew = (existingShare & requiredShare) != requiredShare;
var newBlocksExisting = (share & existingRequiredShare) != existingRequiredShare;
if (existingBlocksNew || newBlocksExisting)
{
throw CommonExceptions.ProcessCannotAccessFileInUse(path);
}
}

pathHandles[guid] = (access, share);
}

public void RemoveHandle(string path, Guid guid)
{
if (handles.TryGetValue(path, out var pathHandles))
{
pathHandles.TryRemove(guid, out _);
if (pathHandles.IsEmpty)
{
handles.TryRemove(path, out _);
}
}
}

private static FileShare AccessToShare(FileAccess access)
{
var share = FileShare.None;
if (access.HasFlag(FileAccess.Read))
{
share |= FileShare.Read;
}
if (access.HasFlag(FileAccess.Write))
{
share |= FileShare.Write;
}
return share;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;

namespace System.IO.Abstractions.TestingHelpers;
Expand Down Expand Up @@ -115,5 +114,5 @@ public interface IMockFileDataAccessor : IFileSystem
/// <summary>
/// Gets a reference to the open file handles.
/// </summary>
ConcurrentDictionary<string, ConcurrentDictionary<Guid, (FileAccess, FileShare)>> FileHandles { get; }
FileHandles FileHandles { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Threading;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Collections.Concurrent;

namespace System.IO.Abstractions.TestingHelpers;

Expand Down Expand Up @@ -102,41 +101,11 @@ public MockFileStream(
mockFileDataAccessor.AddFile(path, fileData);
}

var fileHandlesEntry = mockFileDataAccessor.FileHandles.GetOrAdd(
path,
_ => new ConcurrentDictionary<Guid, (FileAccess access, FileShare share)>());

var requiredShare = AccessToShare(access);
foreach (var (existingAccess, existingShare) in fileHandlesEntry.Values)
{
var existingRequiredShare = AccessToShare(existingAccess);
var existingBlocksNew = (existingShare & requiredShare) != requiredShare;
var newBlocksExisting = (share & existingRequiredShare) != existingRequiredShare;
if (existingBlocksNew || newBlocksExisting)
{
throw CommonExceptions.ProcessCannotAccessFileInUse(path);
}
}

fileHandlesEntry[guid] = (access, share);
mockFileDataAccessor.FileHandles.TryAddHandle(path, guid, access, share);
this.access = access;
this.share = share;
}

private static FileShare AccessToShare(FileAccess access)
{
var share = FileShare.None;
if (access.HasFlag(FileAccess.Read))
{
share |= FileShare.Read;
}
if (access.HasFlag(FileAccess.Write))
{
share |= FileShare.Write;
}
return share;
}

private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access)
{
if (mode == FileMode.Append)
Expand Down Expand Up @@ -181,14 +150,7 @@ protected override void Dispose(bool disposing)
{
return;
}
if (mockFileDataAccessor.FileHandles.TryGetValue(path, out var fileHandlesEntry))
{
fileHandlesEntry.TryRemove(guid, out _);
if (fileHandlesEntry.IsEmpty)
{
mockFileDataAccessor.FileHandles.TryRemove(path, out _);
}
}
mockFileDataAccessor.FileHandles.RemoveHandle(path, guid);
InternalFlush();
base.Dispose(disposing);
OnClose();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand All @@ -23,7 +22,7 @@ public class MockFileSystem : FileSystemBase, IMockFileDataAccessor
#if FEATURE_SERIALIZABLE
[NonSerialized]
#endif
private readonly ConcurrentDictionary<string, ConcurrentDictionary<Guid, (FileAccess access, FileShare share)>> fileHandles = new();
private readonly FileHandles fileHandles = new();
#if FEATURE_SERIALIZABLE
[NonSerialized]
#endif
Expand Down Expand Up @@ -120,8 +119,7 @@ public MockFileSystem(IDictionary<string, MockFileData> files, MockFileSystemOpt
/// <inheritdoc />
public PathVerifier PathVerifier => pathVerifier;
/// <inheritdoc />
public ConcurrentDictionary<string, ConcurrentDictionary<Guid, (FileAccess, FileShare)>> FileHandles
=> fileHandles;
public FileHandles FileHandles => fileHandles;

/// <summary>
/// Replaces the time provider with a mocked instance. This allows to influence the used time in tests.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/TestableIO/System.IO.Abstractions.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")]
public class FileHandles
{
public FileHandles() { }
public void RemoveHandle(string path, System.Guid guid) { }
public void TryAddHandle(string path, System.Guid guid, System.IO.FileAccess access, System.IO.FileShare share) { }
}
namespace System.IO.Abstractions.TestingHelpers
{
public interface IMockFileDataAccessor : System.IO.Abstractions.IFileSystem
Expand All @@ -8,7 +14,7 @@ namespace System.IO.Abstractions.TestingHelpers
System.Collections.Generic.IEnumerable<string> AllDrives { get; }
System.Collections.Generic.IEnumerable<string> AllFiles { get; }
System.Collections.Generic.IEnumerable<string> AllPaths { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
FileHandles FileHandles { get; }
System.IO.Abstractions.IFileSystem FileSystem { get; }
System.IO.Abstractions.TestingHelpers.PathVerifier PathVerifier { get; }
System.IO.Abstractions.TestingHelpers.StringOperations StringOperations { get; }
Expand Down Expand Up @@ -441,7 +447,7 @@ namespace System.IO.Abstractions.TestingHelpers
public override System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public override System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
public override System.IO.Abstractions.IFile File { get; }
public System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
public FileHandles FileHandles { get; }
public override System.IO.Abstractions.IFileInfoFactory FileInfo { get; }
public override System.IO.Abstractions.IFileStreamFactory FileStream { get; }
public System.IO.Abstractions.IFileSystem FileSystem { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/TestableIO/System.IO.Abstractions.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName=".NET Framework 4.7.2")]
public class FileHandles
{
public FileHandles() { }
public void RemoveHandle(string path, System.Guid guid) { }
public void TryAddHandle(string path, System.Guid guid, System.IO.FileAccess access, System.IO.FileShare share) { }
}
namespace System.IO.Abstractions.TestingHelpers
{
public interface IMockFileDataAccessor : System.IO.Abstractions.IFileSystem
Expand All @@ -8,7 +14,7 @@ namespace System.IO.Abstractions.TestingHelpers
System.Collections.Generic.IEnumerable<string> AllDrives { get; }
System.Collections.Generic.IEnumerable<string> AllFiles { get; }
System.Collections.Generic.IEnumerable<string> AllPaths { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
FileHandles FileHandles { get; }
System.IO.Abstractions.IFileSystem FileSystem { get; }
System.IO.Abstractions.TestingHelpers.PathVerifier PathVerifier { get; }
System.IO.Abstractions.TestingHelpers.StringOperations StringOperations { get; }
Expand Down Expand Up @@ -348,7 +354,7 @@ namespace System.IO.Abstractions.TestingHelpers
public override System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public override System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
public override System.IO.Abstractions.IFile File { get; }
public System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
public FileHandles FileHandles { get; }
public override System.IO.Abstractions.IFileInfoFactory FileInfo { get; }
public override System.IO.Abstractions.IFileStreamFactory FileStream { get; }
public System.IO.Abstractions.IFileSystem FileSystem { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/TestableIO/System.IO.Abstractions.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")]
public class FileHandles
{
public FileHandles() { }
public void RemoveHandle(string path, System.Guid guid) { }
public void TryAddHandle(string path, System.Guid guid, System.IO.FileAccess access, System.IO.FileShare share) { }
}
namespace System.IO.Abstractions.TestingHelpers
{
public interface IMockFileDataAccessor : System.IO.Abstractions.IFileSystem
Expand All @@ -8,7 +14,7 @@ namespace System.IO.Abstractions.TestingHelpers
System.Collections.Generic.IEnumerable<string> AllDrives { get; }
System.Collections.Generic.IEnumerable<string> AllFiles { get; }
System.Collections.Generic.IEnumerable<string> AllPaths { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
FileHandles FileHandles { get; }
System.IO.Abstractions.IFileSystem FileSystem { get; }
System.IO.Abstractions.TestingHelpers.PathVerifier PathVerifier { get; }
System.IO.Abstractions.TestingHelpers.StringOperations StringOperations { get; }
Expand Down Expand Up @@ -403,7 +409,7 @@ namespace System.IO.Abstractions.TestingHelpers
public override System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public override System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
public override System.IO.Abstractions.IFile File { get; }
public System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
public FileHandles FileHandles { get; }
public override System.IO.Abstractions.IFileInfoFactory FileInfo { get; }
public override System.IO.Abstractions.IFileStreamFactory FileStream { get; }
public System.IO.Abstractions.IFileSystem FileSystem { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/TestableIO/System.IO.Abstractions.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
public class FileHandles
{
public FileHandles() { }
public void RemoveHandle(string path, System.Guid guid) { }
public void TryAddHandle(string path, System.Guid guid, System.IO.FileAccess access, System.IO.FileShare share) { }
}
namespace System.IO.Abstractions.TestingHelpers
{
public interface IMockFileDataAccessor : System.IO.Abstractions.IFileSystem
Expand All @@ -8,7 +14,7 @@ namespace System.IO.Abstractions.TestingHelpers
System.Collections.Generic.IEnumerable<string> AllDrives { get; }
System.Collections.Generic.IEnumerable<string> AllFiles { get; }
System.Collections.Generic.IEnumerable<string> AllPaths { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
FileHandles FileHandles { get; }
System.IO.Abstractions.IFileSystem FileSystem { get; }
System.IO.Abstractions.TestingHelpers.PathVerifier PathVerifier { get; }
System.IO.Abstractions.TestingHelpers.StringOperations StringOperations { get; }
Expand Down Expand Up @@ -427,7 +433,7 @@ namespace System.IO.Abstractions.TestingHelpers
public override System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public override System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
public override System.IO.Abstractions.IFile File { get; }
public System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
public FileHandles FileHandles { get; }
public override System.IO.Abstractions.IFileInfoFactory FileInfo { get; }
public override System.IO.Abstractions.IFileStreamFactory FileStream { get; }
public System.IO.Abstractions.IFileSystem FileSystem { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/TestableIO/System.IO.Abstractions.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v9.0", FrameworkDisplayName=".NET 9.0")]
public class FileHandles
{
public FileHandles() { }
public void RemoveHandle(string path, System.Guid guid) { }
public void TryAddHandle(string path, System.Guid guid, System.IO.FileAccess access, System.IO.FileShare share) { }
}
namespace System.IO.Abstractions.TestingHelpers
{
public interface IMockFileDataAccessor : System.IO.Abstractions.IFileSystem
Expand All @@ -8,7 +14,7 @@ namespace System.IO.Abstractions.TestingHelpers
System.Collections.Generic.IEnumerable<string> AllDrives { get; }
System.Collections.Generic.IEnumerable<string> AllFiles { get; }
System.Collections.Generic.IEnumerable<string> AllPaths { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
FileHandles FileHandles { get; }
System.IO.Abstractions.IFileSystem FileSystem { get; }
System.IO.Abstractions.TestingHelpers.PathVerifier PathVerifier { get; }
System.IO.Abstractions.TestingHelpers.StringOperations StringOperations { get; }
Expand Down Expand Up @@ -441,7 +447,7 @@ namespace System.IO.Abstractions.TestingHelpers
public override System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public override System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
public override System.IO.Abstractions.IFile File { get; }
public System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
public FileHandles FileHandles { get; }
public override System.IO.Abstractions.IFileInfoFactory FileInfo { get; }
public override System.IO.Abstractions.IFileStreamFactory FileStream { get; }
public System.IO.Abstractions.IFileSystem FileSystem { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/TestableIO/System.IO.Abstractions.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
public class FileHandles
{
public FileHandles() { }
public void RemoveHandle(string path, System.Guid guid) { }
public void TryAddHandle(string path, System.Guid guid, System.IO.FileAccess access, System.IO.FileShare share) { }
}
namespace System.IO.Abstractions.TestingHelpers
{
public interface IMockFileDataAccessor : System.IO.Abstractions.IFileSystem
Expand All @@ -8,7 +14,7 @@ namespace System.IO.Abstractions.TestingHelpers
System.Collections.Generic.IEnumerable<string> AllDrives { get; }
System.Collections.Generic.IEnumerable<string> AllFiles { get; }
System.Collections.Generic.IEnumerable<string> AllPaths { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
FileHandles FileHandles { get; }
System.IO.Abstractions.IFileSystem FileSystem { get; }
System.IO.Abstractions.TestingHelpers.PathVerifier PathVerifier { get; }
System.IO.Abstractions.TestingHelpers.StringOperations StringOperations { get; }
Expand Down Expand Up @@ -348,7 +354,7 @@ namespace System.IO.Abstractions.TestingHelpers
public override System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public override System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
public override System.IO.Abstractions.IFile File { get; }
public System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.ValueTuple<System.IO.FileAccess, System.IO.FileShare>>> FileHandles { get; }
public FileHandles FileHandles { get; }
public override System.IO.Abstractions.IFileInfoFactory FileInfo { get; }
public override System.IO.Abstractions.IFileStreamFactory FileStream { get; }
public System.IO.Abstractions.IFileSystem FileSystem { get; }
Expand Down
Loading