Skip to content

Commit 0fc6b7d

Browse files
authored
fix: Trigger correct notifications when moving directories (#866)
This pull request fixes directory move notifications in the file system watcher to trigger the correct events based on the destination path. The key change ensures that when a directory is moved outside the watched directory, a delete event is properly triggered instead of a rename event. ### Key changes: - Fixed notification filter logic to use appropriate filters for different file system object types (files vs directories) - Updated path matching logic to properly handle directory moves that cross watched directory boundaries - Added comprehensive test coverage for directory move scenarios
1 parent 959ca08 commit 0fc6b7d

File tree

4 files changed

+109
-7
lines changed

4 files changed

+109
-7
lines changed

Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,9 @@ private bool MatchesFilter(ChangeDescription changeDescription)
353353
if (!changeDescription.Path.StartsWith(fullPath,
354354
_fileSystem.Execute.StringComparisonMode))
355355
{
356-
return false;
356+
return changeDescription.ChangeType == WatcherChangeTypes.Renamed &&
357+
changeDescription.OldPath?.StartsWith(fullPath,
358+
_fileSystem.Execute.StringComparisonMode) == true;
357359
}
358360
}
359361
else if (!string.Equals(
@@ -536,12 +538,28 @@ private string TransformPathAndName(
536538
}
537539

538540
name = transformedName;
541+
if (!_fileSystem.Path.IsPathRooted(Path))
542+
{
543+
string rootedWatchedPath = _fileSystem.Path.GetFullPath(Path);
544+
if (path?.StartsWith(rootedWatchedPath, _fileSystem.Execute.StringComparisonMode) ==
545+
true)
546+
{
547+
path = _fileSystem.Path.Combine(Path, path.Substring(rootedWatchedPath.Length));
548+
}
549+
}
550+
539551
return path ?? "";
540552
}
541553

542554
private void TriggerRenameNotification(ChangeDescription item)
543555
{
544-
if (_fileSystem.Execute.IsWindows)
556+
if (!item.Path.StartsWith(Path, _fileSystem.Execute.StringComparisonMode) &&
557+
item.OldPath != null)
558+
{
559+
Deleted?.Invoke(this, ToFileSystemEventArgs(
560+
WatcherChangeTypes.Deleted, item.OldPath, item.OldName));
561+
}
562+
else if (_fileSystem.Execute.IsWindows)
545563
{
546564
if (TryMakeRenamedEventArgs(item,
547565
out RenamedEventArgs? eventArgs))
@@ -551,9 +569,9 @@ private void TriggerRenameNotification(ChangeDescription item)
551569
else if (item.OldPath != null)
552570
{
553571
Deleted?.Invoke(this, ToFileSystemEventArgs(
554-
item.ChangeType, item.OldPath, item.OldName));
572+
WatcherChangeTypes.Deleted, item.OldPath, item.OldName));
555573
Created?.Invoke(this, ToFileSystemEventArgs(
556-
item.ChangeType, item.Path, item.Name));
574+
WatcherChangeTypes.Created, item.Path, item.Name));
557575
}
558576
}
559577
else
@@ -692,7 +710,8 @@ public WaitForChangedResultMock(
692710
public bool TimedOut { get; }
693711
}
694712

695-
internal sealed class ChangeDescriptionEventArgs(ChangeDescription changeDescription) : EventArgs
713+
internal sealed class ChangeDescriptionEventArgs(ChangeDescription changeDescription)
714+
: EventArgs
696715
{
697716
public ChangeDescription ChangeDescription { get; } = changeDescription;
698717
}

Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ private bool IncludeItemInEnumeration(
10281028
ChangeDescription fileSystemChange =
10291029
_fileSystem.ChangeHandler.NotifyPendingChange(WatcherChangeTypes.Renamed,
10301030
container.Type,
1031-
NotifyFilters.FileName,
1031+
ToNotifyFilters(container.Type),
10321032
destination,
10331033
source);
10341034
if (_containers.TryRemove(source, out IStorageContainer? sourceContainer))

Tests/Testably.Abstractions.Testing.Tests/FileSystem/FileSystemWatcherMockTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ public async Task RenamedEventArgs_ShouldUseDirectorySeparatorFromSimulatedFileS
299299
fileSystem.File.WriteAllText(expectedOldFullPath, "foo");
300300

301301
using IFileSystemWatcher fileSystemWatcher =
302-
fileSystem.FileSystemWatcher.New(parentDirectory);
302+
fileSystem.FileSystemWatcher.New(fileSystem.Path.GetFullPath(parentDirectory));
303303
using ManualResetEventSlim ms = new();
304304
fileSystemWatcher.Renamed += (_, eventArgs) =>
305305
{

Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/NotifyFiltersTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,89 @@ public async Task NotifyFilter_MoveFile_ShouldTriggerChangedEventOnNotifyFilters
611611
await That(result.OldName).IsEqualTo(FileSystem.Path.GetFileName(sourceName));
612612
}
613613

614+
[Theory]
615+
[InlineAutoData(NotifyFilters.DirectoryName)]
616+
public async Task NotifyFilter_MoveDirectory_ShouldTriggerChangedEventOnNotifyFilters(
617+
NotifyFilters notifyFilter, string sourceName, string destinationName)
618+
{
619+
SkipIfLongRunningTestsShouldBeSkipped();
620+
621+
FileSystem.Initialize();
622+
FileSystem.Directory.CreateDirectory(sourceName);
623+
RenamedEventArgs? result = null;
624+
using ManualResetEventSlim ms = new();
625+
using IFileSystemWatcher fileSystemWatcher =
626+
FileSystem.FileSystemWatcher.New(BasePath);
627+
fileSystemWatcher.Renamed += (_, eventArgs) =>
628+
{
629+
// ReSharper disable once AccessToDisposedClosure
630+
try
631+
{
632+
result = eventArgs;
633+
ms.Set();
634+
}
635+
catch (ObjectDisposedException)
636+
{
637+
// Ignore any ObjectDisposedException
638+
}
639+
};
640+
641+
fileSystemWatcher.NotifyFilter = notifyFilter;
642+
fileSystemWatcher.IncludeSubdirectories = true;
643+
fileSystemWatcher.EnableRaisingEvents = true;
644+
645+
FileSystem.Directory.Move(sourceName, destinationName);
646+
647+
await That(ms.Wait(ExpectSuccess, TestContext.Current.CancellationToken)).IsTrue();
648+
await That(result).IsNotNull();
649+
await That(result!.ChangeType).IsEqualTo(WatcherChangeTypes.Renamed);
650+
await That(result.FullPath).IsEqualTo(FileSystem.Path.GetFullPath(destinationName));
651+
await That(result.Name).IsEqualTo(FileSystem.Path.GetFileName(destinationName));
652+
await That(result.OldFullPath).IsEqualTo(FileSystem.Path.GetFullPath(sourceName));
653+
await That(result.OldName).IsEqualTo(FileSystem.Path.GetFileName(sourceName));
654+
}
655+
656+
[Theory]
657+
[InlineAutoData(NotifyFilters.DirectoryName)]
658+
public async Task NotifyFilter_MoveDirectoryOutOfTheWatchedDirectory_ShouldTriggerChangedEventOnNotifyFilters(
659+
NotifyFilters notifyFilter, string sourceName, string destinationName)
660+
{
661+
SkipIfLongRunningTestsShouldBeSkipped();
662+
663+
FileSystem.Initialize().WithSubdirectory("watched");
664+
var sourcePath = FileSystem.Path.Combine("watched", sourceName);
665+
FileSystem.Directory.CreateDirectory(sourcePath);
666+
FileSystemEventArgs? result = null;
667+
using ManualResetEventSlim ms = new();
668+
using IFileSystemWatcher fileSystemWatcher =
669+
FileSystem.FileSystemWatcher.New("watched");
670+
fileSystemWatcher.Deleted += (_, eventArgs) =>
671+
{
672+
// ReSharper disable once AccessToDisposedClosure
673+
try
674+
{
675+
result = eventArgs;
676+
ms.Set();
677+
}
678+
catch (ObjectDisposedException)
679+
{
680+
// Ignore any ObjectDisposedException
681+
}
682+
};
683+
684+
fileSystemWatcher.NotifyFilter = notifyFilter;
685+
fileSystemWatcher.IncludeSubdirectories = true;
686+
fileSystemWatcher.EnableRaisingEvents = true;
687+
688+
FileSystem.Directory.Move(sourcePath, destinationName);
689+
690+
await That(ms.Wait(ExpectSuccess, TestContext.Current.CancellationToken)).IsTrue();
691+
await That(result).IsNotNull();
692+
await That(result!.ChangeType).IsEqualTo(WatcherChangeTypes.Deleted);
693+
await That(result.FullPath).IsEqualTo(sourcePath);
694+
await That(result.Name).IsEqualTo(sourceName);
695+
}
696+
614697
[Theory]
615698
[AutoData]
616699
public async Task NotifyFilter_WriteFile_ShouldNotNotifyOnOtherFilters(string fileName)

0 commit comments

Comments
 (0)