4

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);
}
Tagc
  • 8,736
  • 7
  • 61
  • 114
  • Can I ask what the downvote is for? – Tagc Oct 31 '17 at 09:17
  • I don't think it's possible. FileSystemWatcher class is not extensible at all (doesn't even have any useful virtual methods). Instead - abstract away FileSystemWatcher behind some interface which has `Created` etc events and in your unit tests fire those events manually. So after adding directory to your `IFileSystem` - fire corresponding event yourself. – Evk Oct 31 '17 at 09:34
  • That's one way but I was hoping I wouldn't have to resort to that. I wasn't expecting them to directly extend `FileSystemWatcher`, but maybe to have something along the lines of a factory `IFileSystem => IFileSystemWatcher` which checked if the provided `IFileSystem` is real or a mock. If real, then return a wrapper around the standard `FileSystemWatcher`. If mock, then return a custom class that monitors changes to the mock file system. – Tagc Oct 31 '17 at 09:58
  • I guess you have to implement that yourself, or file github issue about that and hope author will implement it. There is issue about implementing mock FileSystemWatcher already: https://github.com/tathamoddie/System.IO.Abstractions/issues/167, which is 1 year old, so I'd not expect fast results. – Evk Oct 31 '17 at 10:20
  • Ah, cheers. That sucks. – Tagc Oct 31 '17 at 10:25

1 Answers1

4

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.

Tagc
  • 8,736
  • 7
  • 61
  • 114
  • If I were to call the .AsMock.AddFile method at a later time, would that invoke the Created event? – as9876 Oct 13 '21 at 21:59