From 252a54cdb4aff8b45363b9c9ff9750cba4264484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 7 Aug 2026 18:43:37 +0200 Subject: [PATCH] fix: support combined `FileSystemTypes` flags in `Notify` and `Intercept` `ChangeDescriptionExtensions.Matches` had the `HasFlag` operands swapped: it asked whether the changed entry's single `FileSystemType` contained the filter, instead of whether the filter contained the entry's type. Because `ChangeDescription.FileSystemType` always carries exactly one flag, a subscription for `FileSystemTypes.DirectoryOrFile` (or `Directory | File`) never matched any event and silently received nothing. Affects `OnChanged`, `OnCreated` and `OnDeleted` on both the notification and the interception handler. `changeType` is now compared with `HasFlag` as well, so both flags enumerations follow the same any-of semantics that the XML docs describe. --- - *Fixes #1067* --- .../Helpers/ChangeDescriptionExtensions.cs | 6 +- .../ChangeDescriptionExtensionsTests.cs | 55 +++++++++++++++++++ .../InterceptionHandlerExtensionsTests.cs | 36 ++++++++++++ ...nHandlerExtensionsTests.FileSystemTests.cs | 41 ++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 Tests/Testably.Abstractions.Testing.Tests/Helpers/ChangeDescriptionExtensionsTests.cs diff --git a/Source/Testably.Abstractions.Testing/Helpers/ChangeDescriptionExtensions.cs b/Source/Testably.Abstractions.Testing/Helpers/ChangeDescriptionExtensions.cs index e924ed4ca..53b404beb 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/ChangeDescriptionExtensions.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/ChangeDescriptionExtensions.cs @@ -17,7 +17,7 @@ internal static class ChangeDescriptionExtensions /// The change description. /// The execution engine simulation the underlying operating system. /// The must have any of the provided flags. - /// The must match this type. + /// The must have any of the provided flags. /// The must match this glob pattern. /// (optional) If provided, additional filter criteria can be implemented via this predicate. /// @@ -31,8 +31,8 @@ internal static bool Matches(this ChangeDescription changeDescription, string globPattern, Func? predicate = null) { - if (changeDescription.ChangeType != changeType || - !changeDescription.FileSystemType.HasFlag(fileSystemType)) + if (!changeType.HasFlag(changeDescription.ChangeType) || + !fileSystemType.HasFlag(changeDescription.FileSystemType)) { return false; } diff --git a/Tests/Testably.Abstractions.Testing.Tests/Helpers/ChangeDescriptionExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/Helpers/ChangeDescriptionExtensionsTests.cs new file mode 100644 index 000000000..c735ceecb --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/Helpers/ChangeDescriptionExtensionsTests.cs @@ -0,0 +1,55 @@ +using System.IO; +using Testably.Abstractions.Testing.FileSystem; +using Testably.Abstractions.Testing.Helpers; + +namespace Testably.Abstractions.Testing.Tests.Helpers; + +public class ChangeDescriptionExtensionsTests +{ + [Test] + [Arguments(WatcherChangeTypes.Created, WatcherChangeTypes.Created, true)] + [Arguments(WatcherChangeTypes.Created, WatcherChangeTypes.Changed, false)] + [Arguments(WatcherChangeTypes.Created, WatcherChangeTypes.Created | WatcherChangeTypes.Deleted, + true)] + [Arguments(WatcherChangeTypes.Deleted, WatcherChangeTypes.Created | WatcherChangeTypes.Deleted, + true)] + [Arguments(WatcherChangeTypes.Changed, WatcherChangeTypes.Created | WatcherChangeTypes.Deleted, + false)] + public async Task Matches_ChangeType_ShouldSupportCombinedFlags( + WatcherChangeTypes changeType, WatcherChangeTypes filter, bool expectedResult) + { + MockFileSystem fileSystem = new(); + ChangeDescription changeDescription = + CreateChangeDescription(fileSystem, changeType, FileSystemTypes.File); + + bool result = changeDescription.Matches( + fileSystem.Execute, FileSystemTypes.DirectoryOrFile, filter, "*"); + + await That(result).IsEqualTo(expectedResult); + } + + [Test] + [Arguments(FileSystemTypes.Directory, FileSystemTypes.Directory, true)] + [Arguments(FileSystemTypes.Directory, FileSystemTypes.File, false)] + [Arguments(FileSystemTypes.Directory, FileSystemTypes.DirectoryOrFile, true)] + [Arguments(FileSystemTypes.File, FileSystemTypes.Directory, false)] + [Arguments(FileSystemTypes.File, FileSystemTypes.File, true)] + [Arguments(FileSystemTypes.File, FileSystemTypes.DirectoryOrFile, true)] + public async Task Matches_FileSystemType_ShouldSupportCombinedFlags( + FileSystemTypes fileSystemType, FileSystemTypes filter, bool expectedResult) + { + MockFileSystem fileSystem = new(); + ChangeDescription changeDescription = + CreateChangeDescription(fileSystem, WatcherChangeTypes.Created, fileSystemType); + + bool result = changeDescription.Matches( + fileSystem.Execute, filter, WatcherChangeTypes.Created, "*"); + + await That(result).IsEqualTo(expectedResult); + } + + private static ChangeDescription CreateChangeDescription(MockFileSystem fileSystem, + WatcherChangeTypes changeType, FileSystemTypes fileSystemType) + => new(changeType, fileSystemType, NotifyFilters.FileName, + fileSystem.Storage.GetLocation("foo"), null); +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/InterceptionHandlerExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/InterceptionHandlerExtensionsTests.cs index 2bcba2075..4ea3e38b9 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/InterceptionHandlerExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/InterceptionHandlerExtensionsTests.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Testably.Abstractions.Testing.Tests; public class InterceptionHandlerExtensionsTests @@ -216,6 +218,22 @@ public async Task Creating_Directory_ShouldUsePredicate(bool expectedResult, str await That(isNotified).IsEqualTo(expectedResult); } + [Test] + [AutoArguments] + public async Task Creating_DirectoryOrFile_ShouldInterceptDirectoriesAndFiles( + string directoryPath, string filePath) + { + List interceptedTypes = []; + FileSystem.Intercept.Creating(FileSystemTypes.DirectoryOrFile, + c => interceptedTypes.Add(c.FileSystemType)); + + FileSystem.Directory.CreateDirectory(directoryPath); + FileSystem.File.WriteAllText(filePath, null); + + await That(interceptedTypes).Contains(FileSystemTypes.Directory); + await That(interceptedTypes).Contains(FileSystemTypes.File); + } + [Test] [AutoArguments] public async Task Creating_File_OtherEvent_ShouldNotTrigger( @@ -423,6 +441,24 @@ public async Task Deleting_Directory_ShouldUsePredicate(bool expectedResult, str await That(isNotified).IsEqualTo(expectedResult); } + [Test] + [AutoArguments] + public async Task Deleting_DirectoryOrFile_ShouldInterceptDirectoriesAndFiles( + string directoryPath, string filePath) + { + List interceptedTypes = []; + FileSystem.Directory.CreateDirectory(directoryPath); + FileSystem.File.WriteAllText(filePath, null); + FileSystem.Intercept.Deleting(FileSystemTypes.DirectoryOrFile, + c => interceptedTypes.Add(c.FileSystemType)); + + FileSystem.Directory.Delete(directoryPath); + FileSystem.File.Delete(filePath); + + await That(interceptedTypes).Contains(FileSystemTypes.Directory); + await That(interceptedTypes).Contains(FileSystemTypes.File); + } + [Test] [AutoArguments] public async Task Deleting_File_OtherEvent_ShouldNotTrigger( diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.FileSystemTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.FileSystemTests.cs index ccd37d278..3711cad13 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.FileSystemTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.FileSystemTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Testably.Abstractions.Testing.FileSystem; namespace Testably.Abstractions.Testing.Tests; @@ -257,6 +258,25 @@ public async Task OnCreated_Directory_ShouldUsePredicate(bool expectedResult, st await That(isNotified).IsEqualTo(expectedResult); } + [Test] + [AutoArguments] + public async Task OnCreated_DirectoryOrFile_ShouldNotifyForDirectoriesAndFiles( + string directoryPath, string filePath) + { + List notifiedTypes = []; + + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.DirectoryOrFile, + c => notifiedTypes.Add(c.FileSystemType)); + FileSystem.Directory.CreateDirectory(directoryPath); + FileSystem.File.WriteAllText(filePath, null); + + onCreated.Wait(count: 2); + + await That(notifiedTypes).Contains(FileSystemTypes.Directory); + await That(notifiedTypes).Contains(FileSystemTypes.File); + } + [Test] [AutoArguments] public async Task OnCreated_File_OtherEvent_ShouldNotTrigger(string path) @@ -507,6 +527,27 @@ public async Task OnDeleted_Directory_ShouldUsePredicate(bool expectedResult, st await That(isNotified).IsEqualTo(expectedResult); } + [Test] + [AutoArguments] + public async Task OnDeleted_DirectoryOrFile_ShouldNotifyForDirectoriesAndFiles( + string directoryPath, string filePath) + { + List notifiedTypes = []; + FileSystem.Directory.CreateDirectory(directoryPath); + FileSystem.File.WriteAllText(filePath, null); + + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.DirectoryOrFile, + c => notifiedTypes.Add(c.FileSystemType)); + FileSystem.Directory.Delete(directoryPath); + FileSystem.File.Delete(filePath); + + onDeleted.Wait(count: 2); + + await That(notifiedTypes).Contains(FileSystemTypes.Directory); + await That(notifiedTypes).Contains(FileSystemTypes.File); + } + [Test] [AutoArguments] public async Task OnDeleted_File_OtherEvent_ShouldNotTrigger(string path)