diff --git a/src/System.IO.Abstractions.TestingHelpers/MockDirectoryEvent.cs b/src/System.IO.Abstractions.TestingHelpers/MockDirectoryEvent.cs new file mode 100644 index 000000000..939f512d2 --- /dev/null +++ b/src/System.IO.Abstractions.TestingHelpers/MockDirectoryEvent.cs @@ -0,0 +1,39 @@ +namespace System.IO.Abstractions.TestingHelpers +{ + /// + /// Notifies about a pending directory event. + /// + public class MockDirectoryEvent + { + /// + /// The path of the directory. + /// + public string Path { get; } + + /// + /// The type of the directory event. + /// + public DirectoryEventType EventType { get; } + + internal MockDirectoryEvent(string path, DirectoryEventType eventType) + { + Path = path; + EventType = eventType; + } + + /// + /// The type of the directory event. + /// + public enum DirectoryEventType + { + /// + /// The directory is created. + /// + Created, + /// + /// The directory is deleted. + /// + Deleted + } + } +} diff --git a/src/System.IO.Abstractions.TestingHelpers/MockFileEvent.cs b/src/System.IO.Abstractions.TestingHelpers/MockFileEvent.cs new file mode 100644 index 000000000..4a465bba1 --- /dev/null +++ b/src/System.IO.Abstractions.TestingHelpers/MockFileEvent.cs @@ -0,0 +1,43 @@ +namespace System.IO.Abstractions.TestingHelpers +{ + /// + /// Notifies about a pending file event. + /// + public class MockFileEvent + { + /// + /// The path of the file. + /// + public string Path { get; } + + /// + /// The type of the file event. + /// + public FileEventType EventType { get; } + + internal MockFileEvent(string path, FileEventType changeType) + { + Path = path; + EventType = changeType; + } + + /// + /// The type of the file event. + /// + public enum FileEventType + { + /// + /// The file is created. + /// + Created, + /// + /// The file is updated. + /// + Updated, + /// + /// The file is deleted. + /// + Deleted + } + } +} diff --git a/src/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs b/src/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs index fa3203ac9..573ba6e2c 100644 --- a/src/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs +++ b/src/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs @@ -16,6 +16,9 @@ public class MockFileSystem : FileSystemBase, IMockFileDataAccessor private readonly IDictionary files; private readonly PathVerifier pathVerifier; + private Action onFileEvent; + private Action onDirectoryEvent; + /// public MockFileSystem() : this(null) { } @@ -88,6 +91,24 @@ public MockFileSystem(IDictionary files, string currentDir /// public PathVerifier PathVerifier => pathVerifier; + /// + /// Registers a callback to be executed when a file event is triggered. + /// + public MockFileSystem OnFileEvent(Action callback) + { + onFileEvent = callback; + return this; + } + + /// + /// Registers a callback to be executed when a directory event is triggered. + /// + public MockFileSystem OnDirectoryEvent(Action callback) + { + onDirectoryEvent = callback; + return this; + } + private string FixPath(string path, bool checkCaps = false) { if (path == null) @@ -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)); } } @@ -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()); } } @@ -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); } @@ -297,7 +339,7 @@ bool PathStartsWith(string path, string[] minMatch) /// public void RemoveFile(string path) { - path = FixPath(path); + path = FixPath(path).TrimSlashes(); lock (files) { @@ -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); } } diff --git a/tests/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemEventTests.cs b/tests/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemEventTests.cs new file mode 100644 index 000000000..b12015414 --- /dev/null +++ b/tests/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemEventTests.cs @@ -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(() => 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 + { + { 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(() => 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 + { + { 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 + { + { 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(); + var fs = new MockFileSystem(new Dictionary + { + { 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 + { + { 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(); + var fs = new MockFileSystem(new Dictionary + { + { 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)); + } + } +}