Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix: fixed FileSystemWatcherMock dropping sub directories
  • Loading branch information
pw-sgr committed Nov 25, 2025
commit ee5e78ecc136c70c0e77c73f0863c8f881087956
198 changes: 101 additions & 97 deletions Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal sealed class FileSystemWatcherMock : Component, IFileSystemWatcher
NotifyFilters.LastWrite;

private string _path = string.Empty;
private string _fullPath = string.Empty;

private ISynchronizeInvoke? _synchronizingObject;

Expand Down Expand Up @@ -213,6 +214,7 @@ public string Path
}

_path = value;
FullPath = _path;
}
}

Expand Down Expand Up @@ -258,6 +260,32 @@ public ISynchronizeInvoke? SynchronizingObject
}
}

/// <summary>
/// Caches the full path of <see cref="Path"/>
/// </summary>
private string FullPath
{
get => _fullPath;
set
{
if (string.IsNullOrEmpty(value))
{
_fullPath = value;

return;
}

string fullPath = _fileSystem.Path.GetFullPath(value);

if (!fullPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar))
{
fullPath += _fileSystem.Path.DirectorySeparatorChar;
}

_fullPath = fullPath;
}
}

/// <inheritdoc cref="IFileSystemWatcher.BeginInit()" />
public void BeginInit()
{
Expand Down Expand Up @@ -399,19 +427,19 @@ private void NotifyChange(ChangeDescription item)
if (item.ChangeType.HasFlag(WatcherChangeTypes.Created))
{
Created?.Invoke(this, ToFileSystemEventArgs(
item.ChangeType, item.Path, item.Name));
item.ChangeType, item.Path));
}

if (item.ChangeType.HasFlag(WatcherChangeTypes.Deleted))
{
Deleted?.Invoke(this, ToFileSystemEventArgs(
item.ChangeType, item.Path, item.Name));
item.ChangeType, item.Path));
}

if (item.ChangeType.HasFlag(WatcherChangeTypes.Changed))
{
Changed?.Invoke(this, ToFileSystemEventArgs(
item.ChangeType, item.Path, item.Name));
item.ChangeType, item.Path));
}

if (item.ChangeType.HasFlag(WatcherChangeTypes.Renamed))
Expand Down Expand Up @@ -502,68 +530,6 @@ private void Stop()
_changeHandler?.Dispose();
}

private FileSystemEventArgs ToFileSystemEventArgs(
WatcherChangeTypes changeType,
string changePath,
string? changeName)
{
string path = TransformPathAndName(
changePath,
changeName,
out string name);

FileSystemEventArgs eventArgs = new(changeType, Path, name);
if (_fileSystem.SimulationMode != SimulationMode.Native)
{
// 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: Have to resort to Reflection to override this behavior!
#if NETFRAMEWORK
typeof(FileSystemEventArgs)
.GetField("fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(eventArgs, path);
#else
typeof(FileSystemEventArgs)
.GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(eventArgs, path);
#endif
}

return eventArgs;
}

private string TransformPathAndName(
string changeDescriptionPath,
string? changeDescriptionName,
out string name)
{
string? transformedName = changeDescriptionName;
string? path = changeDescriptionPath;
if (!_fileSystem.Path.IsPathRooted(Path))
{
string rootedWatchedPath = _fileSystem.Directory.GetCurrentDirectory();
if (!rootedWatchedPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar))
{
rootedWatchedPath += _fileSystem.Path.DirectorySeparatorChar;
}

if (path.StartsWith(rootedWatchedPath, _fileSystem.Execute.StringComparisonMode))
{
path = path.Substring(rootedWatchedPath.Length);
}

transformedName = _fileSystem.Execute.Path.GetFileName(changeDescriptionPath);
}
else if (transformedName == null ||
_fileSystem.Execute.Path.IsPathRooted(changeDescriptionName))
{
transformedName = _fileSystem.Execute.Path.GetFileName(changeDescriptionPath);
}

name = transformedName;

return path ?? "";
}

private void TriggerRenameNotification(ChangeDescription item)
{
if (_fileSystem.Execute.IsWindows)
Expand All @@ -578,13 +544,13 @@ private void TriggerRenameNotification(ChangeDescription item)
if (MatchesWatcherPath(item.OldPath))
{
Deleted?.Invoke(this, ToFileSystemEventArgs(
WatcherChangeTypes.Deleted, item.OldPath, item.OldName));
WatcherChangeTypes.Deleted, item.OldPath));
}

if (MatchesWatcherPath(item.Path))
{
Created?.Invoke(this, ToFileSystemEventArgs(
WatcherChangeTypes.Created, item.Path, item.Name));
WatcherChangeTypes.Created, item.Path));
}
}
}
Expand All @@ -601,54 +567,92 @@ private void TriggerRenameNotification(ChangeDescription item)

private bool TryMakeRenamedEventArgs(
ChangeDescription changeDescription,
[NotNullWhen(true)] out RenamedEventArgs? eventArgs)
[NotNullWhen(true)] out RenamedEventArgs? eventArgs
)
{
if (changeDescription.OldPath == null)
{
eventArgs = null;

return false;
}

string path = TransformPathAndName(
changeDescription.Path,
changeDescription.Name,
out string name);
string name = TransformPathAndName(changeDescription.Path);

string oldName = TransformPathAndName(changeDescription.OldPath);

eventArgs = new RenamedEventArgs(changeDescription.ChangeType, Path, name, oldName);

SetFileSystemEventArgsFullPath(eventArgs, name);
SetRenamedEventArgsFullPath(eventArgs, oldName);

string oldPath = TransformPathAndName(
changeDescription.OldPath,
changeDescription.OldName,
out string oldName);
return _fileSystem.Execute.Path.GetDirectoryName(changeDescription.Path)?.Equals(
_fileSystem.Execute.Path.GetDirectoryName(changeDescription.OldPath),
_fileSystem.Execute.StringComparisonMode
)
?? true;
}

private FileSystemEventArgs ToFileSystemEventArgs(
WatcherChangeTypes changeType,
string changePath)
{
string name = TransformPathAndName(changePath);

FileSystemEventArgs eventArgs = new(changeType, Path, name);

SetFileSystemEventArgsFullPath(eventArgs, name);

return eventArgs;
}

eventArgs = new RenamedEventArgs(
changeDescription.ChangeType,
Path,
name,
oldName);
private string TransformPathAndName(string changeDescriptionPath)
{
return changeDescriptionPath.Substring(FullPath.Length).TrimStart(_fileSystem.Path.DirectorySeparatorChar);
}

if (_fileSystem.SimulationMode != SimulationMode.Native)
private void SetFileSystemEventArgsFullPath(FileSystemEventArgs args, string name)
{
if (_fileSystem.SimulationMode == SimulationMode.Native)
{
// 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
// HACK: Have to resort to Reflection to override this behavior!
return;
}

string fullPath = _fileSystem.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(eventArgs, path);
.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.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(eventArgs, oldPath);
.SetValue(args, fullPath);
#else
typeof(FileSystemEventArgs)
.GetField("_fullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(eventArgs, path);
typeof(RenamedEventArgs)
.GetField("_oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(eventArgs, oldPath);
typeof(RenamedEventArgs)
.GetField("_oldFullPath", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(args, fullPath);
#endif
}

return _fileSystem.Execute.Path.GetDirectoryName(changeDescription.Path)?
.Equals(_fileSystem.Execute.Path.GetDirectoryName(changeDescription.OldPath),
_fileSystem.Execute.StringComparisonMode) ?? true;
}

private IWaitForChangedResult WaitForChangedInternal(
Expand Down
Loading
Loading