Skip to content

Implement MockFileSystemWatcher - #373

Closed
rkoeninger wants to merge 3 commits into
TestableIO:masterfrom
rkoeninger:watcher
Closed

rkoeninger wants to merge 3 commits into
TestableIO:masterfrom
rkoeninger:watcher

Conversation

@rkoeninger

@rkoeninger rkoeninger commented Oct 16, 2018 •

Copy link
Copy Markdown
Contributor

Fixes #329

Here's my attempt at a working MockFileSystemWatcher. The referenced issue was only for the WaitForChanged methods, but really the whole class didn't do anything.

In order to support this, MockFileSystem needed an event dispatch system built into it.

Current Approach

Listeners can get hold of a ConcurrentQueue by calling Listen and the MFS will keep a list of those queues, pushing events into them as modification operations are performed.

For the WaitForChanged methods specifically, they are effectively one-time filtered (by event type) event handlers. A WaitableRef class was added to so the accumulation of waiters on a particular event can be modeled. Each waiter has a ManualResetEvent to signal when the required event type has happened and an internal field to hold the event object. The waiters get added to a dictionary in the MockFileSystemWatcher. When the watcher receives an event, it checks if there are waiters that need that event type, Send it to them (which triggers the Waiting thread to wake up) and removes them from the dictionary.

There are a few tests, but they don't pass reliably on my machine. Concurrency is surely not implemented too well. I would also want to get rid of those Thread.Sleeps.

More notes:

  • Operations on directories are ignored as I believe the FileSystemWatcher only handles events on files.
  • MockFileSystem dispatches events synchronously. Not too big of a deal as the event handling code doesn't immediately run on the same thread, but this does make the calling thread to File.Create, etc spend the time to add the events to the queues.
  • If a file is "moved" to the same directory, but with a different name, it's considered a Renamed event, not two separate Deleted and Created events.
  • Added internal MockFileSystem.MoveFile event to better identify events.
  • MockFileInfo still needs to be updated, in particular, to prevent it from doing delete/create and using the internal MoveFile function instead.
  • Changed event currently not dispatched. I assume this means file contents changed.

Reactive

The current use of concurrency is fairly fragile, and it occurred to me that System.Reactive would be a better way to go. The MFS would have a Subject that it would OnNext events to, and the FSWs would be able to simply Subscribe to the Subject and use .Where to filter for messages under the directory root that they are watching. Waiters would subscribe to their watcher's filtered subscription, but with an additional .FirstAsync(e => e.ChangeType == changeType) so they only get the next event of the required type. Then chain .ToTask().Wait(int) to synchronously wait for the event as the WaitForChanged method is designed that way.

I don't think adding System.Reactive would be too onerous as it would only be added to the TestingHelpers, it wouldn't make the S.IO.A.dll any bigger, or introduce transitive dependency issues in prod. On the other hand, I wasn't sure which version to use to support net40, netstandard1.4 and netstandard2.0 as this library does.


OK, so after all of that, how accurately do we want to reproduce the behavior of FileSystemWatcher? Should we just cut corner on this one? Unit tests could get a hold of the MockFSW passed into SUT and call its OnCreate, etc methods directly. That might be enough?

@fgreinacher

Copy link
Copy Markdown
Contributor

To be honest I'm kind of hesitant to provide this feature at all, mainly for the following reasons

  • it adds a lot of complexity to our maybe already pretty complex codebase
  • users will expect feature-parity to S.IO.FileSystemWatcher which is gonna be hard, especially on the corner cases
  • especially the async parts will cause people to go serious about using the MFS in multi-threaded test scenarios and I expect a lot of issues once this happens

I value your work but I would really tend to not do this.

What do the others (@jpreese, @tathamoddie, @ericnewton76) think?

@rkoeninger

Copy link
Copy Markdown
Contributor Author

I knew this was borderline madness when I started working on it. Would synchronous triggering of event handlers work or still too complicated?

If the library isn't going to provide file watching functionality, it should throw an exception as soon as you create a file watcher and not just let one get created that won't do anything. That would be a very confusing issue to encounter.

@erenes

erenes commented Oct 30, 2018

Copy link
Copy Markdown
Contributor

For my test scenario I have made the event handler internal and call it directly from the unit test, however in case my code initializes the FSW completely wrong (watching the wrong directory, no subdirectories, forgetting to add event handlers, EnableRaisingEvents=false, wrong filter), this is untested.

If I can call a method to trigger an event (Changed, Created, Deleted, Renamed) with a filename (or perhaps 2 filenames in the event of Renamed) and the FSW only raises the event (and calls my code) if the path matches the listener settings, this would give me enough tools to verify that my code is working as intended.

I tried implementing this in a custom MockFSW, but MockFileSystem does not allow me to inject my own FSWFactory, so I deemed it to be too much hassle.

/my 2 cents

@rkoeninger

Copy link
Copy Markdown
Contributor Author

I'm going to retract this PR. I'll update #329 to have the MockFileSystem be able to accept a mocked FSW and have a default FSW factory that just throws.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants