diff --git a/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherFactoryTests.cs b/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherFactoryTests.cs index 53cc83ec5..cf988cf40 100644 --- a/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherFactoryTests.cs +++ b/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherFactoryTests.cs @@ -13,7 +13,8 @@ public class MockFileSystemWatcherFactoryTests public void MockFileSystemWatcherFactory_CreateNew_ShouldReturnNonNullMockWatcher() { // Arrange - var factory = new MockFileSystemWatcherFactory(); + var fs = new MockFileSystem(); + var factory = new MockFileSystemWatcherFactory(fs); // Act var result = factory.CreateNew(); @@ -26,7 +27,8 @@ public void MockFileSystemWatcherFactory_CreateNew_ShouldReturnNonNullMockWatche public void MockFileSystemWatcherFactory_FromPath_ShouldReturnNonNullMockWatcher() { // Arrange - var factory = new MockFileSystemWatcherFactory(); + var fs = new MockFileSystem(); + var factory = new MockFileSystemWatcherFactory(fs); // Act var result = factory.FromPath(@"y:\test"); @@ -40,7 +42,8 @@ public void MockFileSystemWatcherFactory_FromPath_ShouldReturnWatcherForSpecifie { // Arrange const string path = @"z:\test"; - var factory = new MockFileSystemWatcherFactory(); + var fs = new MockFileSystem(); + var factory = new MockFileSystemWatcherFactory(fs); // Act var result = factory.FromPath(path); diff --git a/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherTests.cs b/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherTests.cs new file mode 100644 index 000000000..9e18bf4a8 --- /dev/null +++ b/System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemWatcherTests.cs @@ -0,0 +1,68 @@ +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace System.IO.Abstractions.TestingHelpers.Tests +{ + [TestFixture] + public class MockFileSystemWatcherTests + { + [Test] + public void MockFileSystemWatcher_OnCreated_Listen() + { + var fs = new MockFileSystem(); + fs.AddDirectory(@"C:\"); + var count = 0; + + using (var watcher = fs.FileSystemWatcher.FromPath(@"C:\")) + { + watcher.Created += (sender, e) => count++; + fs.File.Create(@"C:\test.txt").Close(); + } + + Thread.Sleep(500); // TODO: make this unnecessary + Assert.AreEqual(1, count); + } + + [Test] + public void MockFileSystemWatcher_OnCreated_UnderSpecificRoot() + { + var fs = new MockFileSystem(); + fs.AddDirectory(@"C:\root"); + var count = 0; + + using (var watcher = fs.FileSystemWatcher.FromPath(@"C:\root")) + { + watcher.Created += (sender, e) => count++; + fs.File.Create(@"C:\test.txt").Close(); + fs.File.Create(@"C:\root\test.txt").Close(); + } + + Thread.Sleep(500); // TODO: make this unnecessary + Assert.AreEqual(1, count); + } + + [Test] + public void MockFileSystemWatcher_WaitForChanged_SpecificChangeType() + { + var fs = new MockFileSystem(); + fs.AddDirectory(@"C:\root"); + var count = 0; + + using (var watcher = fs.FileSystemWatcher.FromPath(@"C:\root")) + { + var task = Task.Factory + .StartNew(() => watcher.WaitForChanged(WatcherChangeTypes.Deleted, 1000)) + .ContinueWith(_ => count++); + fs.File.Create(@"C:\root\test.txt").Close(); + fs.File.Move(@"C:\root\test.txt", @"C:\root\other.txt"); + Thread.Sleep(500); // TODO: make this unnecessary + Assert.AreEqual(0, count); + fs.File.Delete(@"C:\root\other.txt"); + } + + Thread.Sleep(500); // TODO: make this unnecessary + Assert.AreEqual(1, count); + } + } +} diff --git a/System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs b/System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs index fefb8ede4..3646865ac 100644 --- a/System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs +++ b/System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Reflection; namespace System.IO.Abstractions.TestingHelpers @@ -21,6 +22,7 @@ public interface IMockFileDataAccessor void AddFileFromEmbeddedResource(string path, Assembly resourceAssembly, string embeddedResourcePath); void AddFilesFromEmbeddedNamespace(string path, Assembly resourceAssembly, string embeddedRresourcePath); + void MoveFile(string sourcePath, string destPath); void MoveDirectory(string sourcePath, string destPath); /// @@ -60,9 +62,9 @@ public interface IMockFileDataAccessor PathBase Path { get; } IDirectoryInfoFactory DirectoryInfo { get; } IDriveInfoFactory DriveInfo { get; } - PathVerifier PathVerifier { get; } - IFileSystem FileSystem { get; } + + ConcurrentQueue Listen(); } } diff --git a/System.IO.Abstractions.TestingHelpers/MockFile.cs b/System.IO.Abstractions.TestingHelpers/MockFile.cs index 1322e2e29..6516096a9 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFile.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFile.cs @@ -362,16 +362,11 @@ public override void Move(string sourceFileName, string destFileName) } } - - var sourceFile = mockFileDataAccessor.GetFile(sourceFileName); - - if (sourceFile == null) + if (!mockFileDataAccessor.FileExists(sourceFileName)) throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "The file \"{0}\" could not be found.", sourceFileName), sourceFileName); VerifyDirectoryExists(destFileName); - - mockFileDataAccessor.AddFile(destFileName, new MockFileData(sourceFile.Contents)); - mockFileDataAccessor.RemoveFile(sourceFileName); + mockFileDataAccessor.MoveFile(sourceFileName, destFileName); } public override Stream Open(string path, FileMode mode) diff --git a/System.IO.Abstractions.TestingHelpers/MockFileInfo.cs b/System.IO.Abstractions.TestingHelpers/MockFileInfo.cs index 298d054be..9ecbdbd20 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileInfo.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileInfo.cs @@ -1,5 +1,8 @@ using System.Security.AccessControl; +// TODO: make sure mock file system events are +// fired for operations on MockFileInfo + namespace System.IO.Abstractions.TestingHelpers { [Serializable] @@ -217,6 +220,11 @@ public override void MoveTo(string destFileName) } Delete(); path = movedFileInfo.FullName; + + // TODO: use File.Move instead + //var destPath = mockFileSystem.Path.GetFullPath(destFileName); + //mockFileSystem.MoveFile(path, destPath); + //path = destPath; } public override Stream Open(FileMode mode) diff --git a/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs b/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs index 186dcebbe..def057242 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileSystem.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; @@ -10,9 +11,13 @@ namespace System.IO.Abstractions.TestingHelpers public class MockFileSystem : IFileSystem, IMockFileDataAccessor { private readonly IDictionary files; + [NonSerialized] private readonly PathVerifier pathVerifier; + [NonSerialized] + private readonly List> queues; + public MockFileSystem() : this(null) { } public MockFileSystem(IDictionary files, string currentDirectory = "") @@ -23,9 +28,9 @@ public MockFileSystem(IDictionary files, string currentDir } pathVerifier = new PathVerifier(this); - + queues = new List>(); this.files = new Dictionary(StringComparer.OrdinalIgnoreCase); - + Path = new MockPath(this); File = new MockFile(this); Directory = new MockDirectory(this, File, currentDirectory); @@ -33,7 +38,7 @@ public MockFileSystem(IDictionary files, string currentDir FileStream = new MockFileStreamFactory(this); DirectoryInfo = new MockDirectoryInfoFactory(this); DriveInfo = new MockDriveInfoFactory(this); - FileSystemWatcher = new MockFileSystemWatcherFactory(); + FileSystemWatcher = new MockFileSystemWatcherFactory(this); if (files != null) { @@ -140,6 +145,8 @@ public void AddFile(string path, MockFileData mockFile) SetEntry(fixedPath, mockFile ?? new MockFileData(string.Empty)); } + + DispatchCreated(fixedPath); } public void AddDirectory(string path) @@ -219,6 +226,19 @@ public void AddFilesFromEmbeddedNamespace(string path, Assembly resourceAssembly } } + public void MoveFile(string sourcePath, string destPath) + { + sourcePath = FixPath(sourcePath); + destPath = FixPath(destPath); + var entry = files[destPath] = files[sourcePath]; + files.Remove(sourcePath); + + if (!entry.IsDirectory) + { + DispatchMoved(sourcePath, destPath); + } + } + public void MoveDirectory(string sourcePath, string destPath) { sourcePath = FixPath(sourcePath); @@ -233,8 +253,7 @@ public void MoveDirectory(string sourcePath, string destPath) foreach(var path in affectedPaths) { var newPath = path.Replace(sourcePath, destPath, StringComparison.OrdinalIgnoreCase); - files[newPath] = files[path]; - files.Remove(path); + MoveFile(path, newPath); } } } @@ -250,7 +269,15 @@ public void RemoveFile(string path) throw new UnauthorizedAccessException(string.Format(CultureInfo.InvariantCulture, StringResources.Manager.GetString("ACCESS_TO_THE_PATH_IS_DENIED"), path)); } - files.Remove(path); + if (files.TryGetValue(path, out var entry)) + { + files.Remove(path); + + if (!entry.IsDirectory) + { + DispatchDeleted(path); + } + } } } @@ -326,5 +353,58 @@ private MockFileData GetFileWithoutFixingPath(string path) return result; } } + + public ConcurrentQueue Listen() + { + lock (queues) + { + var queue = new ConcurrentQueue(); + queues.Add(queue); + return queue; + } + } + + private void Dispatch(FileSystemEventArgs e) + { + lock (queues) + { + foreach (var queue in queues) + { + queue.Enqueue(e); + } + } + } + + private void DispatchCreated(string path) => + Dispatch(new FileSystemEventArgs( + WatcherChangeTypes.Created, + Path.GetDirectoryName(path), + Path.GetFileName(path))); + + private void DispatchDeleted(string path) => + Dispatch(new FileSystemEventArgs( + WatcherChangeTypes.Deleted, + Path.GetDirectoryName(path), + Path.GetFileName(path))); + + private void DispatchMoved(string oldPath, string newPath) + { + if (Path.GetDirectoryName(oldPath) == Path.GetDirectoryName(newPath)) + { + DispatchRenamed(oldPath, newPath); + } + else + { + DispatchDeleted(oldPath); + DispatchCreated(newPath); + } + } + + private void DispatchRenamed(string oldPath, string newPath) => + Dispatch(new RenamedEventArgs( + WatcherChangeTypes.Renamed, + Path.GetDirectoryName(newPath), + Path.GetFileName(newPath), + Path.GetFileName(oldPath))); } } diff --git a/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcher.cs b/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcher.cs index a1204412d..e1034a46a 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcher.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcher.cs @@ -1,9 +1,81 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace System.IO.Abstractions.TestingHelpers { public class MockFileSystemWatcher : FileSystemWatcherBase { + private readonly CancellationTokenSource cancel; + private readonly Task task; + private readonly Dictionary> waiters; + + public MockFileSystemWatcher(IMockFileDataAccessor mockFileDataAccessor, string root = null) + { + Path = root; + cancel = new CancellationTokenSource(); + waiters = new Dictionary>(); + var queue = mockFileDataAccessor.Listen(); + var pathBase = mockFileDataAccessor.Path; + + void ConsumeEvents() + { + while (!cancel.IsCancellationRequested) + { + if (queue.TryDequeue(out var e) && + (root == null || pathBase.GetFullPath(e.FullPath).StartsWith(root))) + { + lock (waiters) + { + var keysToRemove = new List(); + + foreach (var keyChangeType in waiters.Keys) + { + // if event has all the change type + // flags as dictionary entry + if ((keyChangeType & e.ChangeType) == keyChangeType) + { + foreach (var waiter in waiters) + { + waiter.Value.Send(e); + } + + keysToRemove.Add(keyChangeType); + } + } + + foreach (var keyToRemove in keysToRemove) + { + waiters.Remove(keyToRemove); + } + } + + if (e.ChangeType.HasFlag(WatcherChangeTypes.Created)) + { + OnCreated(this, e); + } + + if (e.ChangeType.HasFlag(WatcherChangeTypes.Deleted)) + { + OnDeleted(this, e); + } + + if (e.ChangeType.HasFlag(WatcherChangeTypes.Renamed)) + { + OnRenamed(this, (RenamedEventArgs)e); + } + + if (e.ChangeType.HasFlag(WatcherChangeTypes.Changed)) + { + OnChanged(this, e); + } + } + } + } + + task = Task.Factory.StartNew(ConsumeEvents); + } + public override bool IncludeSubdirectories { get; set; } public override bool EnableRaisingEvents { get; set; } public override string Filter { get; set; } @@ -11,11 +83,9 @@ public class MockFileSystemWatcher : FileSystemWatcherBase public override NotifyFilters NotifyFilter { get; set; } public override string Path { get; set; } #if NET40 - public override ISite Site { get; set; } - public override ISynchronizeInvoke SynchronizingObject { get; set; } -#endif + public override ComponentModel.ISite Site { get; set; } + public override ComponentModel.ISynchronizeInvoke SynchronizingObject { get; set; } -#if NET40 public override void BeginInit() { } @@ -25,14 +95,43 @@ public override void EndInit() } #endif - public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType) + public override void Dispose(bool disposing) { - throw new NotImplementedException(); + base.Dispose(disposing); + cancel.Cancel(); + task.Wait(); } + public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType) => + WaitForChanged(changeType, int.MaxValue); + public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int timeout) { - throw new NotImplementedException(); + var waiter = new WaitableRef(); + + lock (waiters) + { + waiters.Add(changeType, waiter); + } + + try + { + var e = waiter.Wait(timeout); + + return new WaitForChangedResult + { + ChangeType = e.ChangeType, + Name = e.Name, + OldName = (e as RenamedEventArgs)?.OldName + }; + } + catch (TimeoutException) + { + return new WaitForChangedResult + { + TimedOut = true + }; + } } } } diff --git a/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcherFactory.cs b/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcherFactory.cs index 14a494074..732e42ec2 100644 --- a/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcherFactory.cs +++ b/System.IO.Abstractions.TestingHelpers/MockFileSystemWatcherFactory.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace System.IO.Abstractions.TestingHelpers +namespace System.IO.Abstractions.TestingHelpers { [Serializable] public class MockFileSystemWatcherFactory : IFileSystemWatcherFactory { - public FileSystemWatcherBase CreateNew() - { - return new MockFileSystemWatcher(); - } + private readonly IMockFileDataAccessor mockFileDataAccessor; + + public MockFileSystemWatcherFactory(IMockFileDataAccessor mockFileDataAccessor) => + this.mockFileDataAccessor = mockFileDataAccessor; + + public FileSystemWatcherBase CreateNew() => + new MockFileSystemWatcher(mockFileDataAccessor); - public FileSystemWatcherBase FromPath(string path) - { - return new MockFileSystemWatcher {Path = path}; - } + public FileSystemWatcherBase FromPath(string path) => + new MockFileSystemWatcher(mockFileDataAccessor, path); } } diff --git a/System.IO.Abstractions.TestingHelpers/WaitableRef.cs b/System.IO.Abstractions.TestingHelpers/WaitableRef.cs new file mode 100644 index 000000000..b8d10f73b --- /dev/null +++ b/System.IO.Abstractions.TestingHelpers/WaitableRef.cs @@ -0,0 +1,32 @@ +using System.Threading; + +namespace System.IO.Abstractions.TestingHelpers +{ + public class WaitableRef + { + private readonly ManualResetEventSlim signal = new ManualResetEventSlim(); + private T value; + + public void Send(T value) + { + this.value = value; + signal.Set(); + } + + public T Wait() + { + signal.Wait(); + return value; + } + + public T Wait(int millisecondsTimeout) + { + if (signal.Wait(millisecondsTimeout)) + { + return value; + } + + throw new TimeoutException(); + } + } +} diff --git a/System.IO.Abstractions/FileSystemWatcherBase.cs b/System.IO.Abstractions/FileSystemWatcherBase.cs index cca10924d..efef4d8b2 100644 --- a/System.IO.Abstractions/FileSystemWatcherBase.cs +++ b/System.IO.Abstractions/FileSystemWatcherBase.cs @@ -52,6 +52,7 @@ public abstract class FileSystemWatcherBase : IDisposable public abstract void BeginInit(); #endif + /// public void Dispose() { Dispose(true); @@ -69,6 +70,10 @@ public void Dispose() /// public abstract WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int timeout); + /// + /// Converts a standard to a + /// compatible . + /// public static implicit operator FileSystemWatcherBase(FileSystemWatcher watcher) { if (watcher == null) @@ -79,31 +84,37 @@ public static implicit operator FileSystemWatcherBase(FileSystemWatcher watcher) return new FileSystemWatcherWrapper(watcher); } + /// public virtual void Dispose(bool disposing) { // do nothing } + /// protected void OnCreated(object sender, FileSystemEventArgs args) { Created?.Invoke(sender, args); } + /// protected void OnChanged(object sender, FileSystemEventArgs args) { Changed?.Invoke(sender, args); } + /// protected void OnDeleted(object sender, FileSystemEventArgs args) { Deleted?.Invoke(sender, args); } + /// protected void OnRenamed(object sender, RenamedEventArgs args) { Renamed?.Invoke(sender, args); } + /// protected void OnError(object sender, ErrorEventArgs args) { Error?.Invoke(sender, args); diff --git a/System.IO.Abstractions/FileSystemWatcherWrapper.cs b/System.IO.Abstractions/FileSystemWatcherWrapper.cs index 9e80bc731..aedeb09db 100644 --- a/System.IO.Abstractions/FileSystemWatcherWrapper.cs +++ b/System.IO.Abstractions/FileSystemWatcherWrapper.cs @@ -1,5 +1,7 @@ using System.ComponentModel; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + namespace System.IO.Abstractions { [Serializable]