Skip to content

feat: Add support for event-driven behaviours in MockFileSystem. #1313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,85 @@ public class SomeClassUsingFileSystemWatcher
}
```

### MockFileSystem Events

The `MockFileSystem` in the testing helpers now supports an event system that allows you to observe file system operations and simulate errors:

```csharp
[Test]
public void Test_SimulateDiskFullError()
{
var fileSystem = new MockFileSystem(new MockFileSystemOptions { EnableEvents = true });

using (fileSystem.Events.Subscribe(FileOperation.Write, args =>
{
if (args.Phase == OperationPhase.Before)
{
args.SetResponse(new OperationResponse
{
Exception = new IOException("There is not enough space on the disk.")
});
}
}))
{
Assert.Throws<IOException>(() => fileSystem.File.WriteAllText(@"C:\test.txt", "content"));
}
}

[Test]
public void Test_TrackFileOperations()
{
var fileSystem = new MockFileSystem(new MockFileSystemOptions { EnableEvents = true });
var operations = new List<string>();

using (fileSystem.Events.Subscribe(args =>
{
if (args.Phase == OperationPhase.After)
{
operations.Add($"{args.Operation} {args.Path}");
}
}))
{
fileSystem.File.Create(@"C:\test.txt").Dispose();
fileSystem.File.WriteAllText(@"C:\test.txt", "content");
fileSystem.File.Delete(@"C:\test.txt");

Assert.That(operations, Is.EqualTo(new[] {
"Create C:\\test.txt",
"Write C:\\test.txt",
"Delete C:\\test.txt"
}));
}
}

[Test]
public void Test_SimulateFileInUse()
{
var fileSystem = new MockFileSystem(new MockFileSystemOptions { EnableEvents = true });
fileSystem.AddFile(@"C:\locked.db", "data");

// Simulate file locking for .db files
using (fileSystem.Events.Subscribe(args =>
{
if (args.Phase == OperationPhase.Before &&
args.Path.EndsWith(".db") &&
args.Operation == FileOperation.Delete)
{
args.SetResponse(new OperationResponse
{
Exception = new IOException("The file is in use.")
});
}
}))
{
var exception = Assert.Throws<IOException>(() => fileSystem.File.Delete(@"C:\locked.db"));
Assert.That(exception.Message, Is.EqualTo("The file is in use."));
}
}
```

The event system is opt-in and has almost zero overhead when not enabled. Enable it by setting `EnableEvents = true` in `MockFileSystemOptions`.

## Related projects

- [`System.IO.Abstractions.Extensions`](https://github.com/TestableIO/System.IO.Abstractions.Extensions)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace System.IO.Abstractions.TestingHelpers.Events;

/// <summary>
/// Represents the type of file system operation.
/// </summary>
public enum FileOperation
{
/// <summary>
/// File or directory creation operation.
Copy link
Member

Choose a reason for hiding this comment

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

It would help to have a list of operations on the file system in the XML-Comment, that trigger the corresponding FileOperations.

/// </summary>
Create,

/// <summary>
/// File open operation.
/// </summary>
Open,

/// <summary>
/// File write operation.
/// </summary>
Write,

/// <summary>
/// File read operation.
/// </summary>
Read,

/// <summary>
/// File or directory deletion operation.
/// </summary>
Delete,

/// <summary>
/// File or directory move operation.
/// </summary>
Move,

/// <summary>
/// File or directory copy operation.
/// </summary>
Copy,

/// <summary>
/// Set attributes operation.
/// </summary>
SetAttributes,

/// <summary>
/// Set file times operation.
/// </summary>
SetTimes,

/// <summary>
/// Set permissions operation.
/// </summary>
SetPermissions
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace System.IO.Abstractions.TestingHelpers.Events;

/// <summary>
/// Provides data for file system operation events.
/// </summary>
public class FileSystemOperationEventArgs : EventArgs
{
private OperationResponse response;

/// <summary>
/// Initializes a new instance of the <see cref="FileSystemOperationEventArgs"/> class.
/// </summary>
/// <param name="path">The path of the resource being operated on.</param>
/// <param name="operation">The type of operation.</param>
/// <param name="resourceType">The type of resource.</param>
/// <param name="phase">The phase of the operation.</param>
public FileSystemOperationEventArgs(
string path,
FileOperation operation,
ResourceType resourceType,
OperationPhase phase)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Operation = operation;
ResourceType = resourceType;
Phase = phase;
}

/// <summary>
/// Gets the path of the resource being operated on.
/// </summary>
public string Path { get; }

/// <summary>
/// Gets the type of operation being performed.
/// </summary>
public FileOperation Operation { get; }

/// <summary>
/// Gets the type of resource being operated on.
/// </summary>
public ResourceType ResourceType { get; }

/// <summary>
/// Gets the phase of the operation.
/// </summary>
public OperationPhase Phase { get; }

/// <summary>
/// Sets a response for the operation. Only valid for Before phase events.
/// </summary>
/// <param name="response">The response to set.</param>
/// <exception cref="InvalidOperationException">Thrown when called on an After phase event.</exception>
public void SetResponse(OperationResponse response)
{
if (Phase != OperationPhase.Before)
{
throw new InvalidOperationException("Response can only be set for Before phase events.");
}

this.response = response ?? throw new ArgumentNullException(nameof(response));
}

/// <summary>
/// Gets the response set for this operation, if any.
/// </summary>
internal OperationResponse GetResponse() => response;
}
Loading
Loading