Skip to content
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

feat: Support event callbacks and custom exceptions in MockFileSystem #883

Closed
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
39 changes: 39 additions & 0 deletions src/System.IO.Abstractions.TestingHelpers/MockDirectoryEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace System.IO.Abstractions.TestingHelpers
{
/// <summary>
/// Notifies about a pending directory event.
/// </summary>
public class MockDirectoryEvent
{
/// <summary>
/// The path of the directory.
/// </summary>
public string Path { get; }

/// <summary>
/// The type of the directory event.
/// </summary>
public DirectoryEventType EventType { get; }

internal MockDirectoryEvent(string path, DirectoryEventType eventType)
{
Path = path;
EventType = eventType;
}

/// <summary>
/// The type of the directory event.
/// </summary>
public enum DirectoryEventType
{
/// <summary>
/// The directory is created.
/// </summary>
Created,
/// <summary>
/// The directory is deleted.
/// </summary>
Deleted
}
}
}
43 changes: 43 additions & 0 deletions src/System.IO.Abstractions.TestingHelpers/MockFileEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace System.IO.Abstractions.TestingHelpers
{
/// <summary>
/// Notifies about a pending file event.
/// </summary>
public class MockFileEvent
{
/// <summary>
/// The path of the file.
/// </summary>
public string Path { get; }

/// <summary>
/// The type of the file event.
/// </summary>
public FileEventType EventType { get; }

internal MockFileEvent(string path, FileEventType changeType)
{
Path = path;
EventType = changeType;
}

/// <summary>
/// The type of the file event.
/// </summary>
public enum FileEventType
{
/// <summary>
/// The file is created.
/// </summary>
Created,
/// <summary>
/// The file is updated.
/// </summary>
Updated,
/// <summary>
/// The file is deleted.
/// </summary>
Deleted
}
}
}
54 changes: 53 additions & 1 deletion src/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public class MockFileSystem : FileSystemBase, IMockFileDataAccessor
private readonly IDictionary<string, FileSystemEntry> files;
private readonly PathVerifier pathVerifier;

private Action<MockFileEvent> onFileEvent;
private Action<MockDirectoryEvent> onDirectoryEvent;

/// <inheritdoc />
public MockFileSystem() : this(null) { }

Expand Down Expand Up @@ -88,6 +91,24 @@ public MockFileSystem(IDictionary<string, MockFileData> files, string currentDir
/// <inheritdoc />
public PathVerifier PathVerifier => pathVerifier;

/// <summary>
/// Registers a callback to be executed when a file event is triggered.
/// </summary>
public MockFileSystem OnFileEvent(Action<MockFileEvent> callback)
{
onFileEvent = callback;
return this;
}

/// <summary>
/// Registers a callback to be executed when a directory event is triggered.
/// </summary>
public MockFileSystem OnDirectoryEvent(Action<MockDirectoryEvent> callback)
{
onDirectoryEvent = callback;
return this;
}

private string FixPath(string path, bool checkCaps = false)
{
if (path == null)
Expand Down Expand Up @@ -164,6 +185,15 @@ public void AddFile(string path, MockFileData mockFile)
AddDirectory(directoryPath);
}

var existingFile = GetFileWithoutFixingPath(fixedPath);
if (existingFile == null)
{
onFileEvent?.Invoke(new MockFileEvent(fixedPath, MockFileEvent.FileEventType.Created));
}
else
{
onFileEvent?.Invoke(new MockFileEvent(fixedPath, MockFileEvent.FileEventType.Updated));
}
SetEntry(fixedPath, mockFile ?? new MockFileData(string.Empty));
}
}
Expand Down Expand Up @@ -211,6 +241,8 @@ public void AddDirectory(string path)
}

var s = StringOperations.EndsWith(fixedPath, separator) ? fixedPath : fixedPath + separator;

onDirectoryEvent?.Invoke(new MockDirectoryEvent(s.TrimSlashes(), MockDirectoryEvent.DirectoryEventType.Created));
SetEntry(s, new MockDirectoryData());
}
}
Expand Down Expand Up @@ -269,6 +301,16 @@ public void MoveDirectory(string sourcePath, string destPath)
var newPath = Path.Combine(destPath, path.Substring(sourcePath.Length).TrimStart(Path.DirectorySeparatorChar));
var entry = files[path];
entry.Path = newPath;
if (entry.Data is MockDirectoryData)
{
onDirectoryEvent?.Invoke(new MockDirectoryEvent(path, MockDirectoryEvent.DirectoryEventType.Deleted));
onDirectoryEvent?.Invoke(new MockDirectoryEvent(newPath, MockDirectoryEvent.DirectoryEventType.Created));
}
else
{
onFileEvent?.Invoke(new MockFileEvent(path, MockFileEvent.FileEventType.Deleted));
onFileEvent?.Invoke(new MockFileEvent(newPath, MockFileEvent.FileEventType.Created));
}
files[newPath] = entry;
files.Remove(path);
}
Expand Down Expand Up @@ -297,7 +339,7 @@ bool PathStartsWith(string path, string[] minMatch)
/// <inheritdoc />
public void RemoveFile(string path)
{
path = FixPath(path);
path = FixPath(path).TrimSlashes();

lock (files)
{
Expand All @@ -306,6 +348,16 @@ public void RemoveFile(string path)
throw CommonExceptions.AccessDenied(path);
}

var file = GetFileWithoutFixingPath(path);
if (file is MockDirectoryData)
{
onDirectoryEvent?.Invoke(new MockDirectoryEvent(path, MockDirectoryEvent.DirectoryEventType.Deleted));
}
else
{
onFileEvent?.Invoke(new MockFileEvent(path, MockFileEvent.FileEventType.Deleted));
}

files.Remove(path);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System.Collections.Generic;
using NUnit.Framework;

namespace System.IO.Abstractions.TestingHelpers.Tests
{
[TestFixture]
public class MockFileSystemEventTests
{
[Test]
public void OnFileChanging_ThrowExceptionInCallback_ShouldThrowExceptionAndNotCreateFile()
{
var basePath = Path.GetFullPath("/foo/bar");
var fileName = "foo.txt";
var exception = new Exception("the file should not be created");
var expectedPath = Path.Combine(basePath, fileName);
var fs = new MockFileSystem(null, basePath)
.OnFileEvent(_ => throw exception);

var receivedException = Assert.Throws<Exception>(() => fs.File.WriteAllText(fileName, "some content"));
var result = fs.File.Exists(expectedPath);

Assert.That(receivedException, Is.EqualTo(exception));
Assert.That(result, Is.False);
}

[Test]
public void OnFileChanging_WithFileEvent_ShouldCallOnFileChangingWithFullFilePath()
{
var basePath = Path.GetFullPath("/foo/bar");
var fileName = "foo.txt";
var expectedPath = Path.Combine(basePath, fileName);
var calledPath = string.Empty;
var fs = new MockFileSystem(null, basePath)
.OnFileEvent(f => calledPath = f.Path);

fs.File.WriteAllText(fileName, "some content");

Assert.That(calledPath, Is.EqualTo(expectedPath));
}

[Test]
public void OnFileChanging_WithDirectoryEvent_ShouldNotBeCalled()
{
var basePath = Path.GetFullPath("/foo/bar");
var directoryName = "test-directory";
bool isCalled = false;
var fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ directoryName, new MockFileData("some content") }
}, basePath)
.OnFileEvent(f => isCalled = true);

_ = fs.Directory.CreateDirectory(directoryName);

Assert.That(isCalled, Is.False);
}

[Test]
public void OnDirectoryChanging_ThrowExceptionInCallback_ShouldThrowExceptionAndNotCreateDirectory()
{
var basePath = Path.GetFullPath("/foo/bar");
var directoryName = "test-directory";
var exception = new Exception("the directory should not be created");
var expectedPath = Path.Combine(basePath, directoryName);
var fs = new MockFileSystem(null, basePath)
.OnDirectoryEvent(_ => throw exception);

var receivedException = Assert.Throws<Exception>(() => fs.Directory.CreateDirectory(directoryName));
var result = fs.Directory.Exists(expectedPath);

Assert.That(receivedException, Is.EqualTo(exception));
Assert.That(result, Is.False);
}

[Test]
public void OnDirectoryChanging_WithDirectoryEvent_ShouldCallOnDirectoryChangingWithFullDirectoryPath()
{
var basePath = Path.GetFullPath("/foo/bar");
var directoryName = "test-directory";
var expectedPath = Path.Combine(basePath, directoryName);
var calledPath = string.Empty;
var fs = new MockFileSystem(null, basePath)
.OnDirectoryEvent(f => calledPath = f.Path);

fs.Directory.CreateDirectory(directoryName);

Assert.That(calledPath, Is.EqualTo(expectedPath));
}

[Test]
public void OnDirectoryChanging_WithFileEvent_ShouldNotBeCalled()
{
var basePath = Path.GetFullPath("/foo/bar");
var fileName = "test-directory";
bool isCalled = false;
var fs = new MockFileSystem(null, basePath)
.OnDirectoryEvent(f => isCalled = true);

fs.File.WriteAllText(fileName, "some content");

Assert.That(isCalled, Is.False);
}

[Test]
public void File_WriteAllText_NewFile_ShouldTriggerOnFileChangingWithCreatedType()
{
var fileName = "foo.txt";
MockFileEvent.FileEventType? receivedEventType = null;
var fs = new MockFileSystem()
.OnFileEvent(f => receivedEventType = f.EventType);

fs.File.WriteAllText(fileName, "some content");

Assert.That(receivedEventType, Is.EqualTo(MockFileEvent.FileEventType.Created));
}

[Test]
public void File_WriteAllText_ExistingFile_ShouldTriggerOnFileChangingWithUpdatedType()
{
var fileName = "foo.txt";
MockFileEvent.FileEventType? receivedEventType = null;
var fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ fileName, new MockFileData("some content") }
}).OnFileEvent(f => receivedEventType = f.EventType);

fs.File.WriteAllText(fileName, "some content");

Assert.That(receivedEventType, Is.EqualTo(MockFileEvent.FileEventType.Updated));
}

[Test]
public void File_Delete_ShouldTriggerOnFileChangingWithDeletedType()
{
var fileName = "foo.txt";
MockFileEvent.FileEventType? receivedEventType = null;
var fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ fileName, new MockFileData("some content") }
}).OnFileEvent(f => receivedEventType = f.EventType);

fs.File.Delete(fileName);

Assert.That(receivedEventType, Is.EqualTo(MockFileEvent.FileEventType.Deleted));
}

[Test]
public void File_Move_ShouldTriggerOnFileChangingWithDeletedAndCreatedTypes()
{
var fileName = "foo.txt";
var receivedEventTypes = new List<MockFileEvent.FileEventType>();
var fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ fileName, new MockFileData("some content") }
}).OnFileEvent(f => receivedEventTypes.Add(f.EventType));

fs.File.Move(fileName, "bar.txt");

Assert.That(receivedEventTypes, Contains.Item(MockFileEvent.FileEventType.Deleted));
Assert.That(receivedEventTypes, Contains.Item(MockFileEvent.FileEventType.Created));
}

[Test]
public void Directory_CreateDirectory_ShouldCallOnDirectoryChangingWithFullDirectoryPath()
{
var directoryName = "test-directory";
MockDirectoryEvent.DirectoryEventType? receivedEventType = null;
var fs = new MockFileSystem()
.OnDirectoryEvent(f => receivedEventType = f.EventType);

fs.Directory.CreateDirectory(directoryName);

Assert.That(receivedEventType, Is.EqualTo(MockDirectoryEvent.DirectoryEventType.Created));
}

[Test]
public void Directory_DeleteDirectory_ShouldCallOnDirectoryChangingWithFullDirectoryPath()
{
var directoryName = "test-directory";
MockDirectoryEvent.DirectoryEventType? receivedEventType = null;
var fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ directoryName, new MockDirectoryData() }
}).OnDirectoryEvent(f => receivedEventType = f.EventType);

fs.Directory.Delete(directoryName);

Assert.That(receivedEventType, Is.EqualTo(MockDirectoryEvent.DirectoryEventType.Deleted));
}

[Test]
public void Directory_Move_ShouldTriggerOnDirectoryChangingWithDeletedAndCreatedTypes()
{
var fileName = "foo.txt";
var receivedEventTypes = new List<MockDirectoryEvent.DirectoryEventType>();
var fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ fileName, new MockDirectoryData() }
}).OnDirectoryEvent(f => receivedEventTypes.Add(f.EventType));

fs.Directory.Move(fileName, "bar.txt");

Assert.That(receivedEventTypes, Contains.Item(MockDirectoryEvent.DirectoryEventType.Deleted));
Assert.That(receivedEventTypes, Contains.Item(MockDirectoryEvent.DirectoryEventType.Created));
}
}
}