Skip to content

Commit

Permalink
refactor: store file handles centrally in InMemoryStorage (#603)
Browse files Browse the repository at this point in the history
Store FileHandles centrally in `InMemoryStorage` instead of creating a separate `ConcurrentDictionary` for each file.
  • Loading branch information
vbreuss authored May 19, 2024
1 parent 0c2b777 commit 31315ac
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 149 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using Testably.Abstractions.RandomSystem;
using Testably.Abstractions.Testing.Statistics;
using Testably.Abstractions.Testing.Storage;

namespace Testably.Abstractions.Testing.Helpers;
Expand Down Expand Up @@ -123,15 +122,6 @@ internal static string GetSubdirectoryPath(this MockFileSystem fileSystem,
return fullFilePath;
}

/// <summary>
/// Ignores all registrations on the <paramref name="statisticsGate" /> until the return value is disposed.
/// </summary>
internal static IDisposable Ignore(this IStatisticsGate statisticsGate)
{
statisticsGate.TryGetLock(out IDisposable? release);
return release;
}

/// <summary>
/// Ignores all registrations on the <see cref="MockFileSystem.Statistics" /> until the return value is disposed.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Testably.Abstractions.Testing.Statistics;
internal sealed class FileSystemStatistics : IFileSystemStatistics, IStatisticsGate
{
private static readonly AsyncLocal<bool> IsDisabled = new();
private static readonly AsyncLocal<bool> IsInit = new();

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

#endregion

/// <summary>
/// Ignores all registrations until the return value is disposed.
/// </summary>
internal IDisposable Ignore()
{
if (IsDisabled.Value)
{
return TemporaryDisable.None;
}

IsDisabled.Value = true;
IsInit.Value = true;
return new TemporaryDisable(() =>
{
IsDisabled.Value = false;
IsInit.Value = false;
});
}

internal bool IsInitializing()
=> IsInit.Value;

private sealed class TemporaryDisable : IDisposable
{
public static IDisposable None { get; } = new NoOpDisposable();
Expand Down
19 changes: 0 additions & 19 deletions Source/Testably.Abstractions.Testing/Statistics/PathStatistics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,6 @@ public IStatistics<TType> this[string path]

#endregion

/// <summary>
/// Registers the <paramref name="name" /> callback with <paramref name="parameters" /> under <paramref name="path" />.
/// </summary>
/// <returns>A disposable which ignores all registrations, until it is disposed.</returns>
internal IDisposable RegisterPathMethod(string path, string name,
ParameterDescription[] parameters)
{
if (_statisticsGate.TryGetLock(out IDisposable release))
{
string key = CreateKey(_fileSystem.Storage.CurrentDirectory, path);
CallStatistics<TType> callStatistics =
_statistics.GetOrAdd(key,
k => new CallStatistics<TType>(_statisticsGate, $"{ToString()}[{k}]"));
return callStatistics.RegisterMethodWithoutLock(release, name, parameters);
}

return release;
}

/// <summary>
/// Registers the <paramref name="name" /> callback without parameters under <paramref name="path" />.
/// </summary>
Expand Down
121 changes: 121 additions & 0 deletions Source/Testably.Abstractions.Testing/Storage/FileHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.IO;

namespace Testably.Abstractions.Testing.Storage;

internal sealed class FileHandle : IStorageAccessHandle
{
public static IStorageAccessHandle Ignore { get; } = new IgnoreFileHandle();
private readonly MockFileSystem _fileSystem;
private readonly Guid _key;
private readonly Action<Guid> _releaseCallback;

public FileHandle(MockFileSystem fileSystem, Guid key, Action<Guid> releaseCallback,
FileAccess access,
FileShare share, bool deleteAccess)
{
_fileSystem = fileSystem;
_releaseCallback = releaseCallback;
Access = access;
DeleteAccess = deleteAccess;
if (_fileSystem.Execute.IsWindows)
{
Share = share;
}
else
{
Share = share == FileShare.None
? FileShare.None
: FileShare.ReadWrite;
}

_key = key;
}

#region IStorageAccessHandle Members

/// <inheritdoc cref="IStorageAccessHandle.Access" />
public FileAccess Access { get; }

/// <inheritdoc cref="IStorageAccessHandle.DeleteAccess" />
public bool DeleteAccess { get; }

/// <inheritdoc cref="IStorageAccessHandle.Share" />
public FileShare Share { get; }

/// <inheritdoc cref="IDisposable.Dispose()" />
public void Dispose()
{
_releaseCallback.Invoke(_key);
}

#endregion

public bool GrantAccess(
FileAccess access,
FileShare share,
bool deleteAccess,
bool ignoreFileShare)
{
FileShare usedShare = share;
FileShare currentShare = Share;
if (!_fileSystem.Execute.IsWindows)
{
usedShare = FileShare.ReadWrite;
if (ignoreFileShare)
{
currentShare = FileShare.ReadWrite;
}
}

if (deleteAccess)
{
return !_fileSystem.Execute.IsWindows || Share == FileShare.Delete;
}

return CheckAccessWithShare(access, currentShare) &&
CheckAccessWithShare(Access, usedShare);
}

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
=> $"{(DeleteAccess ? "Delete" : Access)} | {Share}";

private static bool CheckAccessWithShare(FileAccess access, FileShare share)
{
switch (access)
{
case FileAccess.Read:
return share.HasFlag(FileShare.Read);
case FileAccess.Write:
return share.HasFlag(FileShare.Write);
default:
return share == FileShare.ReadWrite;
}
}

private sealed class IgnoreFileHandle : IStorageAccessHandle
{
#region IStorageAccessHandle Members

/// <inheritdoc cref="IStorageAccessHandle.Access" />
public FileAccess Access
=> FileAccess.ReadWrite;

/// <inheritdoc cref="IStorageAccessHandle.DeleteAccess" />
public bool DeleteAccess
=> false;

/// <inheritdoc cref="IStorageAccessHandle.Share" />
public FileShare Share
=> FileShare.ReadWrite;

/// <inheritdoc cref="IDisposable.Dispose()" />
public void Dispose()
{
// Do nothing
}

#endregion
}
}
24 changes: 24 additions & 0 deletions Source/Testably.Abstractions.Testing/Storage/IStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,28 @@ bool TryAddContainer(IStorageLocation location,
Func<IStorageLocation, MockFileSystem, IStorageContainer>
containerGenerator,
[NotNullWhen(true)] out IStorageContainer? container);

/// <summary>
/// Tries to get access to the file at <paramref name="location" /> with <paramref name="access" /> and
/// <paramref name="share" />.
/// </summary>
/// <param name="location">The location of the file.</param>
/// <param name="access">The requested file access.</param>
/// <param name="share">The requested file share.</param>
/// <param name="deleteAccess">Flag, indicating if the access comes from a delete request.</param>
/// <param name="ignoreFileShare">
/// Flag, indicating if the file share should be ignored.
/// <para />
/// This parameter is required due to OS-specific differences.
/// </param>
/// <param name="fileHandle">(out) The file handle.</param>
/// <returns>
/// <see langword="true" /> if the access to the file was granted, otherwise <see langword="false" />.
/// </returns>
bool TryGetFileAccess(IStorageLocation location,
FileAccess access,
FileShare share,
bool deleteAccess,
bool ignoreFileShare,
[NotNullWhen(true)] out FileHandle? fileHandle);
}
Loading

0 comments on commit 31315ac

Please sign in to comment.