Welcome to mirror list, hosted at ThFree Co, Russian Federation.

FileSystemWatcherTest.cs « Utility « tests « System.IO.FileSystem.Watcher « src - github.com/mono/corefx.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 78dccce319d84296b76339898fcb136e7f20d293 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using Xunit;
using Xunit.Sdk;

namespace System.IO.Tests
{
    public abstract partial class FileSystemWatcherTest : FileCleanupTestBase
    {
        // Events are reported asynchronously by the OS, so allow an amount of time for
        // them to arrive before testing an assertion.  If we expect an event to occur,
        // we can wait for it for a relatively long time, as if it doesn't arrive, we're
        // going to fail the test.  If we don't expect an event to occur, then we need
        // to keep the timeout short, as in a successful run we'll end up waiting for
        // the entire timeout specified.
        public const int WaitForExpectedEventTimeout = 500;         // ms to wait for an event to happen
        public const int LongWaitTimeout = 50000;                   // ms to wait for an event that takes a longer time than the average operation
        public const int SubsequentExpectedWait = 10;               // ms to wait for checks that occur after the first.
        public const int WaitForExpectedEventTimeout_NoRetry = 3000;// ms to wait for an event that isn't surrounded by a retry.
        public const int WaitForUnexpectedEventTimeout = 150;       // ms to wait for a non-expected event.
        public const int DefaultAttemptsForExpectedEvent = 3;       // Number of times an expected event should be retried if failing.
        public const int DefaultAttemptsForUnExpectedEvent = 2;     // Number of times an unexpected event should be retried if failing.
        public const int RetryDelayMilliseconds = 500;              // ms to wait when retrying after failure

        /// <summary>
        /// Watches the Changed WatcherChangeType and unblocks the returned AutoResetEvent when a
        /// Changed event is thrown by the watcher.
        /// </summary>
        public static (AutoResetEvent EventOccured, FileSystemEventHandler Handler) WatchChanged(FileSystemWatcher watcher, string[] expectedPaths = null)
        {
            AutoResetEvent eventOccurred = new AutoResetEvent(false);

            FileSystemEventHandler changeHandler = (o, e) =>
            {
                Assert.Equal(WatcherChangeTypes.Changed, e.ChangeType);
                if (expectedPaths != null)
                {
                    Assert.Contains(Path.GetFullPath(e.FullPath), expectedPaths);
                }
                eventOccurred.Set();
            };

            watcher.Changed += changeHandler;
            return (eventOccurred, changeHandler);
        }

        /// <summary>
        /// Watches the Created WatcherChangeType and unblocks the returned AutoResetEvent when a
        /// Created event is thrown by the watcher.
        /// </summary>
        public static (AutoResetEvent EventOccured, FileSystemEventHandler Handler) WatchCreated(FileSystemWatcher watcher, string[] expectedPaths = null)
        {
            AutoResetEvent eventOccurred = new AutoResetEvent(false);

            FileSystemEventHandler handler = (o, e) =>
            {
                Assert.Equal(WatcherChangeTypes.Created, e.ChangeType);
                if (expectedPaths != null)
                {
                    Assert.Contains(Path.GetFullPath(e.FullPath), expectedPaths);
                }
                eventOccurred.Set();
            };

            watcher.Created += handler;
            return (eventOccurred, handler);
        }

        /// <summary>
        /// Watches the Renamed WatcherChangeType and unblocks the returned AutoResetEvent when a
        /// Renamed event is thrown by the watcher.
        /// </summary>
        public static (AutoResetEvent EventOccured, FileSystemEventHandler Handler) WatchDeleted(FileSystemWatcher watcher, string[] expectedPaths = null)
        {
            AutoResetEvent eventOccurred = new AutoResetEvent(false);
            FileSystemEventHandler handler = (o, e) =>
            {
                Assert.Equal(WatcherChangeTypes.Deleted, e.ChangeType);
                if (expectedPaths != null)
                {
                    Assert.Contains(Path.GetFullPath(e.FullPath), expectedPaths);
                }
                eventOccurred.Set();
            };

            watcher.Deleted += handler;
            return (eventOccurred, handler);
        }

        /// <summary>
        /// Watches the Renamed WatcherChangeType and unblocks the returned AutoResetEvent when a
        /// Renamed event is thrown by the watcher.
        /// </summary>
        public static (AutoResetEvent EventOccured, RenamedEventHandler Handler) WatchRenamed(FileSystemWatcher watcher, string[] expectedPaths = null)
        {
            AutoResetEvent eventOccurred = new AutoResetEvent(false);

            RenamedEventHandler handler = (o, e) =>
            {
                Assert.Equal(WatcherChangeTypes.Renamed, e.ChangeType);
                if (expectedPaths != null)
                {
                    Assert.Contains(Path.GetFullPath(e.FullPath), expectedPaths);
                }
                eventOccurred.Set();
            };

            watcher.Renamed += handler;
            return (eventOccurred, handler);
        }

        /// <summary>
        /// Asserts that the given handle will be signaled within the default timeout.
        /// </summary>
        public static void ExpectEvent(WaitHandle eventOccurred, string eventName_NoRetry)
        {
            string message = string.Format("Didn't observe a {0} event within {1}ms", eventName_NoRetry, WaitForExpectedEventTimeout_NoRetry);
            Assert.True(eventOccurred.WaitOne(WaitForExpectedEventTimeout_NoRetry), message);
        }

        /// <summary>
        /// Does verification that the given watcher will throw exactly/only the events in "expectedEvents" when
        /// "action" is executed.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
        /// <param name="action">The Action that will trigger events.</param>
        /// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        public static void ExpectEvent(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, Action cleanup = null)
        {
            ExpectEvent(watcher, expectedEvents, action, cleanup, (string[])null);
        }

        /// <summary>
        /// Does verification that the given watcher will throw exactly/only the events in "expectedEvents" when
        /// "action" is executed.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
        /// <param name="action">The Action that will trigger events.</param>
        /// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        /// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
        /// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
        public static void ExpectEvent(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, Action cleanup = null, string expectedPath = null, int attempts = DefaultAttemptsForExpectedEvent, int timeout = WaitForExpectedEventTimeout)
        {
            ExpectEvent(watcher, expectedEvents, action, cleanup, expectedPath == null ? null : new string[] { expectedPath }, attempts, timeout);
        }

        /// <summary>
        /// Does verification that the given watcher will throw exactly/only the events in "expectedEvents" when
        /// "action" is executed.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
        /// <param name="action">The Action that will trigger events.</param>
        /// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        /// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
        /// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
        public static void ExpectEvent(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, Action cleanup = null, string[] expectedPaths = null, int attempts = DefaultAttemptsForExpectedEvent, int timeout = WaitForExpectedEventTimeout)
        {
            int attemptsCompleted = 0;
            bool result = false;
            FileSystemWatcher newWatcher = watcher;
            while (!result && attemptsCompleted++ < attempts)
            {
                if (attemptsCompleted > 1)
                {
                    // Re-create the watcher to get a clean iteration.
                    newWatcher = RecreateWatcher(newWatcher);
                    // Most intermittent failures in FSW are caused by either a shortage of resources (e.g. inotify instances)
                    // or by insufficient time to execute (e.g. CI gets bogged down). Immediately re-running a failed test
                    // won't resolve the first issue, so we wait a little while hoping that things clear up for the next run.
                    Thread.Sleep(RetryDelayMilliseconds);
                }

                result = ExecuteAndVerifyEvents(newWatcher, expectedEvents, action, attemptsCompleted == attempts, expectedPaths, timeout);

                if (cleanup != null)
                    cleanup();
            }
        }

        /// <summary>Invokes the specified test action with retry on failure (other than assertion failure).</summary>
        /// <param name="action">The test action.</param>
        /// <param name="maxAttempts">The maximum number of times to attempt to run the test.</param>
        public static void ExecuteWithRetry(Action action, int maxAttempts = DefaultAttemptsForExpectedEvent)
        {
            for (int retry = 0; retry < maxAttempts; retry++)
            {
                try
                {
                    action();
                    return;
                }
                catch (Exception e) when (!(e is XunitException) && retry < maxAttempts - 1)
                {
                    Thread.Sleep(RetryDelayMilliseconds);
                }
            }
        }

        /// <summary>
        /// Does verification that the given watcher will not throw exactly/only the events in "expectedEvents" when
        /// "action" is executed.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="unExpectedEvents">All of the events that are expected to be raised by this action</param>
        /// <param name="action">The Action that will trigger events.</param>
        /// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        /// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
        public static void ExpectNoEvent(FileSystemWatcher watcher, WatcherChangeTypes unExpectedEvents, Action action, Action cleanup = null, string expectedPath = null, int timeout = WaitForExpectedEventTimeout)
        {
            bool result = ExecuteAndVerifyEvents(watcher, unExpectedEvents, action, false, new string[] { expectedPath }, timeout);
            Assert.False(result, "Expected Event occured");

            if (cleanup != null)
                cleanup();
        }

        /// <summary>
        /// Helper for the ExpectEvent function. 
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
        /// <param name="action">The Action that will trigger events.</param>
        /// <param name="assertExpected">True if results should be asserted. Used if there is no retry.</param>
        /// <param name="expectedPath"> Adds path verification to all expected events.</param>
        /// <returns>True if the events raised correctly; else, false.</returns>
        public static bool ExecuteAndVerifyEvents(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, bool assertExpected, string[] expectedPaths, int timeout)
        {
            bool result = true, verifyChanged = true, verifyCreated = true, verifyDeleted = true, verifyRenamed = true;
            (AutoResetEvent EventOccured, FileSystemEventHandler Handler) changed = default, created = default, deleted = default;
            (AutoResetEvent EventOccured, RenamedEventHandler Handler) renamed = default;

            if (verifyChanged = ((expectedEvents & WatcherChangeTypes.Changed) > 0))
                changed = WatchChanged(watcher, expectedPaths);
            if (verifyCreated = ((expectedEvents & WatcherChangeTypes.Created) > 0))
                created = WatchCreated(watcher, expectedPaths);
            if (verifyDeleted = ((expectedEvents & WatcherChangeTypes.Deleted) > 0))
                deleted = WatchDeleted(watcher, expectedPaths);
            if (verifyRenamed = ((expectedEvents & WatcherChangeTypes.Renamed) > 0))
                renamed = WatchRenamed(watcher, expectedPaths);

            watcher.EnableRaisingEvents = true;
            action();

            // Verify Changed
            if (verifyChanged)
            {
                bool Changed_expected = ((expectedEvents & WatcherChangeTypes.Changed) > 0);
                bool Changed_actual = changed.EventOccured.WaitOne(timeout);
                watcher.Changed -= changed.Handler;
                result = Changed_expected == Changed_actual;
                if (assertExpected)
                    Assert.True(Changed_expected == Changed_actual, "Changed event did not occur as expected");
            }

            // Verify Created
            if (verifyCreated)
            {
                bool Created_expected = ((expectedEvents & WatcherChangeTypes.Created) > 0);
                bool Created_actual = created.EventOccured.WaitOne(verifyChanged ? SubsequentExpectedWait : timeout);
                watcher.Created -= created.Handler;
                result = result && Created_expected == Created_actual;
                if (assertExpected)
                    Assert.True(Created_expected == Created_actual, "Created event did not occur as expected");
            }

            // Verify Deleted
            if (verifyDeleted)
            {
                bool Deleted_expected = ((expectedEvents & WatcherChangeTypes.Deleted) > 0);
                bool Deleted_actual = deleted.EventOccured.WaitOne(verifyChanged || verifyCreated ? SubsequentExpectedWait : timeout);
                watcher.Deleted -= deleted.Handler;
                result = result && Deleted_expected == Deleted_actual;
                if (assertExpected)
                    Assert.True(Deleted_expected == Deleted_actual, "Deleted event did not occur as expected");
            }

            // Verify Renamed
            if (verifyRenamed)
            {
                bool Renamed_expected = ((expectedEvents & WatcherChangeTypes.Renamed) > 0);
                bool Renamed_actual = renamed.EventOccured.WaitOne(verifyChanged || verifyCreated || verifyDeleted ? SubsequentExpectedWait : timeout);
                watcher.Renamed -= renamed.Handler;
                result = result && Renamed_expected == Renamed_actual;
                if (assertExpected)
                    Assert.True(Renamed_expected == Renamed_actual, "Renamed event did not occur as expected");
            }

            watcher.EnableRaisingEvents = false;
            return result;
        }

        /// <summary>
        /// Does verification that the given watcher will throw an Error when the given action is executed.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="action">The Action that will trigger a failure.</param>
        /// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        /// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
        public static void ExpectError(FileSystemWatcher watcher, Action action, Action cleanup, int attempts = DefaultAttemptsForExpectedEvent)
        {
            string message = string.Format("Did not observe an error event within {0}ms and {1} attempts.", WaitForExpectedEventTimeout, attempts);
            Assert.True(TryErrorEvent(watcher, action, cleanup, attempts, expected: true), message);
        }

        /// <summary>
        /// Does verification that the given watcher will <b>not</b> throw an Error when the given action is executed.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="action">The Action that will not trigger a failure.</param>
        /// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        /// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
        public static void ExpectNoError(FileSystemWatcher watcher, Action action, Action cleanup, int attempts = DefaultAttemptsForUnExpectedEvent)
        {
            string message = string.Format("Should not observe an error event within {0}ms. Attempted {1} times and received the event each time.", WaitForExpectedEventTimeout, attempts);
            Assert.False(TryErrorEvent(watcher, action, cleanup, attempts, expected: true), message);
        }

        /// /// <summary>
        /// Helper method for the ExpectError/ExpectNoError functions.
        /// </summary>
        /// <param name="watcher">The FileSystemWatcher to test</param>
        /// <param name="action">The Action to execute.</param>
        /// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
        /// <param name="attempts">Number of times the test should be executed if it's failing.</param>
        /// <param name="expected">Whether it is expected that an error event will be arisen.</param>
        /// <returns>True if an Error event was raised by the watcher when the given action was executed; else, false.</returns>
        public static bool TryErrorEvent(FileSystemWatcher watcher, Action action, Action cleanup, int attempts, bool expected)
        {
            int attemptsCompleted = 0;
            bool result = !expected;
            while (result != expected && attemptsCompleted++ < attempts)
            {
                if (attemptsCompleted > 1)
                {
                    // Re-create the watcher to get a clean iteration.
                    watcher = new FileSystemWatcher()
                    {
                        IncludeSubdirectories = watcher.IncludeSubdirectories,
                        NotifyFilter = watcher.NotifyFilter,
                        Filter = watcher.Filter,
                        Path = watcher.Path,
                        InternalBufferSize = watcher.InternalBufferSize
                    };
                    // Most intermittent failures in FSW are caused by either a shortage of resources (e.g. inotify instances)
                    // or by insufficient time to execute (e.g. CI gets bogged down). Immediately re-running a failed test
                    // won't resolve the first issue, so we wait a little while hoping that things clear up for the next run.
                    Thread.Sleep(500);
                }

                AutoResetEvent errorOccurred = new AutoResetEvent(false);
                watcher.Error += (o, e) =>
                {
                    errorOccurred.Set();
                };

                // Enable raising events but be careful with the possibility of the max user inotify instances being reached already.
                if (attemptsCompleted <= attempts)
                {
                    try
                    {
                        watcher.EnableRaisingEvents = true;
                    }
                    catch (IOException) // Max User INotify instances. Isn't the type of error we're checking for.
                    {
                        continue;
                    }
                }
                else
                {
                    watcher.EnableRaisingEvents = true;
                }

                action();
                result = errorOccurred.WaitOne(WaitForExpectedEventTimeout);
                watcher.EnableRaisingEvents = false;
                cleanup();
            }
            return result;
        }

        /// <summary>
        /// In some cases (such as when running without elevated privileges),
        /// the symbolic link may fail to create. Only run this test if it creates
        /// links successfully.
        /// </summary>
        protected static bool CanCreateSymbolicLinks
        {
            get
            {
                bool success = true;

                // Verify file symlink creation
                string path = Path.GetTempFileName();
                string linkPath = path + ".link";
                success = CreateSymLink(path, linkPath, isDirectory: false);
                try { File.Delete(path); } catch { }
                try { File.Delete(linkPath); } catch { }

                // Verify directory symlink creation
                path = Path.GetTempFileName();
                linkPath = path + ".link";
                success = success && CreateSymLink(path, linkPath, isDirectory: true);
                try { Directory.Delete(path); } catch { }
                try { Directory.Delete(linkPath); } catch { }

                return success;
            }
        }

        public static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory)
        {
            Process symLinkProcess = new Process();
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                symLinkProcess.StartInfo.FileName = "cmd";
                symLinkProcess.StartInfo.Arguments = string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath));
            }
            else
            {
                symLinkProcess.StartInfo.FileName = "/bin/ln";
                symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", Path.GetFullPath(targetPath), Path.GetFullPath(linkPath));
            }
            symLinkProcess.StartInfo.RedirectStandardOutput = true;
            symLinkProcess.Start();

            if (symLinkProcess != null)
            {
                symLinkProcess.WaitForExit();
                return (0 == symLinkProcess.ExitCode);
            }
            else
            {
                return false;
            }
        }


        public static IEnumerable<object[]> FilterTypes()
        {
            foreach (NotifyFilters filter in Enum.GetValues(typeof(NotifyFilters)))
                yield return new object[] { filter };
        }

        // Linux and OSX systems have less precise filtering systems than Windows, so most
        // metadata filters are effectively equivalent to each other on those systems. For example
        // there isn't a way to filter only LastWrite events on either system; setting
        // Filters to LastWrite will allow events from attribute change, creation time
        // change, size change, etc.
        public const NotifyFilters LinuxFiltersForAttribute = NotifyFilters.Attributes |
                                                                NotifyFilters.CreationTime |
                                                                NotifyFilters.LastAccess |
                                                                NotifyFilters.LastWrite |
                                                                NotifyFilters.Security |
                                                                NotifyFilters.Size;
        public const NotifyFilters LinuxFiltersForModify = NotifyFilters.LastAccess |
                                                            NotifyFilters.LastWrite |
                                                            NotifyFilters.Security |
                                                            NotifyFilters.Size;
        public const NotifyFilters OSXFiltersForModify = NotifyFilters.Attributes |
                                                        NotifyFilters.CreationTime |
                                                        NotifyFilters.LastAccess |
                                                        NotifyFilters.LastWrite |
                                                        NotifyFilters.Size;

#if MONO
        private static FileSystemWatcher RecreateWatcher(FileSystemWatcher watcher)
        {
            FileSystemWatcher newWatcher = new FileSystemWatcher()
            {
                IncludeSubdirectories = watcher.IncludeSubdirectories,
                NotifyFilter = watcher.NotifyFilter,
                Path = watcher.Path,
                InternalBufferSize = watcher.InternalBufferSize
            };
            return newWatcher;
        }
#endif

        internal readonly struct FiredEvent
        {
            public FiredEvent(WatcherChangeTypes eventType, string dir1, string dir2 = "") => (EventType, Dir1, Dir2) = (eventType, dir1, dir2);

            public readonly WatcherChangeTypes EventType;
            public readonly string Dir1;
            public readonly string Dir2;

            public override bool Equals(object obj) => obj is FiredEvent evt && Equals(evt);

            public bool Equals(FiredEvent other) => EventType == other.EventType &&
                Dir1 == other.Dir1 &&
                Dir2 == other.Dir2;


            public override int GetHashCode() => EventType.GetHashCode() ^ Dir1.GetHashCode() ^ Dir2.GetHashCode();

            public override string ToString() => $"{EventType} {Dir1} {Dir2}";

        }

        // Observe until an expected count of events is triggered, otherwise fail. Return all collected events.
        internal static List<FiredEvent> ExpectEvents(FileSystemWatcher watcher, int expectedEvents, Action action)
        {
            using var eventsOccured = new AutoResetEvent(false);
            var eventsOrrures = 0;

            var events = new List<FiredEvent>();

            ErrorEventArgs error = null;

            FileSystemEventHandler fileWatcherEvent = (_, e) => AddEvent(e.ChangeType, e.FullPath);
            RenamedEventHandler renameWatcherEvent = (_, e) => AddEvent(e.ChangeType, e.FullPath, e.OldFullPath);
            ErrorEventHandler errorHandler = (_, e) => error ??= e ?? new ErrorEventArgs(null);

            watcher.Changed += fileWatcherEvent;
            watcher.Created += fileWatcherEvent;
            watcher.Deleted += fileWatcherEvent;
            watcher.Renamed += renameWatcherEvent;
            watcher.Error += errorHandler;

            bool raisingEvent = watcher.EnableRaisingEvents;
            watcher.EnableRaisingEvents = true;

            try
            {
                action();
                eventsOccured.WaitOne(new TimeSpan(0, 0, 5));
            }
            finally
            {
                watcher.Changed -= fileWatcherEvent;
                watcher.Created -= fileWatcherEvent;
                watcher.Deleted -= fileWatcherEvent;
                watcher.Renamed -= renameWatcherEvent;
                watcher.Error -= errorHandler;
                watcher.EnableRaisingEvents = raisingEvent;
            }

            if (error != null)
            {
                Assert.False(true, $"Filewatcher error event triggered: { error.GetException()?.Message ?? "Unknow error" }");
            }
            Assert.True(eventsOrrures == expectedEvents, $"Expected events ({expectedEvents}) count doesn't match triggered events count ({eventsOrrures}):{Environment.NewLine}{String.Join(Environment.NewLine, events)}");

            return events;

            void AddEvent(WatcherChangeTypes eventType, string dir1, string dir2 = "")
            {
                events.Add(new FiredEvent(eventType, dir1, dir2));
                if (Interlocked.Increment(ref eventsOrrures) == expectedEvents)
                {
                    eventsOccured.Set();
                }
            }
        }
    }
}