using FileManager; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; using System.Text.RegularExpressions; using System.Threading; namespace BackgroundFileSystemTests; /// /// The first tests for this class, prompted by a CI run where every test passed and the run still failed: /// FileLiberator.Tests exited with 0xE0434352 and /// InvalidOperationException: The collection has been marked as complete with regards to additions, /// thrown from FileSystemWatcher_Changed. Disposing while the OS still had events buffered took the process /// down, because on Windows those arrive on a native completion callback where nothing catches anything. /// [TestClass] [DoNotParallelize] public class DisposeWhileEventsAreArriving { private string tempDir = string.Empty; [TestInitialize] public void Initialize() { tempDir = Path.Combine(Path.GetTempPath(), $"libation-bfs-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); } [TestCleanup] public void Cleanup() { try { Directory.Delete(tempDir, recursive: true); } catch (IOException) { // A leftover temp directory is not worth failing a test over. } } /// /// Churns the directory hard enough that events are in flight, then disposes underneath them. The assertion /// that matters is that the process is still alive afterwards: before the fix this crashed the test host /// rather than failing anything. /// [TestMethod] public void disposing_under_a_flood_of_events_does_not_crash() { for (var attempt = 0; attempt < 20; attempt++) { var sut = new BackgroundFileSystem(tempDir, "*.*", SearchOption.AllDirectories); for (var i = 0; i < 50; i++) File.WriteAllText(Path.Combine(tempDir, $"file-{attempt}-{i}.txt"), "x"); // no wait: the point is to dispose while the watcher still has events to deliver sut.Dispose(); foreach (var file in Directory.GetFiles(tempDir)) File.Delete(file); } // give any late callback the chance to arrive and take the process with it Thread.Sleep(250); } [TestMethod] public void disposing_twice_is_harmless() { var sut = new BackgroundFileSystem(tempDir, "*.*", SearchOption.AllDirectories); sut.Dispose(); sut.Dispose(); } [TestMethod] public void files_present_before_and_after_construction_are_both_found() { File.WriteAllText(Path.Combine(tempDir, "before.txt"), "x"); using var sut = new BackgroundFileSystem(tempDir, "*.*", SearchOption.AllDirectories); Assert.IsNotNull(sut.FindFile(new Regex(@"before\.txt$"))); File.WriteAllText(Path.Combine(tempDir, "after.txt"), "x"); // the watcher feeds a background scanner, so the new file appears when it gets there var found = WaitFor(() => sut.FindFile(new Regex(@"after\.txt$")) is not null); Assert.IsTrue(found, "a file created after construction never reached the cache"); } private static bool WaitFor(Func condition, int timeoutMs = 5000) { for (var waited = 0; waited < timeoutMs; waited += 50) { if (condition()) return true; Thread.Sleep(50); } return false; } }