440 lines
16 KiB
ObjectPascal
440 lines
16 KiB
ObjectPascal
unit TestNotifier_ChaosStress;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils,
|
|
System.Classes, // For TThread
|
|
System.SyncObjs, // For TInterlocked, TCriticalSection (needed by implementation)
|
|
System.Generics.Collections, // For TList<T>
|
|
System.Diagnostics, // For TStopwatch
|
|
DUnitX.TestFramework,
|
|
Myc.Core.Notifier; // Your unit
|
|
|
|
type
|
|
IMyStressTestInterface = interface(IInterface)
|
|
['{B8A3F7E1-47D9-4A9C-8C2B-035A1B568E7F}']
|
|
procedure Foo(NotificationType: Integer); // NotificationType for more detailed testing
|
|
function GetValue: Integer;
|
|
function GetNotificationCount: Integer;
|
|
function GetInstanceID: Integer;
|
|
function GetExpectedToBeAdvised: Boolean;
|
|
procedure SetExpectedToBeAdvised(const Value: Boolean);
|
|
property ExpectedToBeAdvised: Boolean read GetExpectedToBeAdvised write SetExpectedToBeAdvised;
|
|
end;
|
|
|
|
TMyStressReceiver = class(TInterfacedObject, IMyStressTestInterface)
|
|
private
|
|
FInstanceID: Integer;
|
|
FValue: Integer;
|
|
FNotificationCount: Integer;
|
|
FExpectedToBeAdvised: Boolean;
|
|
function GetExpectedToBeAdvised: Boolean;
|
|
procedure SetExpectedToBeAdvised(const Value: Boolean);
|
|
public
|
|
// For testing: flag to indicate if the thread that created this believes it's still advised
|
|
constructor Create(AInstanceID, AValue: Integer);
|
|
procedure Foo(NotificationType: Integer);
|
|
function GetValue: Integer;
|
|
function GetNotificationCount: Integer;
|
|
function GetInstanceID: Integer;
|
|
property ExpectedToBeAdvised: Boolean read GetExpectedToBeAdvised write SetExpectedToBeAdvised;
|
|
end;
|
|
|
|
// Record type to store an advised receiver and its tag
|
|
TAdvisedReceiverInfo = record
|
|
Tag: TMycNotifyList<IMyStressTestInterface>.TTag;
|
|
Receiver: IMyStressTestInterface;
|
|
end;
|
|
|
|
TMycNotifierChaosStressTests = class; // Forward declaration
|
|
|
|
TStressWorkerThread = class(TThread)
|
|
private
|
|
FOwnerFixture: TMycNotifierChaosStressTests;
|
|
FThreadID: Integer;
|
|
FIterations: Integer;
|
|
FError: Exception;
|
|
// List of receivers this thread has successfully advised and believes are still active
|
|
FMyAdvisedReceivers: TList<TAdvisedReceiverInfo>;
|
|
protected
|
|
procedure Execute; override;
|
|
public
|
|
constructor Create(AOwnerFixture: TMycNotifierChaosStressTests; AThreadID, AIterations: Integer);
|
|
destructor Destroy; override;
|
|
property Error: Exception read FError;
|
|
function GetAdvisedReceiverCountByThread: Integer;
|
|
// procedure GetMyExpectedLiveReceivers(AList: TList<TMyStressReceiver>); // Not strictly needed with current verification
|
|
end;
|
|
|
|
[TestFixture]
|
|
TMycNotifierChaosStressTests = class(TObject)
|
|
public
|
|
FNotifier: TMycNotifyList<IMyStressTestInterface>;
|
|
// List of all receivers ever created in this test run, for final verification
|
|
FAllReceiversCreated: TList<IMyStressTestInterface>;
|
|
FCriticalSectionForList: TCriticalSection; // To protect FAllReceiversCreated
|
|
FNextReceiverInstanceID: Integer; // Atomically incremented ID for receivers
|
|
|
|
[Setup]
|
|
procedure Setup;
|
|
[TearDown]
|
|
procedure TearDown;
|
|
|
|
[Test]
|
|
procedure Test_MixedOperations_Stress;
|
|
end;
|
|
|
|
implementation
|
|
|
|
// uses System.StrUtils; // Not needed anymore if IfThen is not used
|
|
|
|
{ TMyStressReceiver }
|
|
constructor TMyStressReceiver.Create(AInstanceID, AValue: Integer);
|
|
begin
|
|
inherited Create;
|
|
FInstanceID := AInstanceID;
|
|
FValue := AValue;
|
|
FNotificationCount := 0;
|
|
FExpectedToBeAdvised := False; // Initially not advised or expecting to be
|
|
end;
|
|
|
|
procedure TMyStressReceiver.Foo(NotificationType: Integer);
|
|
begin
|
|
TInterlocked.Increment(FNotificationCount); // Atomic increment
|
|
end;
|
|
|
|
function TMyStressReceiver.GetExpectedToBeAdvised: Boolean;
|
|
begin
|
|
Result := FExpectedToBeAdvised;
|
|
end;
|
|
|
|
function TMyStressReceiver.GetValue: Integer;
|
|
begin
|
|
Result := FValue;
|
|
end;
|
|
|
|
function TMyStressReceiver.GetNotificationCount: Integer;
|
|
begin
|
|
// Atomic read, ensuring visibility of changes from other threads if FNotificationCount was public
|
|
// For private field accessed by owner, direct read is fine if Notify is serialized.
|
|
// TInterlocked.Add(var Target, Value) returns Target+Value. For read: Target+0.
|
|
Result := TInterlocked.Add(FNotificationCount, 0);
|
|
end;
|
|
|
|
function TMyStressReceiver.GetInstanceID: Integer;
|
|
begin
|
|
Result := FInstanceID;
|
|
end;
|
|
|
|
procedure TMyStressReceiver.SetExpectedToBeAdvised(const Value: Boolean);
|
|
begin
|
|
FExpectedToBeAdvised := Value;
|
|
end;
|
|
|
|
{ TStressWorkerThread }
|
|
constructor TStressWorkerThread.Create(AOwnerFixture: TMycNotifierChaosStressTests; AThreadID, AIterations: Integer);
|
|
begin
|
|
inherited Create(True); // Creates the thread in a suspended state
|
|
FreeOnTerminate := False; // The test fixture will free the thread
|
|
FOwnerFixture := AOwnerFixture;
|
|
FThreadID := AThreadID;
|
|
FIterations := AIterations;
|
|
FError := nil;
|
|
FMyAdvisedReceivers := TList<TAdvisedReceiverInfo>.Create;
|
|
end;
|
|
|
|
destructor TStressWorkerThread.Destroy;
|
|
begin
|
|
FMyAdvisedReceivers.Free;
|
|
inherited;
|
|
end;
|
|
|
|
procedure TStressWorkerThread.Execute;
|
|
var
|
|
i, op: Integer;
|
|
newReceiver: TMyStressReceiver;
|
|
tag: TMycNotifyList<IMyStressTestInterface>.TTag;
|
|
itemIndex: Integer;
|
|
advisedItemRec: TAdvisedReceiverInfo;
|
|
localInstanceID: Integer;
|
|
begin
|
|
inherited; // Important for TThread
|
|
try
|
|
for i := 1 to FIterations do
|
|
begin
|
|
if Terminated then
|
|
Break; // Respond to termination request
|
|
|
|
op := Random(100);
|
|
|
|
if op < 35 then // 35% chance to Advise
|
|
begin
|
|
localInstanceID := TInterlocked.Increment(FOwnerFixture.FNextReceiverInstanceID);
|
|
newReceiver := TMyStressReceiver.Create(localInstanceID, FThreadID * 10000 + i);
|
|
// newReceiver.ExpectedToBeAdvised is False by default, will be set to True by this thread upon successful advise.
|
|
|
|
FOwnerFixture.FCriticalSectionForList.Enter;
|
|
try
|
|
FOwnerFixture.FAllReceiversCreated.Add(newReceiver);
|
|
finally
|
|
FOwnerFixture.FCriticalSectionForList.Leave;
|
|
end;
|
|
|
|
FOwnerFixture.FNotifier.Lock;
|
|
try
|
|
tag := FOwnerFixture.FNotifier.Advise(newReceiver);
|
|
advisedItemRec.Tag := tag;
|
|
advisedItemRec.Receiver := newReceiver;
|
|
FMyAdvisedReceivers.Add(advisedItemRec);
|
|
newReceiver.ExpectedToBeAdvised := True; // This thread now expects it to be advised
|
|
finally
|
|
FOwnerFixture.FNotifier.Release;
|
|
end;
|
|
end
|
|
else if (op < 70) and (FMyAdvisedReceivers.Count > 0) then // 35% chance to Unadvise (if possible)
|
|
begin
|
|
itemIndex := Random(FMyAdvisedReceivers.Count);
|
|
advisedItemRec := FMyAdvisedReceivers[itemIndex];
|
|
|
|
FOwnerFixture.FNotifier.Lock;
|
|
try
|
|
FOwnerFixture.FNotifier.Unadvise(advisedItemRec.Tag);
|
|
advisedItemRec.Receiver.ExpectedToBeAdvised := False; // This thread no longer expects it to be advised
|
|
FMyAdvisedReceivers.Delete(itemIndex);
|
|
finally
|
|
FOwnerFixture.FNotifier.Release;
|
|
end;
|
|
end
|
|
else // 30% chance to Notify
|
|
begin
|
|
FOwnerFixture.FNotifier.Lock;
|
|
try
|
|
// if not FOwnerFixture.FNotifier.IsFinalized then // Don't notify if finalized
|
|
begin
|
|
FOwnerFixture.FNotifier.Notify(
|
|
function(Item: IMyStressTestInterface): Boolean
|
|
begin
|
|
Item.Foo(FThreadID); // Pass ThreadID as notification type for context
|
|
Result := True; // Keep item
|
|
end
|
|
);
|
|
end;
|
|
finally
|
|
FOwnerFixture.FNotifier.Release;
|
|
end;
|
|
end;
|
|
|
|
if (i mod 75 = 0) then
|
|
Sleep(0); // Yield occasionally to encourage context switching
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
FError := E; // Store the first error that occurred in the thread
|
|
end;
|
|
end;
|
|
|
|
function TStressWorkerThread.GetAdvisedReceiverCountByThread: Integer;
|
|
begin
|
|
Result := FMyAdvisedReceivers.Count;
|
|
end;
|
|
|
|
// Not strictly needed if main test iterates FAllReceiversCreated and checks ExpectedToBeAdvised flag
|
|
// procedure TStressWorkerThread.GetMyExpectedLiveReceivers(AList: TList<TMyStressReceiver>);
|
|
// var
|
|
// itemRec: TAdvisedReceiverInfo;
|
|
// begin
|
|
// AList.Clear;
|
|
// for itemRec in FMyAdvisedReceivers do
|
|
// begin
|
|
// AList.Add(itemRec.Receiver);
|
|
// end;
|
|
// end;
|
|
|
|
{ TMycNotifierChaosStressTests }
|
|
procedure TMycNotifierChaosStressTests.Setup;
|
|
begin
|
|
Randomize; // Initialize random number generator
|
|
FNotifier.Create;
|
|
FAllReceiversCreated := TList<IMyStressTestInterface>.Create;
|
|
FCriticalSectionForList := TCriticalSection.Create;
|
|
FNextReceiverInstanceID := 0;
|
|
end;
|
|
|
|
procedure TMycNotifierChaosStressTests.TearDown;
|
|
var
|
|
// receiver: TMyStressReceiver; // Not used directly in this simplified TearDown
|
|
i: Integer;
|
|
begin
|
|
// Attempt to shut down the Notifier cleanly.
|
|
try
|
|
FNotifier.Lock; // Lock for the cleanup process
|
|
try
|
|
// UnadviseAll should be safe if the IsFinalized guard is present
|
|
// and the FFirst state is not pathological.
|
|
// if not FNotifier.IsFinalized then
|
|
begin
|
|
FNotifier.UnadviseAll;
|
|
end;
|
|
finally
|
|
if FNotifier.IsLocked then // Only release if the lock is still held
|
|
FNotifier.Release;
|
|
end;
|
|
except
|
|
// Suppress errors during teardown to avoid interfering with other tests.
|
|
// In a real test environment, this would be logged.
|
|
end;
|
|
|
|
// FNotifier is a record field of the fixture and is "destroyed" with it when the fixture is destroyed.
|
|
// Setup reinitializes it for each test.
|
|
|
|
if Assigned(FAllReceiversCreated) then
|
|
begin
|
|
// ARC will handle TMyStressReceiver instances when the list is freed/cleared,
|
|
// as long as no other strong references exist.
|
|
// Setting list items to nil is good practice if the list itself isn't immediately freed.
|
|
for i := 0 to FAllReceiversCreated.Count - 1 do
|
|
FAllReceiversCreated[i] := nil; // Break reference cycles if any, allow ARC
|
|
FAllReceiversCreated.Free;
|
|
FAllReceiversCreated := nil;
|
|
end;
|
|
if Assigned(FCriticalSectionForList) then
|
|
begin
|
|
FCriticalSectionForList.Free;
|
|
FCriticalSectionForList := nil;
|
|
end;
|
|
end;
|
|
|
|
procedure TMycNotifierChaosStressTests.Test_MixedOperations_Stress;
|
|
const
|
|
NumThreads = 24; // Number of concurrent worker threads
|
|
IterationsPerThread = 2000; // Number of random operations per thread
|
|
var
|
|
threads: array of TStressWorkerThread;
|
|
i: Integer;
|
|
LThreadError: Exception;
|
|
totalExpectedLiveByThreadsAtEnd, actualLiveInNotifierAtEnd: Integer;
|
|
actualLiveReceiversInNotifier: TList<IMyStressTestInterface>; // Store interface type from Notify
|
|
receiver: IMyStressTestInterface;
|
|
found: Boolean;
|
|
stopwatch: TStopwatch;
|
|
begin
|
|
stopwatch := TStopwatch.StartNew;
|
|
|
|
SetLength(threads, NumThreads);
|
|
for i := 0 to NumThreads - 1 do
|
|
threads[i] := TStressWorkerThread.Create(Self, i + 1, IterationsPerThread);
|
|
|
|
for i := 0 to NumThreads - 1 do
|
|
threads[i].Start;
|
|
|
|
totalExpectedLiveByThreadsAtEnd := 0;
|
|
for i := 0 to NumThreads - 1 do
|
|
begin
|
|
threads[i].WaitFor;
|
|
LThreadError := threads[i].Error;
|
|
if Assigned(LThreadError) then
|
|
begin
|
|
// If an error occurred, Assert.IsNull would fail.
|
|
// The message can safely use LThreadError.Message here.
|
|
Assert.IsNull(LThreadError, 'Thread ' + threads[i].FThreadID.ToString + ' reported an error: ' + LThreadError.Message);
|
|
end
|
|
else
|
|
begin
|
|
// If no error, Assert.IsNull will pass. No custom message needed for success.
|
|
Assert.IsNull(LThreadError);
|
|
end;
|
|
totalExpectedLiveByThreadsAtEnd := totalExpectedLiveByThreadsAtEnd + threads[i].GetAdvisedReceiverCountByThread;
|
|
threads[i].Free;
|
|
end;
|
|
SetLength(threads, 0);
|
|
|
|
// --- Verification Phase ---
|
|
|
|
// 1. Get all items actually live in the notifier
|
|
actualLiveReceiversInNotifier := TList<IMyStressTestInterface>.Create;
|
|
try
|
|
FNotifier.Lock;
|
|
try
|
|
// if not FNotifier.IsFinalized then
|
|
begin
|
|
FNotifier.Notify(
|
|
function(Item: IMyStressTestInterface): Boolean
|
|
begin
|
|
actualLiveReceiversInNotifier.Add(Item);
|
|
Result := True;
|
|
end
|
|
);
|
|
end;
|
|
finally
|
|
FNotifier.Release;
|
|
end;
|
|
actualLiveInNotifierAtEnd := actualLiveReceiversInNotifier.Count;
|
|
|
|
// 2. Compare overall counts
|
|
Assert.AreEqual(
|
|
totalExpectedLiveByThreadsAtEnd,
|
|
actualLiveInNotifierAtEnd,
|
|
Format(
|
|
'Mismatch in live item count at end. Threads expected %d, Notifier has %d.',
|
|
[totalExpectedLiveByThreadsAtEnd, actualLiveInNotifierAtEnd]
|
|
)
|
|
);
|
|
|
|
// 3. Detailed check: Iterate all receivers ever created.
|
|
// Their 'ExpectedToBeAdvised' flag (last known state from its managing thread)
|
|
// should match their presence in 'actualLiveReceiversInNotifier'.
|
|
FCriticalSectionForList.Enter;
|
|
try
|
|
for receiver in FAllReceiversCreated do
|
|
begin
|
|
// Check if this receiver (by instance) is in the list of interfaces retrieved from Notify
|
|
found := False;
|
|
for var liveIntf in actualLiveReceiversInNotifier do
|
|
begin
|
|
if liveIntf = receiver then // Compare object instances
|
|
begin
|
|
found := True;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
Assert.AreEqual(
|
|
receiver.ExpectedToBeAdvised,
|
|
found,
|
|
Format(
|
|
'Receiver ID %d: Thread expected it to be advised=%d, but its presence in Notifier is %d. NotificationCount=%d',
|
|
[receiver.GetInstanceID, Integer(receiver.ExpectedToBeAdvised), Integer(found), receiver.GetNotificationCount]
|
|
)
|
|
);
|
|
|
|
// Further checks on NotificationCount could be added if specific notification patterns were expected.
|
|
// For this chaos test, ensuring count is non-negative and consistent with advised state is a good start.
|
|
Assert.IsTrue(
|
|
receiver.GetNotificationCount >= 0,
|
|
Format(
|
|
'Receiver ID %d has non-positive notification count: %d',
|
|
[receiver.GetInstanceID, receiver.GetNotificationCount]
|
|
)
|
|
);
|
|
if found and (receiver.GetNotificationCount = 0) then
|
|
begin
|
|
// This might be okay if Notify calls were very sparse or the item was just added and not yet notified.
|
|
// For a long running stress test, one might expect > 0 if Notifies happened while it was live.
|
|
// Log.Debug(Format('Receiver ID %d found in notifier but has 0 notifications.', [receiver.GetInstanceID]));
|
|
end;
|
|
end;
|
|
finally
|
|
FCriticalSectionForList.Leave;
|
|
end;
|
|
finally
|
|
actualLiveReceiversInNotifier.Free;
|
|
end;
|
|
|
|
stopwatch.Stop;
|
|
end;
|
|
|
|
initialization
|
|
TDUnitX.RegisterTestFixture(TMycNotifierChaosStressTests);
|
|
end.
|