diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs index 5b5e70d3..33068dec 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs @@ -30,6 +30,27 @@ internal sealed class FileSystemWatcherMock : Component, IFileSystemWatcher /// private const int BytesPerMessage = 128; + /// + /// Caches the full path of + /// + private string FullPath + { + get; + set + { + if (string.IsNullOrEmpty(value)) + { + field = value; + + return; + } + + string fullPath = GetNormalizedFullPath(value); + + field = fullPath; + } + } = string.Empty; + private CancellationTokenSource? _cancellationTokenSource; private IDisposable? _changeHandler; private readonly MockFileSystem _fileSystem; @@ -258,27 +279,6 @@ public ISynchronizeInvoke? SynchronizingObject } } - /// - /// Caches the full path of - /// - private string FullPath - { - get; - set - { - if (string.IsNullOrEmpty(value)) - { - field = value; - - return; - } - - string fullPath = GetNormalizedFullPath(value); - - field = fullPath; - } - } = string.Empty; - /// public void BeginInit() { @@ -355,6 +355,23 @@ public IWaitForChangedResult WaitForChanged( internal static FileSystemWatcherMock New(MockFileSystem fileSystem) => new(fileSystem); + private static void CheckRenamePremise(RenamedContext context) + { + Debug.Assert( + context is not { ComesFromOutside: true, GoesToInside: true }, + "The premise { ComesFromOutside: true, GoesToInside: true } should have been handled." + ); + + Debug.Assert( + context is not { ComesFromInside: true, GoesToInside: true }, + "The premise { ComesFromInside: true, GoesToInside: true } should have been handled." + ); + + Debug.Assert( + !context.GoesToOutside, "The premise { GoesToOutside: true } should have been handled." + ); + } + /// protected override void Dispose(bool disposing) { @@ -366,6 +383,70 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + private string GetNormalizedFullPath(string path) + { + string normalized = _fileSystem.Execute.Path.GetFullPath(path); + + return normalized.TrimEnd(_fileSystem.Execute.Path.DirectorySeparatorChar); + } + + private string? GetNormalizedParent(string? path) + { + if (path == null) + { + return null; + } + + string normalized = GetNormalizedFullPath(path); + + return _fileSystem.Execute.Path.GetDirectoryName(normalized) + ?.TrimEnd(_fileSystem.Execute.Path.DirectorySeparatorChar); + } + + /// + /// Counts the number of directory separators inside the relative path to + /// + /// + /// The number of directory separators inside the relative path to + /// Returns -1 if the path is outside the + private int GetSubDirectoryCount(string path) + { + string normalizedPath = GetNormalizedFullPath(path); + + if (!normalizedPath.StartsWith(FullPath, _fileSystem.Execute.StringComparisonMode)) + { + return -1; + } + + return normalizedPath.Substring(FullPath.Length) + .TrimStart(_fileSystem.Execute.Path.DirectorySeparatorChar) + .Count(c => c == _fileSystem.Execute.Path.DirectorySeparatorChar); + } + + private bool IsItemNameChange(ChangeDescription changeDescription) + { + string normalizedPath = GetNormalizedFullPath(changeDescription.Path); + string normalizedOldPath = GetNormalizedFullPath(changeDescription.OldPath!); + + string name = _fileSystem.Execute.Path.GetFileName(normalizedPath); + string oldName = _fileSystem.Execute.Path.GetFileName(normalizedOldPath); + + if (name.Equals(oldName, _fileSystem.Execute.StringComparisonMode)) + { + return false; + } + + if (name.Length == 0 || oldName.Length == 0) + { + return false; + } + + string? parent = _fileSystem.Execute.Path.GetDirectoryName(normalizedPath); + string? oldParent = _fileSystem.Execute.Path.GetDirectoryName(normalizedOldPath); + + return string.Equals(parent, oldParent, _fileSystem.Execute.StringComparisonMode); + } + private bool MatchesFilter(ChangeDescription changeDescription) { if (!MatchesWatcherPath(changeDescription.Path)) @@ -465,6 +546,50 @@ private void Restart() } } + private void SetFileSystemEventArgsFullPath(FileSystemEventArgs args, string name) + { + if (_fileSystem.SimulationMode == SimulationMode.Native) + { + return; + } + + string fullPath = _fileSystem.Execute.Path.Combine(Path, name); + + // FileSystemEventArgs implicitly combines the path in https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemEventArgs.cs + // HACK: The combination uses the system separator, so to simulate the behavior, we must override it using reflection! +#if NETFRAMEWORK + typeof(FileSystemEventArgs) + .GetField("fullPath", BindingFlags.Instance | BindingFlags.NonPublic)? + .SetValue(args, fullPath); +#else + typeof(FileSystemEventArgs) + .GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)? + .SetValue(args, fullPath); +#endif + } + + private void SetRenamedEventArgsFullPath(RenamedEventArgs args, string oldName) + { + if (_fileSystem.SimulationMode == SimulationMode.Native) + { + return; + } + + string fullPath = _fileSystem.Execute.Path.Combine(Path, oldName); + + // FileSystemEventArgs implicitly combines the path in https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemEventArgs.cs + // HACK: The combination uses the system separator, so to simulate the behavior, we must override it using reflection! +#if NETFRAMEWORK + typeof(RenamedEventArgs) + .GetField("oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)? + .SetValue(args, fullPath); +#else + typeof(RenamedEventArgs) + .GetField("_oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)? + .SetValue(args, fullPath); +#endif + } + private void Start() { if (_isInitializing) @@ -492,34 +617,45 @@ private void Start() InternalBufferSize, channelCapacity))); } }); - _ = Task.Run(() => + _ = Task.Run(async () => + { + try { - try + while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) { - while (!token.IsCancellationRequested) + while (reader.TryRead(out ChangeDescription? c)) { - if (reader.TryRead(out ChangeDescription? c)) - { - NotifyChange(c); - } + NotifyChange(c); } } - catch (Exception) + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + while (reader.TryRead(out ChangeDescription? c)) { - //Ignore any exception + NotifyChange(c); } - }, - token) - .ContinueWith(__ => - { - if (channel.Writer.TryComplete()) + } + catch (Exception) { + // Ignore any other exception + } + finally + { + channel.Writer.TryComplete(); + + while (reader.TryRead(out _)) + { + // Drain and ignore any remaining items so that Reader.Completion can complete deterministically + } + _ = channel.Reader.Completion.ContinueWith(_ => { cancellationTokenSource.Dispose(); }, CancellationToken.None); } - }, TaskScheduler.Default); + }, + token); } private void Stop() @@ -532,17 +668,37 @@ private void Stop() _changeHandler?.Dispose(); } - private void TriggerRenameNotification(ChangeDescription item) + private FileSystemEventArgs ToFileSystemEventArgs( + WatcherChangeTypes changeType, + string changePath) + { + string name = TransformPathAndName(changePath); + + FileSystemEventArgs eventArgs = new(changeType, Path, name); + + SetFileSystemEventArgsFullPath(eventArgs, name); + + return eventArgs; + } + + private string TransformPathAndName(string changeDescriptionPath) { - // Outside: Outside the FullPath - // Inside: FullPath/ - // Nested: FullPath/*/ - // Deep Nested: FullPath/*/**/ + return changeDescriptionPath.Substring(FullPath.Length) + .TrimStart(_fileSystem.Execute.Path.DirectorySeparatorChar); + } + /// + /// Triggers the appropriate , , or events + /// for a rename operation in the file system. Determines whether an item was moved into, out of, + /// or within the watched directory, and raises the correct event(s) based on the platform + /// (Windows, Linux, macOS) and the current watcher settings (such as ). + /// + /// The representing the rename operation. + private void TriggerRenameNotification(ChangeDescription item) + { bool comesFromOutside = !MatchesWatcherPath(item.OldPath, true); bool goesToInside = MatchesWatcherPath(item.Path, false); - // Outside -> Inside if (comesFromOutside && goesToInside) { Created?.Invoke(this, ToFileSystemEventArgs(WatcherChangeTypes.Created, item.Path)); @@ -553,7 +709,6 @@ private void TriggerRenameNotification(ChangeDescription item) bool comesFromInside = MatchesWatcherPath(item.OldPath, false); bool goesToOutside = !MatchesWatcherPath(item.Path, true); - // ... -> Outside if (goesToOutside && (comesFromInside || IncludeSubdirectories)) { Deleted?.Invoke(this, ToFileSystemEventArgs(WatcherChangeTypes.Deleted, item.OldPath!)); @@ -561,7 +716,6 @@ private void TriggerRenameNotification(ChangeDescription item) return; } - // Inside -> Inside if (comesFromInside && goesToInside) { if (TryMakeRenamedEventArgs(item, out RenamedEventArgs? eventArgs)) @@ -598,32 +752,173 @@ private void TriggerRenameNotification(ChangeDescription item) } } - #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high - private void TriggerWindowsRenameNotification(ChangeDescription item, RenamedContext context) + private bool TryMakeRenamedEventArgs( + ChangeDescription changeDescription, + [NotNullWhen(true)] out RenamedEventArgs? eventArgs + ) { - CheckRenamePremise(context); - - if (context.ComesFromOutside) + if (changeDescription.OldPath == null) { - if (IncludeSubdirectories) - { - FireCreated(); - } + eventArgs = null; + + return false; } - else if (context.ComesFromInside) - { - FireDeleted(); - if (IncludeSubdirectories) + string name = TransformPathAndName(changeDescription.Path); + + string oldName = TransformPathAndName(changeDescription.OldPath); + + eventArgs = new RenamedEventArgs(changeDescription.ChangeType, Path, name, oldName); + + SetFileSystemEventArgsFullPath(eventArgs, name); + SetRenamedEventArgsFullPath(eventArgs, oldName); + + return true; + } + + private IWaitForChangedResult WaitForChangedInternal( + WatcherChangeTypes changeType, TimeSpan timeout) + { + TaskCompletionSource + tcs = new(); + + void EventHandler(object? _, ChangeDescriptionEventArgs c) + { + if ((c.ChangeDescription.ChangeType & changeType) != 0) { - FireCreated(); + tcs.TrySetResult(new WaitForChangedResultMock( + c.ChangeDescription.ChangeType, + c.ChangeDescription.Name, + oldName: c.ChangeDescription.OldName, + timedOut: false)); } } - else if (context.ComesFromNested || context.ComesFromDeepNested) + + InternalEvent += EventHandler; + try { - if (context.GoesToInside) + bool wasEnabled = EnableRaisingEvents; + if (!wasEnabled) { - if (IncludeSubdirectories) + EnableRaisingEvents = true; + } + + #pragma warning disable MA0040 + tcs.Task.Wait(timeout); + #pragma warning restore MA0040 + EnableRaisingEvents = wasEnabled; + } + finally + { + InternalEvent -= EventHandler; + } + +#if NETFRAMEWORK + return tcs.Task.IsCompleted + ? tcs.Task.Result + : WaitForChangedResultMock.TimedOutResult; +#else + return tcs.Task.IsCompletedSuccessfully + ? tcs.Task.Result + : WaitForChangedResultMock.TimedOutResult; +#endif + } + + private struct WaitForChangedResultMock : IWaitForChangedResult + { + public WaitForChangedResultMock( + WatcherChangeTypes changeType, + string? name, + string? oldName, + bool timedOut) + { + ChangeType = changeType; + Name = name; + OldName = oldName; + TimedOut = timedOut; + } + + /// + /// The instance representing a timed out . + /// + public static readonly WaitForChangedResultMock TimedOutResult = + new(changeType: 0, name: null, oldName: null, timedOut: true); + + /// + public WatcherChangeTypes ChangeType { get; } + + /// + public string? Name { get; } + + /// + public string? OldName { get; } + + /// + public bool TimedOut { get; } + } + + [StructLayout(LayoutKind.Auto)] + private readonly struct RenamedContext( + bool comesFromOutside, + bool comesFromInside, + bool goesToInside, + bool goesToOutside, + int oldSubDirectoryCount + ) + { + private const int NestedLevelCount = 1; + + public bool ComesFromOutside { get; } = comesFromOutside; + + public bool ComesFromInside { get; } = comesFromInside; + + public bool GoesToInside { get; } = goesToInside; + + public bool GoesToOutside { get; } = goesToOutside; + + /// + /// If this is then is + /// + public bool ComesFromNested { get; } = oldSubDirectoryCount == NestedLevelCount; + + /// + /// If this is then is + /// + public bool ComesFromDeepNested { get; } = oldSubDirectoryCount > NestedLevelCount; + } + + internal sealed class ChangeDescriptionEventArgs(ChangeDescription changeDescription) + : EventArgs + { + public ChangeDescription ChangeDescription { get; } = changeDescription; + } + + #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high + private void TriggerWindowsRenameNotification(ChangeDescription item, RenamedContext context) + { + CheckRenamePremise(context); + + if (context.ComesFromOutside) + { + if (IncludeSubdirectories) + { + FireCreated(); + } + } + else if (context.ComesFromInside) + { + FireDeleted(); + + if (IncludeSubdirectories) + { + FireCreated(); + } + } + else if (context.ComesFromNested || context.ComesFromDeepNested) + { + if (context.GoesToInside) + { + if (IncludeSubdirectories) { FireDeleted(); } @@ -719,289 +1014,4 @@ private void TriggerLinuxRenameNotification(ChangeDescription item, RenamedConte } } #pragma warning restore S3776 // Cognitive Complexity of methods should not be too high - - private static void CheckRenamePremise(RenamedContext context) - { - Debug.Assert( - context is not { ComesFromOutside: true, GoesToInside: true }, - "The premise { ComesFromOutside: true, GoesToInside: true } should have been handled." - ); - - Debug.Assert( - context is not { ComesFromInside: true, GoesToInside: true }, - "The premise { ComesFromInside: true, GoesToInside: true } should have been handled." - ); - - Debug.Assert( - !context.GoesToOutside, "The premise { GoesToOutside: true } should have been handled." - ); - } - - private string? GetNormalizedParent(string? path) - { - if (path == null) - { - return null; - } - - string normalized = GetNormalizedFullPath(path); - - return _fileSystem.Execute.Path.GetDirectoryName(normalized) - ?.TrimEnd(_fileSystem.Execute.Path.DirectorySeparatorChar); - } - - private string GetNormalizedFullPath(string path) - { - string normalized = _fileSystem.Execute.Path.GetFullPath(path); - - return normalized.TrimEnd(_fileSystem.Execute.Path.DirectorySeparatorChar); - } - - /// - /// Counts the number of directory separators inside the relative path to - /// - /// - /// The number of directory separators inside the relative path to - /// Returns -1 if the path is outside the - private int GetSubDirectoryCount(string path) - { - string normalizedPath = GetNormalizedFullPath(path); - - if (!normalizedPath.StartsWith(FullPath, _fileSystem.Execute.StringComparisonMode)) - { - return -1; - } - - return normalizedPath.Substring(FullPath.Length) - .TrimStart(_fileSystem.Execute.Path.DirectorySeparatorChar) - .Count(c => c == _fileSystem.Execute.Path.DirectorySeparatorChar); - } - - private bool IsItemNameChange(ChangeDescription changeDescription) - { - string normalizedPath = GetNormalizedFullPath(changeDescription.Path); - string normalizedOldPath = GetNormalizedFullPath(changeDescription.OldPath!); - - string name = _fileSystem.Execute.Path.GetFileName(normalizedPath); - string oldName = _fileSystem.Execute.Path.GetFileName(normalizedOldPath); - - if (name.Equals(oldName, _fileSystem.Execute.StringComparisonMode)) - { - return false; - } - - if (name.Length == 0 || oldName.Length == 0) - { - return false; - } - - string? parent = _fileSystem.Execute.Path.GetDirectoryName(normalizedPath); - string? oldParent = _fileSystem.Execute.Path.GetDirectoryName(normalizedOldPath); - - return string.Equals(parent, oldParent, _fileSystem.Execute.StringComparisonMode); - } - - private bool TryMakeRenamedEventArgs( - ChangeDescription changeDescription, - [NotNullWhen(true)] out RenamedEventArgs? eventArgs - ) - { - if (changeDescription.OldPath == null) - { - eventArgs = null; - - return false; - } - - string name = TransformPathAndName(changeDescription.Path); - - string oldName = TransformPathAndName(changeDescription.OldPath); - - eventArgs = new RenamedEventArgs(changeDescription.ChangeType, Path, name, oldName); - - SetFileSystemEventArgsFullPath(eventArgs, name); - SetRenamedEventArgsFullPath(eventArgs, oldName); - - return true; - } - - private FileSystemEventArgs ToFileSystemEventArgs( - WatcherChangeTypes changeType, - string changePath) - { - string name = TransformPathAndName(changePath); - - FileSystemEventArgs eventArgs = new(changeType, Path, name); - - SetFileSystemEventArgsFullPath(eventArgs, name); - - return eventArgs; - } - - private string TransformPathAndName(string changeDescriptionPath) - { - return changeDescriptionPath.Substring(FullPath.Length) - .TrimStart(_fileSystem.Execute.Path.DirectorySeparatorChar); - } - - private void SetFileSystemEventArgsFullPath(FileSystemEventArgs args, string name) - { - if (_fileSystem.SimulationMode == SimulationMode.Native) - { - return; - } - - string fullPath = _fileSystem.Execute.Path.Combine(Path, name); - - // FileSystemEventArgs implicitly combines the path in https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemEventArgs.cs - // HACK: The combination uses the system separator, so to simulate the behavior, we must override it using reflection! -#if NETFRAMEWORK - typeof(FileSystemEventArgs) - .GetField("fullPath", BindingFlags.Instance | BindingFlags.NonPublic)? - .SetValue(args, fullPath); -#else - typeof(FileSystemEventArgs) - .GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)? - .SetValue(args, fullPath); -#endif - } - - private void SetRenamedEventArgsFullPath(RenamedEventArgs args, string oldName) - { - if (_fileSystem.SimulationMode == SimulationMode.Native) - { - return; - } - - string fullPath = _fileSystem.Execute.Path.Combine(Path, oldName); - - // FileSystemEventArgs implicitly combines the path in https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemEventArgs.cs - // HACK: The combination uses the system separator, so to simulate the behavior, we must override it using reflection! -#if NETFRAMEWORK - typeof(RenamedEventArgs) - .GetField("oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)? - .SetValue(args, fullPath); -#else - typeof(RenamedEventArgs) - .GetField("_oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)? - .SetValue(args, fullPath); -#endif - } - - private IWaitForChangedResult WaitForChangedInternal( - WatcherChangeTypes changeType, TimeSpan timeout) - { - TaskCompletionSource - tcs = new(); - - void EventHandler(object? _, ChangeDescriptionEventArgs c) - { - if ((c.ChangeDescription.ChangeType & changeType) != 0) - { - tcs.TrySetResult(new WaitForChangedResultMock( - c.ChangeDescription.ChangeType, - c.ChangeDescription.Name, - oldName: c.ChangeDescription.OldName, - timedOut: false)); - } - } - - InternalEvent += EventHandler; - try - { - bool wasEnabled = EnableRaisingEvents; - if (!wasEnabled) - { - EnableRaisingEvents = true; - } - - #pragma warning disable MA0040 - tcs.Task.Wait(timeout); - #pragma warning restore MA0040 - EnableRaisingEvents = wasEnabled; - } - finally - { - InternalEvent -= EventHandler; - } - -#if NETFRAMEWORK - return tcs.Task.IsCompleted - ? tcs.Task.Result - : WaitForChangedResultMock.TimedOutResult; -#else - return tcs.Task.IsCompletedSuccessfully - ? tcs.Task.Result - : WaitForChangedResultMock.TimedOutResult; -#endif - } - - private struct WaitForChangedResultMock : IWaitForChangedResult - { - public WaitForChangedResultMock( - WatcherChangeTypes changeType, - string? name, - string? oldName, - bool timedOut) - { - ChangeType = changeType; - Name = name; - OldName = oldName; - TimedOut = timedOut; - } - - /// - /// The instance representing a timed out . - /// - public static readonly WaitForChangedResultMock TimedOutResult = - new(changeType: 0, name: null, oldName: null, timedOut: true); - - /// - public WatcherChangeTypes ChangeType { get; } - - /// - public string? Name { get; } - - /// - public string? OldName { get; } - - /// - public bool TimedOut { get; } - } - - [StructLayout(LayoutKind.Auto)] - private readonly struct RenamedContext( - bool comesFromOutside, - bool comesFromInside, - bool goesToInside, - bool goesToOutside, - int oldSubDirectoryCount - ) - { - private const int NestedLevelCount = 1; - - public bool ComesFromOutside { get; } = comesFromOutside; - - public bool ComesFromInside { get; } = comesFromInside; - - public bool GoesToInside { get; } = goesToInside; - - public bool GoesToOutside { get; } = goesToOutside; - - /// - /// If this is then is - /// - public bool ComesFromNested { get; } = oldSubDirectoryCount == NestedLevelCount; - - /// - /// If this is then is - /// - public bool ComesFromDeepNested { get; } = oldSubDirectoryCount > NestedLevelCount; - } - - internal sealed class ChangeDescriptionEventArgs(ChangeDescription changeDescription) - : EventArgs - { - public ChangeDescription ChangeDescription { get; } = changeDescription; - } } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/EventTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/EventTests.cs index 8ab470e3..265a5aee 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/EventTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/EventTests.cs @@ -7,7 +7,6 @@ namespace Testably.Abstractions.Tests.FileSystem.FileSystemWatcher; [FileSystemTests] -[NotInParallel(nameof(EventTests))] public class EventTests(FileSystemTestData testData) : FileSystemTestBase(testData) { [Test] diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Unix.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Unix.cs index 96e23930..34a2b800 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Unix.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Unix.cs @@ -63,7 +63,7 @@ out ConcurrentBag createdBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(deletedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -147,10 +147,10 @@ out ConcurrentBag deletedBag // Assert - await That(deletedMs.Wait(ExpectTimeout, CancellationToken)) + await That(deletedMs.Wait(!isRenamed ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(!isRenamed); - await That(renamedMs.Wait(ExpectTimeout, CancellationToken)) + await That(renamedMs.Wait(isRenamed ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(isRenamed); await That(changedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -255,10 +255,10 @@ out ConcurrentBag createdBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(isCreated ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(isCreated); - await That(renamedMs.Wait(ExpectTimeout, CancellationToken)) + await That(renamedMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(changedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -360,10 +360,10 @@ out ConcurrentBag createdBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(isCreated ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(isCreated); - await That(renamedMs.Wait(ExpectTimeout, CancellationToken)) + await That(renamedMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(changedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Windows.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Windows.cs index 74d2d796..cc555a7a 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Windows.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.Windows.cs @@ -59,7 +59,7 @@ out ConcurrentBag createdBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(deletedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -133,9 +133,9 @@ out ConcurrentBag createdBag // Assert - await That(deletedMs.Wait(ExpectTimeout, CancellationToken)).IsTrue(); + await That(deletedMs.Wait(ExpectSuccess, CancellationToken)).IsTrue(); - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(renamedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -229,10 +229,10 @@ out ConcurrentBag deletedBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(isCreated ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(isCreated); - await That(deletedMs.Wait(ExpectTimeout, CancellationToken)) + await That(deletedMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(renamedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -333,10 +333,10 @@ out ConcurrentBag deletedBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)) + await That(createdMs.Wait(isCreated ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(isCreated); - await That(deletedMs.Wait(ExpectTimeout, CancellationToken)) + await That(deletedMs.Wait(includeSubdirectories ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(includeSubdirectories); await That(renamedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.cs index 7e7e7bf9..e59679be 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileSystemWatcher/MoveTests.cs @@ -6,8 +6,6 @@ namespace Testably.Abstractions.Tests.FileSystem.FileSystemWatcher; [FileSystemTests] -// TODO #956: Investigate, why these tests are not stable when run in parallel -[NotInParallel(nameof(MoveTests))] public partial class MoveTests(FileSystemTestData testData) : FileSystemTestBase(testData) { [Test] @@ -51,7 +49,7 @@ out ConcurrentBag createdBag // Assert - await That(createdMs.Wait(ExpectTimeout, CancellationToken)).IsTrue(); + await That(createdMs.Wait(ExpectSuccess, CancellationToken)).IsTrue(); await That(deletedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); await That(renamedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -122,7 +120,7 @@ out ConcurrentBag deletedBag // Assert - await That(deletedMs.Wait(ExpectTimeout, CancellationToken)) + await That(deletedMs.Wait(shouldInvokeDeleted ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(shouldInvokeDeleted); await That(createdMs.Wait(ExpectTimeout, CancellationToken)).IsFalse(); @@ -206,7 +204,7 @@ params string[] paths // Assert - await That(renamedMs.Wait(ExpectTimeout, CancellationToken)) + await That(renamedMs.Wait(shouldInvokeRenamed ? ExpectSuccess : ExpectTimeout, CancellationToken)) .IsEqualTo(shouldInvokeRenamed); await That(deletedMs.Wait(ExpectTimeout, CancellationToken)).IsFalse();