Search code examples
c#filesystemsfilesystemwatcher

How can I use FileSystemWatcher from System.IO.Abstractions to monitor a mock filesystem?


I'm trying to create a class to monitor USB device arrivals and removals on Linux. On Linux, USB devices are represented as device files under /dev/bus/usb which are created/deleted in response to these events.

It seems the best way to track these events is using FileSystemWatcher. To make the class testable, I'm using System.IO.Abstractions and injecting an instance of IFileSystem to the class during construction. What I want is to create something that behaves like a FileSystemWatcher but monitors changes to the injected IFileSystem, not the real file-system directly.

Looking at FileSystemWatcherBase and FileSystemWatcherWrapper from System.IO.Abstractions, I'm not sure how to do this. At the moment I have this (which I know is wrong):

public DevMonitor(
    [NotNull] IFileSystem fileSystem,
    [NotNull] IDeviceFileParser deviceFileParser,
    [NotNull] ILogger logger,
    [NotNull] string devDirectoryPath = DefaultDevDirectoryPath)
{
    Raise.ArgumentNullException.IfIsNull(logger, nameof(logger));
    Raise.ArgumentNullException.IfIsNull(devDirectoryPath, nameof(devDirectoryPath));

    _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
    _deviceFileParser = deviceFileParser ?? throw new ArgumentNullException(nameof(deviceFileParser));
    _logger = logger.ForContext<DevMonitor>();
    _watcher = new FileSystemWatcherWrapper(devDirectoryPath);
}

Solution

  • In light of the fact that System.IO.Abstractions doesn't seem to support this yet, I went with this:

    I defined an IWatchableFileSystem interface that extends IFileSystem:

    /// <summary>
    /// Represents a(n) <see cref="IFileSystem" /> that can be watched for changes.
    /// </summary>
    public interface IWatchableFileSystem : IFileSystem
    {
        /// <summary>
        /// Creates a <c>FileSystemWatcher</c> that can be used to monitor changes to this file system.
        /// </summary>
        /// <returns>A <c>FileSystemWatcher</c>.</returns>
        FileSystemWatcherBase CreateWatcher();
    }
    

    For production purposes I implement this as WatchableFileSystem:

    /// <inheritdoc />
    public sealed class WatchableFileSystem : IWatchableFileSystem
    {
        private readonly IFileSystem _fileSystem;
    
        /// <summary>
        /// Initializes a new instance of the <see cref="WatchableFileSystem" /> class.
        /// </summary>
        public WatchableFileSystem() => _fileSystem = new FileSystem();
    
        /// <inheritdoc />
        public DirectoryBase Directory => _fileSystem.Directory;
    
        /// <inheritdoc />
        public IDirectoryInfoFactory DirectoryInfo => _fileSystem.DirectoryInfo;
    
        /// <inheritdoc />
        public IDriveInfoFactory DriveInfo => _fileSystem.DriveInfo;
    
        /// <inheritdoc />
        public FileBase File => _fileSystem.File;
    
        /// <inheritdoc />
        public IFileInfoFactory FileInfo => _fileSystem.FileInfo;
    
        /// <inheritdoc />
        public PathBase Path => _fileSystem.Path;
    
        /// <inheritdoc />
        public FileSystemWatcherBase CreateWatcher() => new FileSystemWatcher();
    }
    

    Within my unit test class I implement it as MockWatchableFileSystem which exposes MockFileSystem and Mock<FileSystemWatcherBase> as properties which I can use for arranging & asserting within my tests:

    private class MockWatchableFileSystem : IWatchableFileSystem
    {
        /// <inheritdoc />
        public MockWatchableFileSystem()
        {
            Watcher = new Mock<FileSystemWatcherBase>();
            AsMock = new MockFileSystem();
    
            AsMock.AddDirectory("/dev/bus/usb");
            Watcher.SetupAllProperties();
        }
    
        public MockFileSystem AsMock { get; }
    
        /// <inheritdoc />
        public DirectoryBase Directory => AsMock.Directory;
    
        /// <inheritdoc />
        public IDirectoryInfoFactory DirectoryInfo => AsMock.DirectoryInfo;
    
        /// <inheritdoc />
        public IDriveInfoFactory DriveInfo => AsMock.DriveInfo;
    
        /// <inheritdoc />
        public FileBase File => AsMock.File;
    
        /// <inheritdoc />
        public IFileInfoFactory FileInfo => AsMock.FileInfo;
    
        /// <inheritdoc />
        public PathBase Path => AsMock.Path;
    
        public Mock<FileSystemWatcherBase> Watcher { get; }
    
        /// <inheritdoc />
        public FileSystemWatcherBase CreateWatcher() => Watcher.Object;
    }
    

    Finally, in my client class I can just do:

    public DevMonitor(
        [NotNull] IWatchableFileSystem fileSystem,
        [NotNull] IDeviceFileParser deviceFileParser,
        [NotNull] ILogger logger,
        [NotNull] string devDirectoryPath = DefaultDevDirectoryPath)
    {
        // ...
        _watcher = fileSystem.CreateWatcher();
    
        _watcher.IncludeSubdirectories = true;
        _watcher.EnableRaisingEvents = true;
        _watcher.Path = devDirectoryPath;
    }
    

    During tests, the client gets an IWatchableFileSystem that wraps MockFileSystem and returns a mock instance of FileSystemWatcherBase. During production, it gets an IWatchableFileSystem that wraps FileSystem and generates unique instances of FileSystemWatcher.