Skip to content

Commit 0b17ed5

Browse files
Copilotjtschuster
andauthored
Fix flaky File_Move_Multiple_From_Watched_To_Unwatched_Mac test caused by duplicate FSEvents (#125779)
macOS FSEvents can deliver duplicate `Deleted` events when moving files/directories from a watched to an unwatched directory. The existing `isFilteredOut` filter (Mac path, `skipOldEvents=true`) only discarded stale `Created`/`Changed` events—not duplicate `Deleted` events—causing `ExpectEvents` to signal completion prematurely and collect an extra event: ``` Expected: [Deleted file0, Deleted file1] Actual: [Deleted file0, Deleted file0, Deleted file1] ``` Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
1 parent 7ef9c00 commit 0b17ed5

3 files changed

Lines changed: 19 additions & 2 deletions

File tree

src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Move.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ private void DirectoryMove_Multiple_FromWatchedToUnwatched(int filesCount, bool
173173
Action action = () => Array.ForEach(dirs, dir => Directory.Move(dir.DirectoryInWatchedDir, dir.DirectoryInUnwatchedDir));
174174

175175
// Filter out Created events as there is a race-condition when moving a directory and then observing a parent folder. It receives Create event although Watcher is not registered yet.
176-
Func<FiredEvent, bool>? isFilteredOut = skipOldEvents ? x => x.EventType == WatcherChangeTypes.Created : null;
176+
// Also filter out duplicate events as Mac FSEvents can deliver the same Deleted event multiple times.
177+
Func<FiredEvent, bool>? isFilteredOut = skipOldEvents
178+
? CreateDeduplicatingFilter(WatcherChangeTypes.Created)
179+
: null;
177180

178181
IEnumerable<FiredEvent> events = ExpectEvents(watcher, filesCount, action, isFilteredOut);
179182

src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Move.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ private void FileMove_Multiple_FromWatchedToUnwatched(int filesCount, bool skipO
225225
Action action = () => Array.ForEach(files, file => File.Move(file.FileInWatchedDir, file.FileInUnwatchedDir));
226226

227227
// Filter out Created and Changed events as there is a race-condition when moving a file and then observing a parent folder. It receives Create and Changed events although Watcher is not registered yet.
228-
Func<FiredEvent, bool>? isFilteredOut = skipOldEvents ? x => (x.EventType & (WatcherChangeTypes.Created | WatcherChangeTypes.Changed)) != 0 : null;
228+
// Also filter out duplicate events as Mac FSEvents can deliver the same Deleted event multiple times.
229+
Func<FiredEvent, bool>? isFilteredOut = skipOldEvents
230+
? CreateDeduplicatingFilter(WatcherChangeTypes.Created | WatcherChangeTypes.Changed)
231+
: null;
229232

230233
IEnumerable<FiredEvent> events = ExpectEvents(watcher, filesCount, action, isFilteredOut);
231234

src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Collections.Generic;
56
using System.ComponentModel;
67
using System.Diagnostics;
@@ -505,6 +506,16 @@ public bool Equals(FiredEvent other) => EventType == other.EventType &&
505506

506507
}
507508

509+
// Returns a predicate that returns true for events that should be filtered out.
510+
// Events whose type matches filteredTypes are always filtered; remaining events are deduplicated
511+
// so that only the first occurrence passes through.
512+
// Used on platforms like macOS where FSEvents may deliver the same event more than once.
513+
internal static Func<FiredEvent, bool> CreateDeduplicatingFilter(WatcherChangeTypes filteredTypes = 0)
514+
{
515+
var seenEvents = new ConcurrentDictionary<FiredEvent, bool>();
516+
return firedEvent => (firedEvent.EventType & filteredTypes) != 0 || !seenEvents.TryAdd(firedEvent, true);
517+
}
518+
508519
// Observe until an expected count of events is triggered, otherwise fail. Return all filtered events.
509520
internal static List<FiredEvent> ExpectEvents(FileSystemWatcher watcher, int expectedEvents, Action action, Func<FiredEvent, bool>? isFilteredOut = null)
510521
{

0 commit comments

Comments
 (0)