Skip to content

Commit 1ce0ca5

Browse files
pw-sgrvbreuss
andauthored
fix: fixed FileSystemWatcherMock dropping sub directories (#900)
* fix: fixed FileSystemWatcherMock dropping sub directories * tests: fixed tests in FileSystemWatcherMockTests.EventArgsTests * test: split IncludeSubdirectories_SetToTrue_ArgsNameShouldContainRelativePath for better control * Fix failing test on .NET Framework --------- Co-authored-by: Valentin Breuß <[email protected]>
1 parent 256d5b2 commit 1ce0ca5

File tree

3 files changed

+410
-120
lines changed

3 files changed

+410
-120
lines changed

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

Lines changed: 101 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal sealed class FileSystemWatcherMock : Component, IFileSystemWatcher
3838
NotifyFilters.LastWrite;
3939

4040
private string _path = string.Empty;
41+
private string _fullPath = string.Empty;
4142

4243
private ISynchronizeInvoke? _synchronizingObject;
4344

@@ -213,6 +214,7 @@ public string Path
213214
}
214215

215216
_path = value;
217+
FullPath = _path;
216218
}
217219
}
218220

@@ -258,6 +260,32 @@ public ISynchronizeInvoke? SynchronizingObject
258260
}
259261
}
260262

263+
/// <summary>
264+
/// Caches the full path of <see cref="Path"/>
265+
/// </summary>
266+
private string FullPath
267+
{
268+
get => _fullPath;
269+
set
270+
{
271+
if (string.IsNullOrEmpty(value))
272+
{
273+
_fullPath = value;
274+
275+
return;
276+
}
277+
278+
string fullPath = _fileSystem.Path.GetFullPath(value);
279+
280+
if (!fullPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar))
281+
{
282+
fullPath += _fileSystem.Path.DirectorySeparatorChar;
283+
}
284+
285+
_fullPath = fullPath;
286+
}
287+
}
288+
261289
/// <inheritdoc cref="IFileSystemWatcher.BeginInit()" />
262290
public void BeginInit()
263291
{
@@ -399,19 +427,19 @@ private void NotifyChange(ChangeDescription item)
399427
if (item.ChangeType.HasFlag(WatcherChangeTypes.Created))
400428
{
401429
Created?.Invoke(this, ToFileSystemEventArgs(
402-
item.ChangeType, item.Path, item.Name));
430+
item.ChangeType, item.Path));
403431
}
404432

405433
if (item.ChangeType.HasFlag(WatcherChangeTypes.Deleted))
406434
{
407435
Deleted?.Invoke(this, ToFileSystemEventArgs(
408-
item.ChangeType, item.Path, item.Name));
436+
item.ChangeType, item.Path));
409437
}
410438

411439
if (item.ChangeType.HasFlag(WatcherChangeTypes.Changed))
412440
{
413441
Changed?.Invoke(this, ToFileSystemEventArgs(
414-
item.ChangeType, item.Path, item.Name));
442+
item.ChangeType, item.Path));
415443
}
416444

417445
if (item.ChangeType.HasFlag(WatcherChangeTypes.Renamed))
@@ -502,68 +530,6 @@ private void Stop()
502530
_changeHandler?.Dispose();
503531
}
504532

505-
private FileSystemEventArgs ToFileSystemEventArgs(
506-
WatcherChangeTypes changeType,
507-
string changePath,
508-
string? changeName)
509-
{
510-
string path = TransformPathAndName(
511-
changePath,
512-
changeName,
513-
out string name);
514-
515-
FileSystemEventArgs eventArgs = new(changeType, Path, name);
516-
if (_fileSystem.SimulationMode != SimulationMode.Native)
517-
{
518-
// 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
519-
// HACK: Have to resort to Reflection to override this behavior!
520-
#if NETFRAMEWORK
521-
typeof(FileSystemEventArgs)
522-
.GetField("fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
523-
.SetValue(eventArgs, path);
524-
#else
525-
typeof(FileSystemEventArgs)
526-
.GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
527-
.SetValue(eventArgs, path);
528-
#endif
529-
}
530-
531-
return eventArgs;
532-
}
533-
534-
private string TransformPathAndName(
535-
string changeDescriptionPath,
536-
string? changeDescriptionName,
537-
out string name)
538-
{
539-
string? transformedName = changeDescriptionName;
540-
string? path = changeDescriptionPath;
541-
if (!_fileSystem.Path.IsPathRooted(Path))
542-
{
543-
string rootedWatchedPath = _fileSystem.Directory.GetCurrentDirectory();
544-
if (!rootedWatchedPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar))
545-
{
546-
rootedWatchedPath += _fileSystem.Path.DirectorySeparatorChar;
547-
}
548-
549-
if (path.StartsWith(rootedWatchedPath, _fileSystem.Execute.StringComparisonMode))
550-
{
551-
path = path.Substring(rootedWatchedPath.Length);
552-
}
553-
554-
transformedName = _fileSystem.Execute.Path.GetFileName(changeDescriptionPath);
555-
}
556-
else if (transformedName == null ||
557-
_fileSystem.Execute.Path.IsPathRooted(changeDescriptionName))
558-
{
559-
transformedName = _fileSystem.Execute.Path.GetFileName(changeDescriptionPath);
560-
}
561-
562-
name = transformedName;
563-
564-
return path ?? "";
565-
}
566-
567533
private void TriggerRenameNotification(ChangeDescription item)
568534
{
569535
if (_fileSystem.Execute.IsWindows)
@@ -578,13 +544,13 @@ private void TriggerRenameNotification(ChangeDescription item)
578544
if (MatchesWatcherPath(item.OldPath))
579545
{
580546
Deleted?.Invoke(this, ToFileSystemEventArgs(
581-
WatcherChangeTypes.Deleted, item.OldPath, item.OldName));
547+
WatcherChangeTypes.Deleted, item.OldPath));
582548
}
583549

584550
if (MatchesWatcherPath(item.Path))
585551
{
586552
Created?.Invoke(this, ToFileSystemEventArgs(
587-
WatcherChangeTypes.Created, item.Path, item.Name));
553+
WatcherChangeTypes.Created, item.Path));
588554
}
589555
}
590556
}
@@ -601,54 +567,92 @@ private void TriggerRenameNotification(ChangeDescription item)
601567

602568
private bool TryMakeRenamedEventArgs(
603569
ChangeDescription changeDescription,
604-
[NotNullWhen(true)] out RenamedEventArgs? eventArgs)
570+
[NotNullWhen(true)] out RenamedEventArgs? eventArgs
571+
)
605572
{
606573
if (changeDescription.OldPath == null)
607574
{
608575
eventArgs = null;
576+
609577
return false;
610578
}
611579

612-
string path = TransformPathAndName(
613-
changeDescription.Path,
614-
changeDescription.Name,
615-
out string name);
580+
string name = TransformPathAndName(changeDescription.Path);
581+
582+
string oldName = TransformPathAndName(changeDescription.OldPath);
583+
584+
eventArgs = new RenamedEventArgs(changeDescription.ChangeType, Path, name, oldName);
585+
586+
SetFileSystemEventArgsFullPath(eventArgs, name);
587+
SetRenamedEventArgsFullPath(eventArgs, oldName);
616588

617-
string oldPath = TransformPathAndName(
618-
changeDescription.OldPath,
619-
changeDescription.OldName,
620-
out string oldName);
589+
return _fileSystem.Execute.Path.GetDirectoryName(changeDescription.Path)?.Equals(
590+
_fileSystem.Execute.Path.GetDirectoryName(changeDescription.OldPath),
591+
_fileSystem.Execute.StringComparisonMode
592+
)
593+
?? true;
594+
}
595+
596+
private FileSystemEventArgs ToFileSystemEventArgs(
597+
WatcherChangeTypes changeType,
598+
string changePath)
599+
{
600+
string name = TransformPathAndName(changePath);
601+
602+
FileSystemEventArgs eventArgs = new(changeType, Path, name);
603+
604+
SetFileSystemEventArgsFullPath(eventArgs, name);
605+
606+
return eventArgs;
607+
}
621608

622-
eventArgs = new RenamedEventArgs(
623-
changeDescription.ChangeType,
624-
Path,
625-
name,
626-
oldName);
609+
private string TransformPathAndName(string changeDescriptionPath)
610+
{
611+
return changeDescriptionPath.Substring(FullPath.Length).TrimStart(_fileSystem.Path.DirectorySeparatorChar);
612+
}
627613

628-
if (_fileSystem.SimulationMode != SimulationMode.Native)
614+
private void SetFileSystemEventArgsFullPath(FileSystemEventArgs args, string name)
615+
{
616+
if (_fileSystem.SimulationMode == SimulationMode.Native)
629617
{
630-
// RenamedEventArgs implicitly combines the path in https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/RenamedEventArgs.cs
631-
// HACK: Have to resort to Reflection to override this behavior!
618+
return;
619+
}
620+
621+
string fullPath = _fileSystem.Path.Combine(Path, name);
622+
623+
// 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
624+
// HACK: The combination uses the system separator, so to simulate the behavior, we must override it using reflection!
632625
#if NETFRAMEWORK
633626
typeof(FileSystemEventArgs)
634627
.GetField("fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
635-
.SetValue(eventArgs, path);
628+
.SetValue(args, fullPath);
629+
#else
630+
typeof(FileSystemEventArgs)
631+
.GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
632+
.SetValue(args, fullPath);
633+
#endif
634+
}
635+
636+
private void SetRenamedEventArgsFullPath(RenamedEventArgs args, string oldName)
637+
{
638+
if (_fileSystem.SimulationMode == SimulationMode.Native)
639+
{
640+
return;
641+
}
642+
643+
string fullPath = _fileSystem.Path.Combine(Path, oldName);
644+
645+
// 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
646+
// HACK: The combination uses the system separator, so to simulate the behavior, we must override it using reflection!
647+
#if NETFRAMEWORK
636648
typeof(RenamedEventArgs)
637649
.GetField("oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
638-
.SetValue(eventArgs, oldPath);
650+
.SetValue(args, fullPath);
639651
#else
640-
typeof(FileSystemEventArgs)
641-
.GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
642-
.SetValue(eventArgs, path);
643-
typeof(RenamedEventArgs)
644-
.GetField("_oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
645-
.SetValue(eventArgs, oldPath);
652+
typeof(RenamedEventArgs)
653+
.GetField("_oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
654+
.SetValue(args, fullPath);
646655
#endif
647-
}
648-
649-
return _fileSystem.Execute.Path.GetDirectoryName(changeDescription.Path)?
650-
.Equals(_fileSystem.Execute.Path.GetDirectoryName(changeDescription.OldPath),
651-
_fileSystem.Execute.StringComparisonMode) ?? true;
652656
}
653657

654658
private IWaitForChangedResult WaitForChangedInternal(

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ public async Task FileSystemEventArgs_ShouldUseDirectorySeparatorFromSimulatedFi
249249
FileSystemEventArgs? result = null;
250250
string expectedFullPath = fileSystem.Path.GetFullPath(
251251
fileSystem.Path.Combine(parentDirectory, directoryName));
252-
string expectedName = fileSystem.Path.Combine(parentDirectory, directoryName);
253252

254253
using IFileSystemWatcher fileSystemWatcher =
255254
fileSystem.FileSystemWatcher.New(fileSystem.Path.GetFullPath(parentDirectory));
@@ -269,12 +268,12 @@ public async Task FileSystemEventArgs_ShouldUseDirectorySeparatorFromSimulatedFi
269268
};
270269
fileSystemWatcher.NotifyFilter = NotifyFilters.DirectoryName;
271270
fileSystemWatcher.EnableRaisingEvents = true;
272-
fileSystem.Directory.CreateDirectory(expectedName);
271+
fileSystem.Directory.CreateDirectory(expectedFullPath);
273272
ms.Wait(5000, TestContext.Current.CancellationToken);
274273

275274
await That(result).IsNotNull();
276275
await That(result!.FullPath).IsEqualTo(expectedFullPath);
277-
await That(result.Name).IsEqualTo(expectedName);
276+
await That(result.Name).IsEqualTo(directoryName);
278277
await That(result.ChangeType).IsEqualTo(WatcherChangeTypes.Created);
279278
}
280279
#endif

0 commit comments

Comments
 (0)