Units renamed & Chart refactored
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
unit Test.Core.Mutable;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.SyncObjs,
|
||||
System.Classes,
|
||||
DUnitX.TestFramework,
|
||||
Myc.Signals,
|
||||
Myc.Mutable,
|
||||
Myc.Core.Mutable;
|
||||
|
||||
type
|
||||
[TestFixture]
|
||||
TTestCoreMutable = class
|
||||
private
|
||||
FNotifyCount: Integer;
|
||||
procedure SubscriberCallback;
|
||||
public
|
||||
[Setup]
|
||||
procedure Setup;
|
||||
|
||||
[Test]
|
||||
procedure TestNullMutable;
|
||||
|
||||
[Test]
|
||||
procedure TestFuncMutable_GetValue;
|
||||
|
||||
[Test]
|
||||
procedure TestFuncMutable_Signal;
|
||||
|
||||
[Test]
|
||||
[TestCase('Writeable', '10,20')]
|
||||
procedure TestWriteableMutable(InitialValue, NewValue: Integer);
|
||||
|
||||
[Test]
|
||||
procedure TestWriteableMutable_SignalOnSameValue;
|
||||
|
||||
[Test]
|
||||
[TestCase('Protected', '100,200')]
|
||||
procedure TestProtectedMutable_Delegation(InitialValue, NewValue: Integer);
|
||||
|
||||
[Test]
|
||||
procedure TestProtectedMutable_ThreadSafety;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.Generics.Defaults;
|
||||
|
||||
type
|
||||
// A simple subscriber implementation for testing purposes.
|
||||
TTestSubscriber = class(TInterfacedObject, TSignal.ISubscriber)
|
||||
private
|
||||
FOnNotify: TProc;
|
||||
public
|
||||
constructor Create(const AOnNotify: TProc);
|
||||
function Notify: Boolean;
|
||||
end;
|
||||
|
||||
{ TTestSubscriber }
|
||||
|
||||
constructor TTestSubscriber.Create(const AOnNotify: TProc);
|
||||
begin
|
||||
inherited Create;
|
||||
FOnNotify := AOnNotify;
|
||||
end;
|
||||
|
||||
function TTestSubscriber.Notify: Boolean;
|
||||
begin
|
||||
if Assigned(FOnNotify) then
|
||||
begin
|
||||
FOnNotify;
|
||||
end;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
{ TTestCoreMutable }
|
||||
|
||||
procedure TTestCoreMutable.Setup;
|
||||
begin
|
||||
FNotifyCount := 0;
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.SubscriberCallback;
|
||||
begin
|
||||
TInterlocked.Increment(FNotifyCount);
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestNullMutable;
|
||||
var
|
||||
mutable: TMutable<Integer>;
|
||||
begin
|
||||
mutable := TMutable<Integer>.Create(TMycNullMutable<Integer>.Create(42));
|
||||
Assert.AreEqual(42, mutable.Value);
|
||||
// Explicitly cast record helper to fully qualified interface for comparison
|
||||
Assert.IsTrue(TSignal.ISignal(mutable.Changed) = TSignal.Null, 'Changed signal should be Null');
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestFuncMutable_GetValue;
|
||||
var
|
||||
mutable: TMutable<Integer>;
|
||||
callCount: Integer;
|
||||
func: TFunc<Integer>;
|
||||
begin
|
||||
callCount := 0;
|
||||
func := function: Integer
|
||||
begin
|
||||
Inc(callCount);
|
||||
Result := 42;
|
||||
end;
|
||||
mutable := TMutable<Integer>.Construct(TEvent.CreateEvent.Signal, func);
|
||||
|
||||
Assert.AreEqual(42, mutable.Value, 'First value access failed');
|
||||
Assert.AreEqual(1, callCount, 'Function should have been called once');
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestFuncMutable_Signal;
|
||||
var
|
||||
source: TEvent;
|
||||
mutable: TMutable<Integer>;
|
||||
lazy: TLazy<Integer>;
|
||||
func: TFunc<Integer>;
|
||||
begin
|
||||
source := TEvent.CreateEvent;
|
||||
func := function: Integer
|
||||
begin
|
||||
Result := 123;
|
||||
end;
|
||||
mutable := TMutable<Integer>.Construct(source.Signal, func);
|
||||
|
||||
lazy := TLazy<Integer>.Create(mutable);
|
||||
Assert.IsFalse(lazy.Update, 'Lazy wrapper should not detect a change initially.');
|
||||
|
||||
source.Notify;
|
||||
|
||||
Assert.IsTrue(lazy.Update, 'Lazy wrapper should detect the change after signal.');
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestWriteableMutable(InitialValue, NewValue: Integer);
|
||||
var
|
||||
writeable: TWriteable<Integer>;
|
||||
subscriber: TSignal.ISubscriber;
|
||||
subscription: TSignal.TSubscription;
|
||||
exchangedValue: Integer;
|
||||
begin
|
||||
writeable := TWriteable<Integer>.CreateWriteable(InitialValue);
|
||||
subscriber := TTestSubscriber.Create(SubscriberCallback);
|
||||
subscription := writeable.Changed.Subscribe(subscriber);
|
||||
|
||||
writeable.Value := NewValue;
|
||||
Assert.AreEqual(1, FNotifyCount, 'Changed.Notify should have been called on SetValue');
|
||||
|
||||
subscription.Unsubscribe;
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestWriteableMutable_SignalOnSameValue;
|
||||
var
|
||||
writeable: TWriteable<Integer>;
|
||||
subscriber: TSignal.ISubscriber;
|
||||
subscription: TSignal.TSubscription;
|
||||
begin
|
||||
writeable := TWriteable<Integer>.CreateWriteable(100);
|
||||
subscriber := TTestSubscriber.Create(SubscriberCallback);
|
||||
subscription := writeable.Changed.Subscribe(subscriber);
|
||||
|
||||
subscription.Unsubscribe;
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestProtectedMutable_Delegation(InitialValue, NewValue: Integer);
|
||||
var
|
||||
writeable: TWriteable<Integer>;
|
||||
protectedWriteable: TWriteable<Integer>;
|
||||
subscriber: TSignal.ISubscriber;
|
||||
subscription: TSignal.TSubscription;
|
||||
begin
|
||||
writeable := TWriteable<Integer>.CreateWriteable(InitialValue);
|
||||
protectedWriteable := writeable.Protect;
|
||||
subscriber := TTestSubscriber.Create(SubscriberCallback);
|
||||
subscription := protectedWriteable.Changed.Subscribe(subscriber);
|
||||
|
||||
protectedWriteable.Value := NewValue;
|
||||
Assert.AreEqual(1, FNotifyCount, 'Changed.Notify should have been called on SetValue');
|
||||
|
||||
subscription.Unsubscribe;
|
||||
end;
|
||||
|
||||
procedure TTestCoreMutable.TestProtectedMutable_ThreadSafety;
|
||||
const
|
||||
ThreadCount = 8;
|
||||
Iterations = 2500;
|
||||
var
|
||||
writeable: TWriteable<Integer>;
|
||||
threads: TArray<TThread>;
|
||||
i: Integer;
|
||||
begin
|
||||
// This test verifies that the lock prevents crashes under concurrent access.
|
||||
// It does not test for an atomic sum, as the protected mutable does not
|
||||
// offer a combined Compare-and-Exchange operation.
|
||||
writeable := TWriteable<Integer>.CreateWriteable(0).Protect;
|
||||
|
||||
for i := 0 to ThreadCount - 1 do
|
||||
begin
|
||||
threads := threads + [TThread.CreateAnonymousThread(
|
||||
procedure
|
||||
var
|
||||
j: Integer;
|
||||
begin
|
||||
for j := 1 to Iterations do
|
||||
begin
|
||||
// This is an intentional race condition to test lock stability.
|
||||
// The lock protects the GetValue calls individually,
|
||||
// preventing memory corruption during concurrent access.
|
||||
var curr := writeable.Value;
|
||||
writeable.Value := curr + 1;
|
||||
end;
|
||||
end
|
||||
)];
|
||||
end;
|
||||
|
||||
for var thread in threads do
|
||||
begin
|
||||
thread.FreeOnTerminate := false;
|
||||
thread.Start;
|
||||
end;
|
||||
|
||||
for var thread in threads do
|
||||
begin
|
||||
thread.WaitFor;
|
||||
thread.Free;
|
||||
end;
|
||||
|
||||
// We cannot assert a specific value due to the race condition,
|
||||
// but success means the test completed without exceptions.
|
||||
Assert.IsTrue(True, 'Thread safety test completed without exceptions.');
|
||||
end;
|
||||
|
||||
end.
|
||||
Reference in New Issue
Block a user