Skip to content

Commit 31315ac

Browse files
authored
refactor: store file handles centrally in InMemoryStorage (#603)
Store FileHandles centrally in `InMemoryStorage` instead of creating a separate `ConcurrentDictionary` for each file.
1 parent 0c2b777 commit 31315ac

File tree

7 files changed

+250
-149
lines changed

7 files changed

+250
-149
lines changed

Source/Testably.Abstractions.Testing/Helpers/FileSystemExtensions.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using Testably.Abstractions.RandomSystem;
3-
using Testably.Abstractions.Testing.Statistics;
43
using Testably.Abstractions.Testing.Storage;
54

65
namespace Testably.Abstractions.Testing.Helpers;
@@ -123,15 +122,6 @@ internal static string GetSubdirectoryPath(this MockFileSystem fileSystem,
123122
return fullFilePath;
124123
}
125124

126-
/// <summary>
127-
/// Ignores all registrations on the <paramref name="statisticsGate" /> until the return value is disposed.
128-
/// </summary>
129-
internal static IDisposable Ignore(this IStatisticsGate statisticsGate)
130-
{
131-
statisticsGate.TryGetLock(out IDisposable? release);
132-
return release;
133-
}
134-
135125
/// <summary>
136126
/// Ignores all registrations on the <see cref="MockFileSystem.Statistics" /> until the return value is disposed.
137127
/// </summary>

Source/Testably.Abstractions.Testing/Statistics/FileSystemStatistics.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Testably.Abstractions.Testing.Statistics;
77
internal sealed class FileSystemStatistics : IFileSystemStatistics, IStatisticsGate
88
{
99
private static readonly AsyncLocal<bool> IsDisabled = new();
10+
private static readonly AsyncLocal<bool> IsInit = new();
1011

1112
/// <summary>
1213
/// The total count of registered statistic calls.
@@ -98,6 +99,28 @@ public bool TryGetLock(out IDisposable release)
9899

99100
#endregion
100101

102+
/// <summary>
103+
/// Ignores all registrations until the return value is disposed.
104+
/// </summary>
105+
internal IDisposable Ignore()
106+
{
107+
if (IsDisabled.Value)
108+
{
109+
return TemporaryDisable.None;
110+
}
111+
112+
IsDisabled.Value = true;
113+
IsInit.Value = true;
114+
return new TemporaryDisable(() =>
115+
{
116+
IsDisabled.Value = false;
117+
IsInit.Value = false;
118+
});
119+
}
120+
121+
internal bool IsInitializing()
122+
=> IsInit.Value;
123+
101124
private sealed class TemporaryDisable : IDisposable
102125
{
103126
public static IDisposable None { get; } = new NoOpDisposable();

Source/Testably.Abstractions.Testing/Statistics/PathStatistics.cs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,6 @@ public IStatistics<TType> this[string path]
4242

4343
#endregion
4444

45-
/// <summary>
46-
/// Registers the <paramref name="name" /> callback with <paramref name="parameters" /> under <paramref name="path" />.
47-
/// </summary>
48-
/// <returns>A disposable which ignores all registrations, until it is disposed.</returns>
49-
internal IDisposable RegisterPathMethod(string path, string name,
50-
ParameterDescription[] parameters)
51-
{
52-
if (_statisticsGate.TryGetLock(out IDisposable release))
53-
{
54-
string key = CreateKey(_fileSystem.Storage.CurrentDirectory, path);
55-
CallStatistics<TType> callStatistics =
56-
_statistics.GetOrAdd(key,
57-
k => new CallStatistics<TType>(_statisticsGate, $"{ToString()}[{k}]"));
58-
return callStatistics.RegisterMethodWithoutLock(release, name, parameters);
59-
}
60-
61-
return release;
62-
}
63-
6445
/// <summary>
6546
/// Registers the <paramref name="name" /> callback without parameters under <paramref name="path" />.
6647
/// </summary>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace Testably.Abstractions.Testing.Storage;
5+
6+
internal sealed class FileHandle : IStorageAccessHandle
7+
{
8+
public static IStorageAccessHandle Ignore { get; } = new IgnoreFileHandle();
9+
private readonly MockFileSystem _fileSystem;
10+
private readonly Guid _key;
11+
private readonly Action<Guid> _releaseCallback;
12+
13+
public FileHandle(MockFileSystem fileSystem, Guid key, Action<Guid> releaseCallback,
14+
FileAccess access,
15+
FileShare share, bool deleteAccess)
16+
{
17+
_fileSystem = fileSystem;
18+
_releaseCallback = releaseCallback;
19+
Access = access;
20+
DeleteAccess = deleteAccess;
21+
if (_fileSystem.Execute.IsWindows)
22+
{
23+
Share = share;
24+
}
25+
else
26+
{
27+
Share = share == FileShare.None
28+
? FileShare.None
29+
: FileShare.ReadWrite;
30+
}
31+
32+
_key = key;
33+
}
34+
35+
#region IStorageAccessHandle Members
36+
37+
/// <inheritdoc cref="IStorageAccessHandle.Access" />
38+
public FileAccess Access { get; }
39+
40+
/// <inheritdoc cref="IStorageAccessHandle.DeleteAccess" />
41+
public bool DeleteAccess { get; }
42+
43+
/// <inheritdoc cref="IStorageAccessHandle.Share" />
44+
public FileShare Share { get; }
45+
46+
/// <inheritdoc cref="IDisposable.Dispose()" />
47+
public void Dispose()
48+
{
49+
_releaseCallback.Invoke(_key);
50+
}
51+
52+
#endregion
53+
54+
public bool GrantAccess(
55+
FileAccess access,
56+
FileShare share,
57+
bool deleteAccess,
58+
bool ignoreFileShare)
59+
{
60+
FileShare usedShare = share;
61+
FileShare currentShare = Share;
62+
if (!_fileSystem.Execute.IsWindows)
63+
{
64+
usedShare = FileShare.ReadWrite;
65+
if (ignoreFileShare)
66+
{
67+
currentShare = FileShare.ReadWrite;
68+
}
69+
}
70+
71+
if (deleteAccess)
72+
{
73+
return !_fileSystem.Execute.IsWindows || Share == FileShare.Delete;
74+
}
75+
76+
return CheckAccessWithShare(access, currentShare) &&
77+
CheckAccessWithShare(Access, usedShare);
78+
}
79+
80+
/// <inheritdoc cref="object.ToString()" />
81+
public override string ToString()
82+
=> $"{(DeleteAccess ? "Delete" : Access)} | {Share}";
83+
84+
private static bool CheckAccessWithShare(FileAccess access, FileShare share)
85+
{
86+
switch (access)
87+
{
88+
case FileAccess.Read:
89+
return share.HasFlag(FileShare.Read);
90+
case FileAccess.Write:
91+
return share.HasFlag(FileShare.Write);
92+
default:
93+
return share == FileShare.ReadWrite;
94+
}
95+
}
96+
97+
private sealed class IgnoreFileHandle : IStorageAccessHandle
98+
{
99+
#region IStorageAccessHandle Members
100+
101+
/// <inheritdoc cref="IStorageAccessHandle.Access" />
102+
public FileAccess Access
103+
=> FileAccess.ReadWrite;
104+
105+
/// <inheritdoc cref="IStorageAccessHandle.DeleteAccess" />
106+
public bool DeleteAccess
107+
=> false;
108+
109+
/// <inheritdoc cref="IStorageAccessHandle.Share" />
110+
public FileShare Share
111+
=> FileShare.ReadWrite;
112+
113+
/// <inheritdoc cref="IDisposable.Dispose()" />
114+
public void Dispose()
115+
{
116+
// Do nothing
117+
}
118+
119+
#endregion
120+
}
121+
}

Source/Testably.Abstractions.Testing/Storage/IStorage.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,28 @@ bool TryAddContainer(IStorageLocation location,
202202
Func<IStorageLocation, MockFileSystem, IStorageContainer>
203203
containerGenerator,
204204
[NotNullWhen(true)] out IStorageContainer? container);
205+
206+
/// <summary>
207+
/// Tries to get access to the file at <paramref name="location" /> with <paramref name="access" /> and
208+
/// <paramref name="share" />.
209+
/// </summary>
210+
/// <param name="location">The location of the file.</param>
211+
/// <param name="access">The requested file access.</param>
212+
/// <param name="share">The requested file share.</param>
213+
/// <param name="deleteAccess">Flag, indicating if the access comes from a delete request.</param>
214+
/// <param name="ignoreFileShare">
215+
/// Flag, indicating if the file share should be ignored.
216+
/// <para />
217+
/// This parameter is required due to OS-specific differences.
218+
/// </param>
219+
/// <param name="fileHandle">(out) The file handle.</param>
220+
/// <returns>
221+
/// <see langword="true" /> if the access to the file was granted, otherwise <see langword="false" />.
222+
/// </returns>
223+
bool TryGetFileAccess(IStorageLocation location,
224+
FileAccess access,
225+
FileShare share,
226+
bool deleteAccess,
227+
bool ignoreFileShare,
228+
[NotNullWhen(true)] out FileHandle? fileHandle);
205229
}

0 commit comments

Comments
 (0)