From cb9d59aa5781eb728a642bb23964cf17a829323d Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 17 Jul 2026 17:39:38 +0200 Subject: [PATCH 1/3] Fix flaky TokensFiredForOldAndNewNamesOnRename test The test waited a fixed 500ms after triggering the rename before asserting that the tokens had fired. PhysicalFilesWatcher.CancelToken cancels the token on a background thread-pool task, so under load (e.g. JitStress on slow arm64) the cancellation was not guaranteed to run within that window, causing the test to intermittently fail. Replace the fixed delay with the deterministic RegisterChangeCallback + TaskCompletionSource pattern already used by the sibling TokensFiredForNewDirectoryContentsOnRename test, awaiting the callbacks with a generous timeout instead. Fixes #129027 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e73cf9d-189a-4b07-96d7-156844a6d647 --- .../tests/PhysicalFileProviderTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs index 7b98222a33e980..5276c90c4dff98 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs @@ -1234,12 +1234,17 @@ public async Task TokensFiredForOldAndNewNamesOnRename() { var oldFileName = Guid.NewGuid().ToString(); var oldToken = provider.Watch(oldFileName); + var oldTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + oldToken.RegisterChangeCallback(_ => oldTcs.TrySetResult(true), null); var newFileName = Guid.NewGuid().ToString(); var newToken = provider.Watch(newFileName); + var newTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + newToken.RegisterChangeCallback(_ => newTcs.TrySetResult(true), null); fileSystemWatcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Renamed, root.Path, newFileName, oldFileName)); - await Task.Delay(WaitTimeForTokenToFire); + + await Task.WhenAll(oldTcs.Task, newTcs.Task).WaitAsync(TimeSpan.FromSeconds(30)); Assert.True(oldToken.HasChanged); Assert.True(newToken.HasChanged); From 180366693e24e61f44763f9c670d8d182b72954e Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 17 Jul 2026 17:40:41 +0200 Subject: [PATCH 2/3] Extract shared timeout fields in PhysicalFileProviderTests Extract the repeated 30-second token-fire timeout into a single s_maxWaitForTokenToFire field and convert the existing WaitTimeForTokenToFire and WaitTimeForTokenCallback constants to TimeSpan fields for consistency. No behavioral change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e73cf9d-189a-4b07-96d7-156844a6d647 --- .../tests/PhysicalFileProviderTests.cs | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs index 5276c90c4dff98..1c9a8ad89e58a7 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs @@ -17,8 +17,9 @@ namespace Microsoft.Extensions.FileProviders { public partial class PhysicalFileProviderTests : FileCleanupTestBase { - private const int WaitTimeForTokenToFire = 500; - private const int WaitTimeForTokenCallback = 10000; + private static readonly TimeSpan s_waitTimeForTokenToFire = TimeSpan.FromMilliseconds(500); + private static readonly TimeSpan s_waitTimeForTokenCallback = TimeSpan.FromSeconds(10); + private static readonly TimeSpan s_maxWaitForTokenToFire = TimeSpan.FromSeconds(30); [Fact] public void Constructor_DoesNotThrow_WhenRootDirectoryDoesNotExist() @@ -117,7 +118,7 @@ public void PollingFileProviderShouldntConsumeINotifyInstances() var oldPollingInterval = PhysicalFilesWatcher.DefaultPollingInterval; try { - PhysicalFilesWatcher.DefaultPollingInterval = TimeSpan.FromMilliseconds(WaitTimeForTokenToFire); + PhysicalFilesWatcher.DefaultPollingInterval = s_waitTimeForTokenToFire; for (int i = 0; i < instances; i++) { PhysicalFileProvider pfp = new PhysicalFileProvider(root.Path) @@ -133,7 +134,7 @@ public void PollingFileProviderShouldntConsumeINotifyInstances() root.CreateFile("test.txt"); // wait for at least one event. - Assert.True(are.WaitOne(WaitTimeForTokenCallback)); + Assert.True(are.WaitOne(s_waitTimeForTokenCallback)); } finally { @@ -374,7 +375,7 @@ public async Task TokensFiredOnFileChange() Assert.True(token.ActiveChangeCallbacks); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } @@ -411,7 +412,7 @@ public async Task TokenCallbackInvokedOnFileChange() }, state: null); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenCallback); + await Task.Delay(s_waitTimeForTokenCallback); Assert.True(callbackInvoked, "Callback should have been invoked"); } @@ -442,7 +443,7 @@ public async Task WatcherWithPolling_ReturnsTrueForFileChangedWhenFileSystemWatc { var token = provider.Watch(fileName); File.WriteAllText(fileLocation, "some-content"); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } } @@ -472,7 +473,7 @@ public async Task WatcherWithPolling_ReturnsTrueForFileRemovedWhenFileSystemWatc var token = provider.Watch(fileName); File.Delete(fileLocation); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } } @@ -499,7 +500,7 @@ public async Task TokensFiredOnFileDeleted() Assert.True(token.ActiveChangeCallbacks); fileSystemWatcher.CallOnDeleted(new FileSystemEventArgs(WatcherChangeTypes.Deleted, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenToFire).ConfigureAwait(false); + await Task.Delay(s_waitTimeForTokenToFire).ConfigureAwait(false); Assert.True(token.HasChanged); } @@ -832,11 +833,11 @@ public async Task FileChangeTokenNotNotifiedAfterExpiry() // Callback expected. fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenCallback); + await Task.Delay(s_waitTimeForTokenCallback); // Callback not expected. fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.Equal(1, invocationCount); } @@ -879,13 +880,13 @@ public async Task CorrectTokensFiredForMultipleFiles() var token2 = provider.Watch(fileName2); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName1)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token1.HasChanged); Assert.False(token2.HasChanged); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName2)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token2.HasChanged); } @@ -915,7 +916,7 @@ public async Task TokenNotAffectedByExceptions() }, null); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenCallback); + await Task.Delay(s_waitTimeForTokenCallback); Assert.True(token.HasChanged); } @@ -1015,7 +1016,7 @@ public async Task TokenFiredOnCreation() var token = provider.Watch(name); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Created, root.Path, name)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } @@ -1040,7 +1041,7 @@ public async Task TokenFiredOnDeletion() var token = provider.Watch(name); fileSystemWatcher.CallOnDeleted(new FileSystemEventArgs(WatcherChangeTypes.Deleted, root.Path, name)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } @@ -1078,7 +1079,7 @@ public async Task TokenFiredForFilesUnderPathEndingWithSlash() newDirectory, directoryName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } @@ -1124,7 +1125,7 @@ private async Task TokenFiredForRelativePathStartingWithSlash(string slashes) var token = provider.Watch(slashes + fileName); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } @@ -1167,7 +1168,7 @@ private async Task TokenNotFiredForInvalidPathStartingWithSlash(string slashes) var token = provider.Watch(slashes + fileName); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.IsType(token); Assert.False(token.HasChanged); @@ -1198,7 +1199,7 @@ public async Task TokenFiredForGlobbingPatternsPointingToSubDirectory() var token = provider.Watch(pattern); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, Path.Combine(root.Path, subDirectoryName, subSubDirectoryName), fileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token.HasChanged); } @@ -1244,7 +1245,7 @@ public async Task TokensFiredForOldAndNewNamesOnRename() fileSystemWatcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Renamed, root.Path, newFileName, oldFileName)); - await Task.WhenAll(oldTcs.Task, newTcs.Task).WaitAsync(TimeSpan.FromSeconds(30)); + await Task.WhenAll(oldTcs.Task, newTcs.Task).WaitAsync(s_maxWaitForTokenToFire); Assert.True(oldToken.HasChanged); Assert.True(newToken.HasChanged); @@ -1312,7 +1313,7 @@ void Fail(object state) fileSystemWatcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Renamed, root.Path, newDirectoryName, oldDirectoryName)); - await Task.WhenAll(oldDirectoryTcs.Task, newDirectoryTcs.Task, newSubDirectoryTcs.Task, newFileTcs.Task).WaitAsync(TimeSpan.FromSeconds(30)); + await Task.WhenAll(oldDirectoryTcs.Task, newDirectoryTcs.Task, newSubDirectoryTcs.Task, newFileTcs.Task).WaitAsync(s_maxWaitForTokenToFire); Assert.False(oldSubDirectoryToken.HasChanged, "Old subdirectory token should not have changed"); Assert.False(oldFileToken.HasChanged, "Old file token should not have changed"); @@ -1343,7 +1344,7 @@ public async Task TokenNotFiredForFileNameStartingWithPeriod() var token = provider.Watch(Path.GetFileName(fileName)); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, fileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.False(token.HasChanged); } @@ -1381,11 +1382,11 @@ public async Task TokensNotFiredForHiddenAndSystemFiles() var systemFiletoken = provider.Watch(Path.GetFileName(systemFileName)); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, hiddenFileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.False(hiddenFiletoken.HasChanged); fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, systemFileName)); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.False(systemFiletoken.HasChanged); } } @@ -1410,7 +1411,7 @@ public async Task TokensFiredForAllEntriesOnError() var token3 = provider.Watch(Guid.NewGuid().ToString()); fileSystemWatcher.CallOnError(new ErrorEventArgs(new Exception())); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); Assert.True(token1.HasChanged); Assert.True(token2.HasChanged); @@ -1440,7 +1441,7 @@ public async Task WildCardToken_RaisesEventsForNewFilesAdded() // Act fileSystemWatcher.CallOnCreated(new FileSystemEventArgs(WatcherChangeTypes.Created, directory, "a.txt")); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); // Assert Assert.True(token.HasChanged); @@ -1473,7 +1474,7 @@ public async Task WildCardToken_RaisesEventsWhenFileSystemWatcherDoesNotFire() // Act fileSystemWatcher.EnableRaisingEvents = false; File.Delete(filePath); - await Task.Delay(WaitTimeForTokenToFire); + await Task.Delay(s_waitTimeForTokenToFire); // Assert Assert.True(token.HasChanged); @@ -1556,7 +1557,7 @@ public async Task UsePollingFileWatcher_UseActivePolling_HasChanged(bool useWild var tcs = new TaskCompletionSource(); changeToken.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var cts = new CancellationTokenSource(s_maxWaitForTokenToFire); cts.Token.Register(() => tcs.TrySetCanceled()); // Act @@ -1586,7 +1587,7 @@ public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_FileDeleted( var tcs = new TaskCompletionSource(); changeToken.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var cts = new CancellationTokenSource(s_maxWaitForTokenToFire); cts.Token.Register(() => tcs.TrySetCanceled()); // Act @@ -1635,7 +1636,7 @@ public async Task CanDeleteWatchedDirectory(bool useActivePolling) var token = provider.Watch(fileName); Directory.Delete(root.Path, true); - await Task.Delay(WaitTimeForTokenToFire).ConfigureAwait(false); + await Task.Delay(s_waitTimeForTokenToFire).ConfigureAwait(false); Assert.True(token.HasChanged); } From ccd435e01edb8d1f92a9d4eac902298d921d6d00 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 17 Jul 2026 17:58:50 +0200 Subject: [PATCH 3/3] Use TaskCompletionSource for token-fired signals Address review feedback: use TaskCompletionSource instead of TaskCompletionSource for the token-fired signals, which keeps the intent explicit and avoids boxing. Apply the same change to the existing signals in TokensFiredForNewDirectoryContentsOnRename for consistency. The non-generic TaskCompletionSource is not used because the test project also targets .NET Framework, where it is unavailable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e73cf9d-189a-4b07-96d7-156844a6d647 --- .../tests/PhysicalFileProviderTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs index 1c9a8ad89e58a7..c8b1e251424456 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs @@ -1235,12 +1235,12 @@ public async Task TokensFiredForOldAndNewNamesOnRename() { var oldFileName = Guid.NewGuid().ToString(); var oldToken = provider.Watch(oldFileName); - var oldTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var oldTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); oldToken.RegisterChangeCallback(_ => oldTcs.TrySetResult(true), null); var newFileName = Guid.NewGuid().ToString(); var newToken = provider.Watch(newFileName); - var newTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var newTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); newToken.RegisterChangeCallback(_ => newTcs.TrySetResult(true), null); fileSystemWatcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Renamed, root.Path, newFileName, oldFileName)); @@ -1259,7 +1259,7 @@ public async Task TokensFiredForOldAndNewNamesOnRename() [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] public async Task TokensFiredForNewDirectoryContentsOnRename() { - var tcsShouldNotFire = new TaskCompletionSource(); + var tcsShouldNotFire = new TaskCompletionSource(); void Fail(object state) { tcsShouldNotFire.TrySetException(new InvalidOperationException("This token should not have fired")); @@ -1287,7 +1287,7 @@ void Fail(object state) File.Create(Path.Combine(root.Path, newDirectoryName, newSubDirectoryName, newFileName)); var oldDirectoryToken = provider.Watch(oldDirectoryName); - var oldDirectoryTcs = new TaskCompletionSource(); + var oldDirectoryTcs = new TaskCompletionSource(); oldDirectoryToken.RegisterChangeCallback(_ => oldDirectoryTcs.TrySetResult(true), null); var oldSubDirectoryToken = provider.Watch(oldSubDirectoryPath); oldSubDirectoryToken.RegisterChangeCallback(Fail, null); @@ -1295,13 +1295,13 @@ void Fail(object state) oldFileToken.RegisterChangeCallback(Fail, null); var newDirectoryToken = provider.Watch(newDirectoryName); - var newDirectoryTcs = new TaskCompletionSource(); + var newDirectoryTcs = new TaskCompletionSource(); newDirectoryToken.RegisterChangeCallback(_ => newDirectoryTcs.TrySetResult(true), null); var newSubDirectoryToken = provider.Watch(newSubDirectoryPath); - var newSubDirectoryTcs = new TaskCompletionSource(); + var newSubDirectoryTcs = new TaskCompletionSource(); newSubDirectoryToken.RegisterChangeCallback(_ => newSubDirectoryTcs.TrySetResult(true), null); var newFileToken = provider.Watch(newFilePath); - var newFileTcs = new TaskCompletionSource(); + var newFileTcs = new TaskCompletionSource(); newFileToken.RegisterChangeCallback(_ => newFileTcs.TrySetResult(true), null); Assert.False(oldDirectoryToken.HasChanged, "Old directory token should not have changed");