unit Tests.Myc.DirectoryMonitor; interface uses DUnitX.TestFramework, System.SysUtils, System.IOUtils, System.Classes, System.Generics.Collections, System.SyncObjs, Myc.DirectoryMonitor, Myc.TaskManager, Myc.Signals; type [TestFixture] TTestDirectoryMonitor = class(TObject) private // Class fields for paths, shared across all tests in the fixture class var FTestDir: string; FSubDir1: string; FMultiDir1: string; FMultiDir1a: string; FMultiDir2: string; FMultiDir2a: string; FMonitorFactory: IDirectoryMonitorFactory; procedure CreateEmptyFile(const AFileName: string); class procedure CleanDirectoryContent(const Path: string); class procedure ForceDeleteDirectory(const Path: string); public [SetupFixture] class procedure FixtureSetup; [TearDownFixture] class procedure FixtureTearDown; [Setup] procedure Setup; [TearDown] procedure TearDown; [Test] procedure TestInitialization_Finds_Correct_Initial_Files; [Test] [IgnoreMemoryLeaks] procedure TestFile_Triggers_Changed_Notification; [Test] procedure TestMultiDirectory_MultiExtension_And_Subdirs; [Test] [IgnoreMemoryLeaks] procedure TestFile_Rename_And_Move_Triggers_Correct_Notifications; [Test] procedure TestRapid_CreateDelete_IsHandledCorrectly; [Test] [Timeout(30000)] // 30 second timeout for this potentially slow test procedure TestStress_BufferOverflowRecovery; end; implementation { TTestDirectoryMonitor } class procedure TTestDirectoryMonitor.FixtureSetup; begin // This runs once before any tests in this class. // Initialize all paths // FTestDir := TPath.Combine(TPath.GetTempPath, 'DirectoryMonitorTest'); FTestDir := TPath.Combine('\\COFFEE\TickData\Test', 'DirectoryMonitorTest'); FSubDir1 := TPath.Combine(FTestDir, 'Sub1'); FMultiDir1 := TPath.Combine(FTestDir, 'Dir1'); FMultiDir1a := TPath.Combine(FMultiDir1, 'Sub1A'); FMultiDir2 := TPath.Combine(FTestDir, 'Dir2'); FMultiDir2a := TPath.Combine(FMultiDir2, 'Sub2A'); // Ensure a clean state from previous runs ForceDeleteDirectory(FTestDir); Sleep(200); // Give network file system time to process deletion // Create the entire directory structure needed for all tests TDirectory.CreateDirectory(FTestDir); TDirectory.CreateDirectory(FSubDir1); TDirectory.CreateDirectory(FMultiDir1); TDirectory.CreateDirectory(FMultiDir1a); TDirectory.CreateDirectory(FMultiDir2); TDirectory.CreateDirectory(FMultiDir2a); end; class procedure TTestDirectoryMonitor.FixtureTearDown; begin end; procedure TTestDirectoryMonitor.Setup; begin // This runs before each individual test. // Clean files from previous test, but leave directory structure intact. CleanDirectoryContent(FTestDir); // Get a fresh factory instance for each test. FMonitorFactory := CreateDirectoryMonitorFactory; end; procedure TTestDirectoryMonitor.TearDown; begin // This runs after each individual test. // Release the factory and the monitor it created. FMonitorFactory := nil; end; class procedure TTestDirectoryMonitor.CleanDirectoryContent(const Path: string); begin // Helper to delete all files and recursively clean subdirectories, // leaving the directory structure itself in place. if not TDirectory.Exists(Path) then exit; for var dir in TDirectory.GetDirectories(Path) do CleanDirectoryContent(dir); for var f in TDirectory.GetFiles(Path) do TFile.Delete(f); end; class procedure TTestDirectoryMonitor.ForceDeleteDirectory(const Path: string); begin if not TDirectory.Exists(Path) then exit; CleanDirectoryContent(Path); // Finally, delete the now-empty directory TDirectory.Delete(Path, false); end; procedure TTestDirectoryMonitor.CreateEmptyFile(const AFileName: string); begin TFile.WriteAllText(AFileName, '', TEncoding.UTF8); end; procedure TTestDirectoryMonitor.TestInitialization_Finds_Correct_Initial_Files; var monitor: IDirectoryMonitor; file1_txt: string; file2_log: string; file3_sub_txt: string; begin // Arrange: The directories exist; we only need to create the files. file1_txt := TPath.Combine(FTestDir, 'file1.txt'); file2_log := TPath.Combine(FTestDir, 'file2.log'); file3_sub_txt := TPath.Combine(FSubDir1, 'file3.txt'); CreateEmptyFile(file1_txt); CreateEmptyFile(file2_log); CreateEmptyFile(file3_sub_txt); FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Recursive); FMonitorFactory.AddExtension('.txt'); FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]); // Act monitor := FMonitorFactory.CreateMonitor; // Assert Assert.AreEqual(2, monitor.FileCount, 'Should find exactly 2 .txt files'); Assert.IsNotNull(monitor.Files[file1_txt], 'file1.txt should be found.'); Assert.IsNotNull(monitor.Files[file3_sub_txt], 'file3.txt should be found.'); Assert.IsNull(monitor.Files[file2_log], 'file2.log should be ignored.'); end; procedure TTestDirectoryMonitor.TestFile_Triggers_Changed_Notification; var monitor: IDirectoryMonitor; fileToChange: string; initialFileInfo: IFileInfo; notificationReceived: System.SyncObjs.TEvent; notifiedState: TFileState; receivedFileInfo: IFileInfo; waitResult: TWaitResult; begin // Arrange fileToChange := TPath.Combine(FTestDir, 'change_me.txt'); CreateEmptyFile(fileToChange); FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Ignore); FMonitorFactory.AddExtension('.txt'); FMonitorFactory.SetChangeKinds([TFileChangeKind.LastWriteChange, TFileChangeKind.SizeChange]); monitor := FMonitorFactory.CreateMonitor; initialFileInfo := monitor.Files[fileToChange]; Assert.IsNotNull(initialFileInfo, 'File to change should be present after initialization.'); notificationReceived := System.SyncObjs.TEvent.Create(nil, true, false, ''); try notifiedState := TFileState.Unchanged; receivedFileInfo := nil; initialFileInfo.AddChangeNotification( function(AFileInfo: IFileInfo; ANewState: TFileState): Boolean begin receivedFileInfo := AFileInfo; notifiedState := ANewState; notificationReceived.SetEvent; Result := false; end ); // Act TFile.AppendAllText(fileToChange, 'some new content'); // Assert waitResult := notificationReceived.WaitFor(2000); Assert.AreEqual(TWaitResult.wrSignaled, waitResult, 'Notification event was not signaled in time.'); Assert.AreEqual(TFileState.Changed, notifiedState, 'Notification state should be "Changed".'); finally notificationReceived.Free; end; end; procedure TTestDirectoryMonitor.TestMultiDirectory_MultiExtension_And_Subdirs; var monitor: IDirectoryMonitor; f1_log, f3_tmp, f5_log: string; f2_txt, f6_ignored_tmp: string; begin // Arrange: Directories are pre-created. We just create files. f1_log := TPath.Combine(FMultiDir1, 'file1.log'); f2_txt := TPath.Combine(FMultiDir1, 'file2.txt'); f3_tmp := TPath.Combine(FMultiDir1a, 'file3.tmp'); f5_log := TPath.Combine(FMultiDir2, 'file5.log'); f6_ignored_tmp := TPath.Combine(FMultiDir2a, 'file6.tmp'); CreateEmptyFile(f1_log); CreateEmptyFile(f2_txt); CreateEmptyFile(f3_tmp); CreateEmptyFile(f5_log); CreateEmptyFile(f6_ignored_tmp); FMonitorFactory.AddDirectory(FMultiDir1, TSubDirectoryHandling.Recursive); FMonitorFactory.AddDirectory(FMultiDir2, TSubDirectoryHandling.Ignore); FMonitorFactory.AddExtension('.log'); FMonitorFactory.AddExtension('.tmp'); FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]); // Act monitor := FMonitorFactory.CreateMonitor; // Assert Assert.AreEqual(3, monitor.FileCount, 'Should find 3 files (.log or .tmp) respecting recursion rules.'); Assert.IsNotNull(monitor.Files[f1_log], 'f1.log should be found in Dir1.'); Assert.IsNotNull(monitor.Files[f3_tmp], 'f3.tmp should be found in recursive Dir1/Sub1A.'); Assert.IsNotNull(monitor.Files[f5_log], 'f5.log should be found in Dir2.'); Assert.IsNull(monitor.Files[f2_txt], 'f2.txt should be ignored due to extension mismatch.'); Assert.IsNull(monitor.Files[f6_ignored_tmp], 'f6.tmp should be ignored because Dir2 is not monitored recursively.'); end; procedure TTestDirectoryMonitor.TestFile_Rename_And_Move_Triggers_Correct_Notifications; var monitor: IDirectoryMonitor; originalFile, renamedFile, movedFile: string; countdown: TCountdownEvent; initialInfo: IFileInfo; waitResult: TWaitResult; begin // Arrange originalFile := TPath.Combine(FTestDir, 'original.txt'); renamedFile := TPath.Combine(FTestDir, 'renamed.txt'); movedFile := TPath.Combine(FSubDir1, 'moved.txt'); CreateEmptyFile(originalFile); FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Recursive); FMonitorFactory.AddExtension('.txt'); FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]); monitor := FMonitorFactory.CreateMonitor; countdown := TCountdownEvent.Create(1); try initialInfo := monitor.Files[originalFile]; Assert.IsNotNull(initialInfo, 'Initial file must exist in monitor.'); initialInfo.AddChangeNotification( function(AFileInfo: IFileInfo; ANewState: TFileState): Boolean begin if ANewState = TFileState.Deleted then countdown.Signal; Result := true; end ); // Act & Assert 1: Rename TFile.Move(originalFile, renamedFile); waitResult := countdown.WaitFor(2000); Assert.AreEqual(TWaitResult.wrSignaled, waitResult, 'Delete notification for original file did not arrive in time.'); Sleep(100); Assert.IsNotNull(monitor.Files[renamedFile], 'Renamed file should be present in monitor.'); // Arrange 2 countdown.Reset(1); var renamedInfo := monitor.Files[renamedFile]; Assert.IsNotNull(renamedInfo); renamedInfo.AddChangeNotification( function(AFileInfo: IFileInfo; ANewState: TFileState): Boolean begin if ANewState = TFileState.Deleted then countdown.Signal; Result := true; end ); // Act & Assert 2: Move TFile.Move(renamedFile, movedFile); waitResult := countdown.WaitFor(2000); Assert.AreEqual(TWaitResult.wrSignaled, waitResult, 'Delete notification for renamed file did not arrive in time.'); Sleep(100); Assert.IsNotNull(monitor.Files[movedFile], 'Moved file should be present in monitor.'); Assert.AreEqual(1, monitor.FileCount, 'Monitor should only contain one file at the end.'); finally countdown.Free; end; end; procedure TTestDirectoryMonitor.TestRapid_CreateDelete_IsHandledCorrectly; var monitor: IDirectoryMonitor; rapidFile: string; begin // Arrange rapidFile := TPath.Combine(FTestDir, 'rapid.txt'); FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Ignore); FMonitorFactory.AddExtension('.txt'); FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]); monitor := FMonitorFactory.CreateMonitor; // Act CreateEmptyFile(rapidFile); TFile.Delete(rapidFile); Sleep(500); // Assert Assert.AreEqual(0, monitor.FileCount, 'Monitor should be empty after rapid create/delete.'); Assert.IsNull(monitor.Files[rapidFile], 'The rapidly created/deleted file should not exist in the monitor.'); end; procedure TTestDirectoryMonitor.TestStress_BufferOverflowRecovery; const NumFiles = 4000; var monitor: IDirectoryMonitor; i: Integer; filePath: string; begin // Arrange FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Ignore); FMonitorFactory.AddExtension('.tmp'); FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]); monitor := FMonitorFactory.CreateMonitor; // Act for i := 1 to NumFiles do begin filePath := TPath.Combine(FTestDir, IntToStr(i) + '.tmp'); CreateEmptyFile(filePath); end; Sleep(5000); // Assert Assert.AreEqual(NumFiles, monitor.FileCount, 'Monitor should recover from overflow and report the correct file count.'); end; end.