diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs index 97a302806..4b6d2be3d 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs @@ -353,7 +353,9 @@ private bool MatchesFilter(ChangeDescription changeDescription) if (!changeDescription.Path.StartsWith(fullPath, _fileSystem.Execute.StringComparisonMode)) { - return false; + return changeDescription.ChangeType == WatcherChangeTypes.Renamed && + changeDescription.OldPath?.StartsWith(fullPath, + _fileSystem.Execute.StringComparisonMode) == true; } } else if (!string.Equals( @@ -536,12 +538,28 @@ private string TransformPathAndName( } name = transformedName; + if (!_fileSystem.Path.IsPathRooted(Path)) + { + string rootedWatchedPath = _fileSystem.Path.GetFullPath(Path); + if (path?.StartsWith(rootedWatchedPath, _fileSystem.Execute.StringComparisonMode) == + true) + { + path = _fileSystem.Path.Combine(Path, path.Substring(rootedWatchedPath.Length)); + } + } + return path ?? ""; } private void TriggerRenameNotification(ChangeDescription item) { - if (_fileSystem.Execute.IsWindows) + if (!item.Path.StartsWith(Path, _fileSystem.Execute.StringComparisonMode) && + item.OldPath != null) + { + Deleted?.Invoke(this, ToFileSystemEventArgs( + WatcherChangeTypes.Deleted, item.OldPath, item.OldName)); + } + else if (_fileSystem.Execute.IsWindows) { if (TryMakeRenamedEventArgs(item, out RenamedEventArgs? eventArgs)) @@ -551,9 +569,9 @@ private void TriggerRenameNotification(ChangeDescription item) else if (item.OldPath != null) { Deleted?.Invoke(this, ToFileSystemEventArgs( - item.ChangeType, item.OldPath, item.OldName)); + WatcherChangeTypes.Deleted, item.OldPath, item.OldName)); Created?.Invoke(this, ToFileSystemEventArgs( - item.ChangeType, item.Path, item.Name)); + WatcherChangeTypes.Created, item.Path, item.Name)); } } else @@ -692,7 +710,8 @@ public WaitForChangedResultMock( public bool TimedOut { get; } } - internal sealed class ChangeDescriptionEventArgs(ChangeDescription changeDescription) : EventArgs + internal sealed class ChangeDescriptionEventArgs(ChangeDescription changeDescription) + : EventArgs { public ChangeDescription ChangeDescription { get; } = changeDescription; } diff --git a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs index c07b7ef14..0312254e6 100644 --- a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs +++ b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs @@ -1028,7 +1028,7 @@ private bool IncludeItemInEnumeration( ChangeDescription fileSystemChange = _fileSystem.ChangeHandler.NotifyPendingChange(WatcherChangeTypes.Renamed, container.Type, - NotifyFilters.FileName, + ToNotifyFilters(container.Type), destination, source); if (_containers.TryRemove(source, out IStorageContainer? sourceContainer)) diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/FileSystemWatcherMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/FileSystemWatcherMockTests.cs index 21a7901b6..642071650 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/FileSystemWatcherMockTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/FileSystemWatcherMockTests.cs @@ -299,7 +299,7 @@ public async Task RenamedEventArgs_ShouldUseDirectorySeparatorFromSimulatedFileS fileSystem.File.WriteAllText(expectedOldFullPath, "foo"); using IFileSystemWatcher fileSystemWatcher = - fileSystem.FileSystemWatcher.New(parentDirectory); + fileSystem.FileSystemWatcher.New(fileSystem.Path.GetFullPath(parentDirectory)); using ManualResetEventSlim ms = new(); fileSystemWatcher.Renamed += (_, eventArgs) => { diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/NotifyFiltersTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/NotifyFiltersTests.cs index 90a91da42..d63efdddf 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/NotifyFiltersTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/NotifyFiltersTests.cs @@ -611,6 +611,89 @@ public async Task NotifyFilter_MoveFile_ShouldTriggerChangedEventOnNotifyFilters await That(result.OldName).IsEqualTo(FileSystem.Path.GetFileName(sourceName)); } + [Theory] + [InlineAutoData(NotifyFilters.DirectoryName)] + public async Task NotifyFilter_MoveDirectory_ShouldTriggerChangedEventOnNotifyFilters( + NotifyFilters notifyFilter, string sourceName, string destinationName) + { + SkipIfLongRunningTestsShouldBeSkipped(); + + FileSystem.Initialize(); + FileSystem.Directory.CreateDirectory(sourceName); + RenamedEventArgs? result = null; + using ManualResetEventSlim ms = new(); + using IFileSystemWatcher fileSystemWatcher = + FileSystem.FileSystemWatcher.New(BasePath); + fileSystemWatcher.Renamed += (_, eventArgs) => + { + // ReSharper disable once AccessToDisposedClosure + try + { + result = eventArgs; + ms.Set(); + } + catch (ObjectDisposedException) + { + // Ignore any ObjectDisposedException + } + }; + + fileSystemWatcher.NotifyFilter = notifyFilter; + fileSystemWatcher.IncludeSubdirectories = true; + fileSystemWatcher.EnableRaisingEvents = true; + + FileSystem.Directory.Move(sourceName, destinationName); + + await That(ms.Wait(ExpectSuccess, TestContext.Current.CancellationToken)).IsTrue(); + await That(result).IsNotNull(); + await That(result!.ChangeType).IsEqualTo(WatcherChangeTypes.Renamed); + await That(result.FullPath).IsEqualTo(FileSystem.Path.GetFullPath(destinationName)); + await That(result.Name).IsEqualTo(FileSystem.Path.GetFileName(destinationName)); + await That(result.OldFullPath).IsEqualTo(FileSystem.Path.GetFullPath(sourceName)); + await That(result.OldName).IsEqualTo(FileSystem.Path.GetFileName(sourceName)); + } + + [Theory] + [InlineAutoData(NotifyFilters.DirectoryName)] + public async Task NotifyFilter_MoveDirectoryOutOfTheWatchedDirectory_ShouldTriggerChangedEventOnNotifyFilters( + NotifyFilters notifyFilter, string sourceName, string destinationName) + { + SkipIfLongRunningTestsShouldBeSkipped(); + + FileSystem.Initialize().WithSubdirectory("watched"); + var sourcePath = FileSystem.Path.Combine("watched", sourceName); + FileSystem.Directory.CreateDirectory(sourcePath); + FileSystemEventArgs? result = null; + using ManualResetEventSlim ms = new(); + using IFileSystemWatcher fileSystemWatcher = + FileSystem.FileSystemWatcher.New("watched"); + fileSystemWatcher.Deleted += (_, eventArgs) => + { + // ReSharper disable once AccessToDisposedClosure + try + { + result = eventArgs; + ms.Set(); + } + catch (ObjectDisposedException) + { + // Ignore any ObjectDisposedException + } + }; + + fileSystemWatcher.NotifyFilter = notifyFilter; + fileSystemWatcher.IncludeSubdirectories = true; + fileSystemWatcher.EnableRaisingEvents = true; + + FileSystem.Directory.Move(sourcePath, destinationName); + + await That(ms.Wait(ExpectSuccess, TestContext.Current.CancellationToken)).IsTrue(); + await That(result).IsNotNull(); + await That(result!.ChangeType).IsEqualTo(WatcherChangeTypes.Deleted); + await That(result.FullPath).IsEqualTo(sourcePath); + await That(result.Name).IsEqualTo(sourceName); + } + [Theory] [AutoData] public async Task NotifyFilter_WriteFile_ShouldNotNotifyOnOtherFilters(string fileName)