Files
MycLib/Test/Tests.Myc.DirectoryMonitor.pas
T
Michael Schimmel d2c47843a7 DataRecord
2025-07-17 23:00:56 +02:00

373 lines
13 KiB
ObjectPascal

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.