Test Refactoring of signals

This commit is contained in:
Michael Schimmel
2025-06-05 22:11:54 +02:00
parent c9817ca200
commit e06a023742
17 changed files with 528 additions and 774 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ type
TMycGateFuncFuture<T> = class(TMycFuture<T>)
private
FInit: TState.TSubscription;
FDone: IMycLatch;
FDone: IMycEvent;
FResult: T;
protected
function GetResult: T; override;
@@ -54,7 +54,7 @@ constructor TMycGateFuncFuture<T>.Create(const ATaskManager: IMycTaskManager; co
begin
inherited Create;
FDone := TLatch.Construct(1);
FDone := TEvent.CreateLatch(1);
// Subscribe the job execution to AGate.
// The job will run when AGate notifies the subscriber returned by Run.
+101 -26
View File
@@ -8,38 +8,61 @@ uses
Myc.Lazy;
type
TMycLazy<T> = class abstract(TInterfacedObject, IMycLazy<T>)
TMycNullLazy<T> = class(TInterfacedObject, IMycLazy<T>)
protected
function GetChanged: IMycState; virtual; abstract;
function GetChanged: TState;
public
function Pop(out Res: T): Boolean; virtual; abstract;
function Pop(out Res: T): Boolean;
end;
TMycNullLazy<T> = class(TMycLazy<T>)
protected
function GetChanged: IMycState; override;
public
function Pop(out Res: T): Boolean; override;
end;
TMycFuncLazy<T> = class(TMycLazy<T>)
private
FChanged: IMycDirty;
TMycLazyBase<T> = class(TInterfacedObject, IMycLazy<T>)
strict private
FChanged: TFlag;
FChangeState: TState.TSubscription;
function GetChanged: TState;
protected
function GetValue: T; virtual; abstract;
public
constructor Create(const AChanged: TState);
destructor Destroy; override;
function Pop(out Res: T): Boolean;
end;
TMycFuncLazy<T> = class(TMycLazyBase<T>)
private
FProc: TFunc<T>;
protected
function GetChanged: IMycState; override;
function GetValue: T; override;
public
constructor Create(const AChanged: TState; const AProc: TFunc<T>);
destructor Destroy; override;
function Pop(out Res: T): Boolean; override;
end;
TMycChainedLazy<T> = class(TMycLazyBase<T>)
private
FValue: IMycLazy<T>;
protected
function GetValue: T; override;
public
constructor Create(const AValue: IMycLazy<T>);
end;
TMycMutable<T> = class(TInterfacedObject, IMycLazy<T>, IMycMutable<T>)
private
FNotifier: TEvent;
FValue: T;
protected
function GetChanged: TState;
public
constructor Create;
function Pop(out Res: T): Boolean;
procedure SendValue(const Value: T);
end;
implementation
{ TMycNullLazy<T> }
function TMycNullLazy<T>.GetChanged: IMycState;
function TMycNullLazy<T>.GetChanged: TState;
begin
Result := TState.Null;
end;
@@ -50,36 +73,88 @@ begin
Result := true;
end;
{ TMycFuncLazy<T> }
{ TMycLazyBase<T> }
constructor TMycFuncLazy<T>.Create(const AChanged: TState; const AProc: TFunc<T>);
constructor TMycLazyBase<T>.Create(const AChanged: TState);
begin
inherited Create;
FChanged := TDirty.Construct;
FProc := AProc;
FChanged := TFlag.CreateFlag;
FChangeState := AChanged.Subscribe(FChanged);
end;
destructor TMycFuncLazy<T>.Destroy;
destructor TMycLazyBase<T>.Destroy;
begin
FChangeState.Unsubscribe;
inherited;
end;
function TMycFuncLazy<T>.GetChanged: IMycState;
function TMycLazyBase<T>.GetChanged: TState;
begin
Result := FChanged.State;
end;
function TMycFuncLazy<T>.Pop(out Res: T): Boolean;
function TMycLazyBase<T>.Pop(out Res: T): Boolean;
begin
Result := FChanged.State.IsSet;
if Result then
begin
if Assigned(FProc) then
Res := FProc;
Res := GetValue;
FChanged.Reset;
end;
end;
{ TMycFuncLazy<T> }
constructor TMycFuncLazy<T>.Create(const AChanged: TState; const AProc: TFunc<T>);
begin
inherited Create( AChanged );
FProc := AProc;
Assert( Assigned(FProc) );
end;
function TMycFuncLazy<T>.GetValue: T;
begin
Result := FProc();
end;
{ TMycChainedLazy<T> }
constructor TMycChainedLazy<T>.Create(const AValue: IMycLazy<T>);
begin
inherited Create( AValue.Changed );
FValue := AValue;
end;
function TMycChainedLazy<T>.GetValue: T;
begin
if not FValue.Pop( Result ) then
raise Exception.Create('Lazy chain already popped');
end;
{ TMycMutable<T> }
constructor TMycMutable<T>.Create;
begin
inherited Create;
FNotifier := TEvent.CreateNotifier;
FValue := Default(T);
end;
function TMycMutable<T>.GetChanged: TState;
begin
Result := FNotifier.State;
end;
function TMycMutable<T>.Pop(out Res: T): Boolean;
begin
Res := FValue;
exit(true);
end;
procedure TMycMutable<T>.SendValue(const Value: T);
begin
FValue := Value;
FNotifier.Notify;
end;
end.
+116 -48
View File
@@ -8,32 +8,38 @@ uses
Myc.Signals;
type
TMycState = class abstract(TInterfacedObject, IMycState)
protected
function GetIsSet: Boolean; virtual; abstract;
public
function Subscribe(Subscriber: IMycSubscriber): Pointer; virtual; abstract;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag); virtual; abstract;
property IsSet: Boolean read GetIsSet;
end;
// TMycNullState implements a "null object" pattern for IMycState.
// This state is always considered "set".
TMycNullState = class(TInterfacedObject, IMycState)
protected
// IMycState
function GetIsSet: Boolean;
function Subscribe(Subscriber: IMycSubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
public
constructor Create;
function Subscribe(Subscriber: IMycSubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
end;
// A state that acts as an event. It's state ist always set and it will always notify it's subscribers, if it gets notified by itself.
TMycNotifyEvent = class(TInterfacedObject, IMycState, IMycEvent)
strict private
FSubscribers: TMycNotifyList<IMycSubscriber>;
protected
function GetIsSet: Boolean;
function GetState: TState;
public
constructor Create;
destructor Destroy; override;
function Subscribe(Subscriber: IMycSubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
function Notify: Boolean;
end;
// TMycLatch implements a countdown latch.
// It is initialized with a count. Calls to its Notify method (as an IMycSubscriber)
// decrement this count. When the count reaches zero, the latch transitions to the "set"
// state (IsSet becomes true), notifies its current subscribers once, and then remains set.
TMycLatch = class(TMycState, IMycLatch)
TMycLatch = class(TInterfacedObject, IMycState, IMycEvent)
strict private
FSubscribers: TMycNotifyList<IMycSubscriber>; // List of subscribers waiting for this latch to be set.
private
@@ -41,7 +47,7 @@ type
FCount: Integer; // The internal countdown value for the latch.
function GetState: TState; // Implementation for IMycLatch.GetState and IMycFlag.GetState.
protected
function GetIsSet: Boolean; override; final; // Implementation for IMycState.GetIsSet.
function GetIsSet: Boolean;
public
// Creates the latch with an initial count.
// If ACount is 0 or less, the latch is effectively pre-set (delegates to FNull via CreateLatch factory).
@@ -49,8 +55,8 @@ type
destructor Destroy; override;
// IMycSignal implementation
function Subscribe(Subscriber: IMycSubscriber): Pointer; override;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag); override;
function Subscribe(Subscriber: IMycSubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
// IMycSubscriber implementation for TMycLatch itself.
// Decrements the internal count. If the count reaches zero,
@@ -58,16 +64,16 @@ type
function Notify: Boolean;
end;
TMycNullLatch = class(TInterfacedObject, IMycLatch)
TMycNullEvent = class(TInterfacedObject, IMycEvent)
private
function GetState: TState;
function Notify: Boolean;
end;
// TMycDirty implements a resettable "dirty flag".
// TMycFlag implements a resettable "dirty flag".
// It can be explicitly set to dirty (via Notify) or reset to clean (via Reset).
// Subscribers are notified of relevant state changes.
TMycDirty = class(TMycState, IMycDirty)
TMycFlag = class(TInterfacedObject, IMycState, IMycFlag)
strict private
FSubscribers: TMycNotifyList<IMycSubscriber>; // List of subscribers interested in state changes of this dirty flag.
private
@@ -75,27 +81,27 @@ type
FFlag: Boolean; // Internal state: true if dirty/set, false if clean/reset.
function GetState: TState;
protected
function GetIsSet: Boolean; override; final; // Implementation for IMycState.GetIsSet (true if dirty).
function GetIsSet: Boolean;
public
// Creates a new dirty flag, initially set to dirty (true).
constructor Create;
destructor Destroy; override;
// Factory method to create a new TMycDirty instance.
class function CreateDirty: IMycDirty; static;
// Factory method to create a new TMycFlag instance.
class function CreateDirty: IMycFlag; static;
function Subscribe(Subscriber: IMycSubscriber): Pointer; override;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag); override;
function Subscribe(Subscriber: IMycSubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
// IMycSubscriber implementation for TMycDirty.
// IMycSubscriber implementation for TMycFlag.
// Sets this flag to dirty (true). Notifies subscribers if it transitioned from clean to dirty.
function Notify: Boolean;
// IMycDirty implementation. Resets the flag to clean (false).
// IMycFlag implementation. Resets the flag to clean (false).
function Reset: Boolean;
end;
TMycNullDirty = class(TInterfacedObject, IMycDirty)
TMycNullFlag = class(TInterfacedObject, IMycFlag)
private
function GetState: TState;
function Notify: Boolean;
@@ -135,14 +141,76 @@ begin
// Unsubscribing from a null state has no effect.
end;
{ TMycNullLatch }
{ TMycNotifyEvent }
function TMycNullLatch.GetState: TState;
constructor TMycNotifyEvent.Create;
begin
inherited Create;
FSubscribers.Create;
end;
destructor TMycNotifyEvent.Destroy;
begin
FSubscribers.Destroy;
inherited;
end;
function TMycNotifyEvent.GetIsSet: Boolean;
begin
Result := true;
end;
function TMycNotifyEvent.GetState: TState;
begin
Result := Self;
end;
function TMycNotifyEvent.Notify: Boolean;
begin
FSubscribers.Lock;
try
FSubscribers.Notify(function(Subscriber: IMycSubscriber): Boolean begin Result := Subscriber.Notify; end);
finally
FSubscribers.Release;
end;
end;
function TMycNotifyEvent.Subscribe(Subscriber: IMycSubscriber): Pointer;
begin
Result := nil;
if not Assigned(Subscriber) then
exit;
FSubscribers.Lock;
try
Subscriber.Notify;
Result := FSubscribers.Advise(Subscriber);
finally
FSubscribers.Release;
end;
end;
procedure TMycNotifyEvent.Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
begin
if Tag = nil then
exit;
FSubscribers.Lock;
try
FSubscribers.Unadvise(Tag);
finally
FSubscribers.Release;
end;
end;
{ TMycNullEvent }
function TMycNullEvent.GetState: TState;
begin
Result := TState.Null;
end;
function TMycNullLatch.Notify: Boolean;
function TMycNullEvent.Notify: Boolean;
begin
Result := false;
end;
@@ -246,63 +314,63 @@ begin
end;
end;
{ TMycNullDirty }
{ TMycNullFlag }
function TMycNullDirty.GetState: TState;
function TMycNullFlag.GetState: TState;
begin
Result := TState.Null;
end;
function TMycNullDirty.Notify: Boolean;
function TMycNullFlag.Notify: Boolean;
begin
Result := false;
end;
function TMycNullDirty.Reset: Boolean;
function TMycNullFlag.Reset: Boolean;
begin
Result := true;
end;
{ TMycDirty }
{ TMycFlag }
constructor TMycDirty.Create;
constructor TMycFlag.Create;
begin
inherited Create;
FFlag := true; // A new dirty flag is initially considered dirty.
FSubscribers.Create;
end;
class function TMycDirty.CreateDirty: IMycDirty;
class function TMycFlag.CreateDirty: IMycFlag;
begin
// Factory method to create a new TMycDirty instance.
Result := TMycDirty.Create;
// Factory method to create a new TMycFlag instance.
Result := TMycFlag.Create;
end;
destructor TMycDirty.Destroy;
destructor TMycFlag.Destroy;
begin
FSubscribers.Destroy; // Clean up the subscriber list.
inherited;
end;
function TMycDirty.Reset: Boolean;
function TMycFlag.Reset: Boolean;
begin
// Sets the flag to false (clean) and returns true if it was previously true (dirty).
Result := TInterlocked.Exchange(FFlag, false);
end;
function TMycDirty.GetIsSet: Boolean;
function TMycFlag.GetIsSet: Boolean;
begin
// IsSet is true if the flag is dirty (FFlag = true).
Result := FFlag;
end;
function TMycDirty.GetState: TState;
function TMycFlag.GetState: TState;
begin
// TMycDirty itself implements IMycState.
// TMycFlag itself implements IMycState.
Result := Self;
end;
function TMycDirty.Notify: Boolean;
function TMycFlag.Notify: Boolean;
var
wasPreviouslyClean: Boolean;
begin
@@ -316,7 +384,7 @@ begin
FSubscribers.Notify(function(Subscriber: IMycSubscriber): Boolean begin Result := Subscriber.Notify; end);
end;
// The return value of this Notify (as an IMycSubscriber) indicates if this
// TMycDirty instance itself would want more notifications if it were subscribed to something.
// TMycFlag instance itself would want more notifications if it were subscribed to something.
// Here, it reflects whether a state change to dirty occurred due to this call.
Result := wasPreviouslyClean;
finally
@@ -324,12 +392,12 @@ begin
end;
end;
function TMycDirty.Subscribe(Subscriber: IMycSubscriber): Pointer;
function TMycFlag.Subscribe(Subscriber: IMycSubscriber): Pointer;
begin
if not Assigned(Subscriber) then
exit(nil);
// For TMycDirty, we always add the subscriber and then check if we need to notify immediately.
// For TMycFlag, we always add the subscriber and then check if we need to notify immediately.
// Ref counting for the subscriber interface is assumed to be handled by TMycNotifyList.Advise/Unadvise.
// Or, if explicit management is needed like in TMycLatch, it should be added.
// For consistency with TMycLatch, let's add AddRef/Release here too.
@@ -346,7 +414,7 @@ begin
end;
end;
procedure TMycDirty.Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
procedure TMycFlag.Unsubscribe(Tag: TMycNotifyList<IMycSubscriber>.TTag);
begin
if Tag = nil then
exit;
+3 -3
View File
@@ -203,11 +203,11 @@ end;
function TMycTaskFactory.CreateThread(const Proc: TProc): IMycState;
var
res: IMycLatch;
res: IMycEvent;
capturedProc: TProc;
begin
// Create a latch that will be signaled when the proc finishes
res := TLatch.Construct(1); // Changed to use direct static call on TMycLatch
res := TEvent.CreateLatch(1);
capturedProc := Proc; // Capture Proc for the anonymous method
CreateAnonymousThread(
@@ -309,7 +309,7 @@ begin
raise ETaskException.Create('Task factory terminated');
if not Assigned(Job) then
exit(TLatch.Null); // Already correct, uses direct static property on TMycLatch
exit(TEvent.Null); // Already correct, uses direct static property on TMycLatch
// Create a pending job
Result := TMycPendingJob.Create(Self, Job);
+16 -3
View File
@@ -7,12 +7,19 @@ uses
Myc.Signals;
type
IMycMutable<T> = interface
function GetChanged: TState;
function GetValue: T;
property Changed: TState read GetChanged;
property Value: T read GetValue;
end;
IMycLazy<T> = interface
{$REGION 'property access'}
function GetChanged: IMycState;
function GetChanged: TState;
{$ENDREGION}
function Pop(out Res: T): Boolean;
property Changed: IMycState read GetChanged;
property Changed: TState read GetChanged;
end;
TLazy<T> = record
@@ -26,7 +33,8 @@ type
public
constructor Create(const ALazy: IMycLazy<T>);
class function Construct(const Changing: IMycState; const Proc: TFunc<T>): IMycLazy<T>; static;
class function Construct(const Changing: IMycState; const Proc: TFunc<T>): IMycLazy<T>; overload; static;
class function Construct(const Value: IMycLazy<T>): IMycLazy<T>; overload; static;
class operator Implicit(const A: IMycLazy<T>): TLazy<T>; overload;
class operator Implicit(const A: TLazy<T>): IMycLazy<T>; overload;
@@ -58,6 +66,11 @@ begin
FNull := TMycNullLazy<T>.Create;
end;
class function TLazy<T>.Construct(const Value: IMycLazy<T>): IMycLazy<T>;
begin
Result := TMycChainedLazy<T>.Create(Value);
end;
function TLazy<T>.GetChanged: IMycState;
begin
Result := FLazy.Changed;
+110 -51
View File
@@ -61,44 +61,44 @@ type
property IsSet: Boolean read GetIsSet;
end;
// IMycLatch is a specific type of flag that, once set, remains set (non-resettable).
// It typically becomes set when an internal countdown reaches zero.
IMycLatch = interface(IMycSubscriber)
// Provides access to the IMycState interface of the flag.
IMycEvent = interface(IMycSubscriber)
function GetState: TState;
property State: TState read GetState;
end;
TLatch = record
TEvent = record
strict private
class var
FNull: IMycLatch;
FNull: IMycEvent;
class constructor ClassCreate;
private
FLatch: IMycLatch;
FLatch: IMycEvent;
function GetState: TState; inline;
public
constructor Create(const ALatch: IMycLatch);
class operator Initialize(out Dest: TLatch);
class operator Implicit(const A: IMycLatch): TLatch; overload;
class operator Implicit(const A: TLatch): IMycLatch; overload;
constructor Create(const ALatch: IMycEvent);
class operator Initialize(out Dest: TEvent);
class operator Implicit(const A: IMycEvent): TEvent; overload;
class operator Implicit(const A: TEvent): IMycEvent; overload;
class function Construct(Count: Integer): TLatch; static;
// A notifier is an event that acts as a multi-cast event. Its state is always set and it forwards all notifications the all subscribers.
class function CreateNotifier: TEvent; static;
class function Enqueue(var Gate: TLatch; Count: Integer = 1): TState; static;
// A latch is a specific type of event that, once set, remains set (non-resettable).
// It becomes set when the internal countdown reaches zero.
class function CreateLatch(Count: Integer): TEvent; static;
class property Null: IMycLatch read FNull;
class function Enqueue(var Gate: TEvent; Count: Integer = 1): TState; static;
class property Null: IMycEvent read FNull;
function Notify: Boolean; inline;
property State: TState read GetState;
end;
// IMycDirty represents a resettable flag, typically indicating if a State is "dirty" (requiring attention) or "clean".
// It inherits from IMycFlag, meaning it has a State, can be notified, and subscribed to.
IMycDirty = interface(IMycSubscriber)
// Provides access to the IMycState interface of the flag.
// IMycFlag represents a resettable flag, typically indicating if a State is "dirty" (requiring attention) or "clean".
IMycFlag = interface(IMycSubscriber)
function GetState: TState;
// Resets the flag to its "clean" (not set / not dirty) State.
// Returns true if the flag was actually dirty before this reset, false otherwise.
@@ -106,14 +106,29 @@ type
property State: TState read GetState;
end;
TDirty = record
TFlag = record
strict private
class var
FNull: IMycDirty;
FNull: IMycFlag;
class constructor ClassCreate;
private
FDirty: IMycFlag;
function GetState: TState; inline;
public
class function Construct: IMycDirty; static;
class property Null: IMycDirty read FNull;
constructor Create(const ADirty: IMycFlag);
class operator Implicit(const A: IMycFlag): TFlag; overload;
class operator Implicit(const A: TFlag): IMycFlag; overload;
class operator Initialize(out Dest: TFlag);
class function CreateFlag: IMycFlag; static;
class property Null: IMycFlag read FNull;
function Notify: Boolean; inline;
function Reset: Boolean;
property State: TState read GetState;
end;
implementation
@@ -161,9 +176,9 @@ end;
class function TState.All(const States: TArray<TState>): TState;
var
Latch: IMycLatch;
Latch: IMycEvent;
begin
Latch := TLatch.Construct(Length(States));
Latch := TEvent.CreateLatch(Length(States));
for var i := 0 to High(States) do
States[i].Subscribe(Latch);
Result := Latch.State;
@@ -171,7 +186,7 @@ end;
class function TState.Any(const States: TArray<TState>): TState;
var
Latch: IMycLatch;
Latch: IMycEvent;
begin
if Length(States) = 0 then
begin
@@ -179,7 +194,7 @@ begin
end
else
begin
Latch := TLatch.Construct(1);
Latch := TEvent.CreateLatch(1);
for var i := 0 to High(States) do
States[i].Subscribe(Latch);
Result := Latch.State;
@@ -211,24 +226,75 @@ begin
Result.Create(A);
end;
{ TLatch }
{ TFlag }
class constructor TLatch.ClassCreate;
class constructor TFlag.ClassCreate;
begin
// Create a singleton null latch instance that is initially (and always) set.
FNull := TMycNullLatch.Create; // Calls the instance constructor
FNull := TMycNullFlag.Create;
end;
{ TLatch }
{ TFlag }
constructor TLatch.Create(const ALatch: IMycLatch);
constructor TFlag.Create(const ADirty: IMycFlag);
begin
FDirty := ADirty;
if not Assigned(FDirty) then
FDirty := FNull;
end;
class function TFlag.CreateFlag: IMycFlag;
begin
Result := TMycFlag.Create;
end;
function TFlag.GetState: TState;
begin
Result := FDirty.State;
end;
function TFlag.Notify: Boolean;
begin
Result := FDirty.Notify;
end;
function TFlag.Reset: Boolean;
begin
Result := FDirty.Reset;
end;
class operator TFlag.Implicit(const A: IMycFlag): TFlag;
begin
Result.Create(A);
end;
class operator TFlag.Implicit(const A: TFlag): IMycFlag;
begin
Result := A.FDirty;
end;
class operator TFlag.Initialize(out Dest: TFlag);
begin
Dest.FDirty := FNull;
end;
{ TEvent }
class constructor TEvent.ClassCreate;
begin
// Create a singleton null latch instance that is initially (and always) set.
FNull := TMycNullEvent.Create;
end;
{ TEvent }
constructor TEvent.Create(const ALatch: IMycEvent);
begin
FLatch := ALatch;
if not Assigned(FLatch) then
FLatch := FNull;
end;
class function TLatch.Construct(Count: Integer): TLatch;
class function TEvent.CreateLatch(Count: Integer): TEvent;
begin
if Count > 0 then
Result := TMycLatch.Create(Count)
@@ -236,7 +302,12 @@ begin
Result := FNull;
end;
class function TLatch.Enqueue(var Gate: TLatch; Count: Integer = 1): TState;
class function TEvent.CreateNotifier: TEvent;
begin
Result := TMycNotifyEvent.Create;
end;
class function TEvent.Enqueue(var Gate: TEvent; Count: Integer = 1): TState;
begin
var gateState := Gate.State;
Gate := TMycLatch.Create(Count);
@@ -244,41 +315,29 @@ begin
exit(Gate.State);
end;
function TLatch.GetState: TState;
function TEvent.GetState: TState;
begin
Result := FLatch.State;
end;
function TLatch.Notify: Boolean;
function TEvent.Notify: Boolean;
begin
Result := FLatch.Notify;
end;
class operator TLatch.Implicit(const A: IMycLatch): TLatch;
class operator TEvent.Implicit(const A: IMycEvent): TEvent;
begin
Result.Create(A);
end;
class operator TLatch.Implicit(const A: TLatch): IMycLatch;
class operator TEvent.Implicit(const A: TEvent): IMycEvent;
begin
Result := A.FLatch;
end;
class operator TLatch.Initialize(out Dest: TLatch);
class operator TEvent.Initialize(out Dest: TEvent);
begin
Dest.FLatch := FNull;
end;
{ TDirty }
class constructor TDirty.ClassCreate;
begin
FNull := TMycNullDirty.Create;
end;
class function TDirty.Construct: IMycDirty;
begin
Result := TMycDirty.Create;
end;
end.
+22 -22
View File
@@ -162,9 +162,9 @@ procedure TTestMycCoreLazy.TestFuncLazy_GetChanged_IsAlwaysTrueAfterCreation;
var
funcLazy: IMycLazy<Integer>;
changedState: IMycState;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State, function: Integer begin Result := 10; end);
Assert.IsNotNull(funcLazy, 'TMycFuncLazy<Integer> instance should not be nil');
@@ -178,11 +178,11 @@ var
funcLazy: IMycLazy<Integer>;
value: Integer;
result: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
procExecuted: Boolean;
expectedValue: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
procExecuted := False;
expectedValue := 50;
@@ -211,9 +211,9 @@ var
value: Integer;
preCallValue: Integer;
result: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State, nil);
Assert.IsNotNull(funcLazy, 'TMycFuncLazy<Integer> instance should not be nil');
@@ -235,10 +235,10 @@ end;
procedure TTestMycCoreLazy.TestFuncLazy_AfterFirstPop_IfNoSourceChange_GetChangedIsFalse;
var
funcLazy: IMycLazy<Integer>;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
initialProcValue: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
initialProcValue := 33;
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State, function: Integer begin Result := initialProcValue; end);
@@ -251,11 +251,11 @@ var
funcLazy: IMycLazy<Integer>;
value: Integer;
result: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
initialProcValue: Integer;
procExecutedCount: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
initialProcValue := 44;
procExecutedCount := 0;
@@ -279,10 +279,10 @@ procedure TTestMycCoreLazy.TestFuncLazy_AfterSourceChange_GetChanged_IsSet;
var
funcLazy: IMycLazy<Integer>;
changedState: IMycState;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
initialProcValue: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
initialProcValue := 20;
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State, function: Integer begin Result := initialProcValue; end);
@@ -297,11 +297,11 @@ var
funcLazy: IMycLazy<Integer>;
value: Integer;
result: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
procCallCount: Integer;
currentExpectedValue: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
procCallCount := 0;
funcLazy :=
@@ -335,10 +335,10 @@ var
funcLazy: IMycLazy<Integer>;
value: Integer;
resultPop1, resultPop2: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
procCallCount: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
procCallCount := 0;
funcLazy :=
@@ -367,11 +367,11 @@ var
funcLazy: IMycLazy<Integer>;
value: Integer;
result: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
procCallCount: Integer;
initialValue, firstSourceChangeValue: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset; // sourceDirty.IsSet is FALSE
procCallCount := 0;
initialValue := 51;
@@ -427,10 +427,10 @@ end;
procedure TTestMycCoreLazy.TestFuncLazy_Destroy_UnsubscribesFromSource;
var
funcLazyObj: TMycFuncLazy<Integer>;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
tempVal: Integer;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
sourceDirty.Reset;
funcLazyObj := TMycFuncLazy<Integer>.Create(sourceDirty.State, function: Integer begin Result := 1; end);
Assert.IsTrue(funcLazyObj.Pop(tempVal), 'Initial Pop should succeed');
@@ -448,11 +448,11 @@ var
funcLazy: IMycLazy<Integer>;
value: Integer;
result: Boolean;
sourceDirty: IMycDirty;
sourceDirty: IMycFlag;
expectedValue: Integer;
procExecuted: Boolean;
begin
sourceDirty := TDirty.Construct;
sourceDirty := TFlag.CreateFlag;
Assert.IsTrue(sourceDirty.State.IsSet, 'SourceDirty should be initially set for this test scenario');
expectedValue := 70;
procExecuted := False;
+4 -4
View File
@@ -12,7 +12,7 @@ type
[TestFixture]
TTestMyLazy = class(TObject)
private
FChangingSignal: IMycDirty; // Used as the 'Changing' state for functional lazy objects
FChangingSignal: IMycFlag; // Used as the 'Changing' state for functional lazy objects
// Helper to consume the initial pop, which is always expected to succeed
// for a TLazy wrapping a functional lazy object due to "Changed.IsSet initially true" design.
@@ -76,7 +76,7 @@ begin
// Create a common signal source for tests that need it.
// TState.CreateDirty is from Myc.Signals.pas (interface part)
// Its implementation might rely on Myc.Core.Signals, but that's an indirect usage.
FChangingSignal := TDirty.Construct;
FChangingSignal := TFlag.CreateFlag;
FChangingSignal.Reset; // Start with a clean (not set) signal for predictable test starts
end;
@@ -306,9 +306,9 @@ end;
procedure TTestMyLazy.TestConstruct_Destruction_UnsubscribesAndNoCrashOnSourceNotify;
var
lazyIntf: IMycLazy<Integer>;
localChangingSignal: IMycDirty; // Use a local signal for this test to control its lifetime
localChangingSignal: IMycFlag; // Use a local signal for this test to control its lifetime
begin
localChangingSignal := TDirty.Construct;
localChangingSignal := TFlag.CreateFlag;
localChangingSignal.Reset;
lazyIntf := TLazy<Integer>.Construct(localChangingSignal.State, function: Integer begin Result := 1; end);
+512
View File
@@ -0,0 +1,512 @@
unit Myc.Test.Signals.Latch;
interface
uses
DUnitX.TestFramework,
System.SysUtils,
Myc.Signals; // Unit under test, using TMycLatch static members
type
// Helper class to mock a subscriber.
TMockSubscriber = class(TInterfacedObject, IMycSubscriber)
public
NotifyCount: Integer;
NotifyShouldReturn: Boolean; // Determines what this mock's Notify method returns
WasCalled: Boolean;
constructor Create(AShouldReturn: Boolean = True); // Parameter name AShouldReturn for clarity
function Notify: Boolean; // IMycSubscriber implementation.
end;
[TestFixture]
TTestMycLatch = class(TObject)
public
[Setup]
procedure Setup;
[TearDown]
procedure TearDown;
// --- Category 1: Basic Counter and State Functionality (Parameterized) ---
[TestCase('InitialPositive', '1,False')]
[TestCase('InitialZero_ReturnsNullLatch', '0,True')]
[TestCase('InitialNegative_ReturnsNullLatch', '-1,True')]
[TestCase('InitialLargePositive', '5,False')]
procedure TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean);
[TestCase('DecrementPositiveToZero', '1,False,True')] // InitialCount, ExpectedReturnFromLatchNotify, ExpectedIsSetAfterNotify
[TestCase('DecrementPositiveToPositive', '2,True,False')]
[TestCase('DecrementZeroToNegative', '0,False,True')] // Latch.Notify on count 0 returns false (0 > 0 is false)
[TestCase('DecrementNegativeToNegative', '-1,False,True')] // Latch.Notify on count -1 returns false (-1 > 0 is false)
procedure TestNotify_ReturnValueAndState_AfterOneNotify(InitialCount: Integer; ExpectedReturn: Boolean; ExpectedIsSet: Boolean);
[Test]
procedure TestNotify_DecrementsCounter_StateChanges;
[Test]
procedure TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet;
// --- Category 2: Notification to Subscribers ---
[Test]
procedure TestSubscriber_Single_IsNotifiedWhenLatchSet;
[Test]
procedure TestSubscriber_Multiple_AreNotifiedWhenLatchSet;
[Test]
procedure TestSubscriber_NotNotified_BeforeLatchSet;
[Test]
procedure TestSubscriber_NotifiedOnlyOnce_AfterLatchSetAndUnadvised; // Clarified name
[Test]
procedure TestSubscribe_ToAlreadySetLatch_NotifiesImmediately;
// --- Category 3: Chaining and Cascading Latches ---
[Test]
procedure TestChaining_A_Notifies_B;
[Test]
procedure TestCascade_A_Notifies_B_Notifies_C;
[Test]
procedure TestChaining_B_NeedsMultipleNotifiesFromA_WithSubscriberOnB;
// --- Category 4: Special Cases ---
[Test]
procedure TestInitiallySetLatch_NotifiesNewSubscriberImmediately; // Using CreateLatch(0)
[Test]
procedure TestUnsubscribe_SubscriberNotNotified;
// --- Category 5: Counter Integrity / State for Later Subscribers ---
[Test]
procedure TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber;
// --- Category 6: UnadviseAll Behavior on Latch Set ---
[Test]
procedure TestLatchSet_ClearsSubscribers_NoFurtherNotification;
[Test]
procedure TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll;
[Test]
procedure TestLatchSet_NewSubscriberToSetLatch_NotifiedImmediatelyButNotPersistedForLatchNotify;
end;
implementation
{ TMockSubscriber }
constructor TMockSubscriber.Create(AShouldReturn: Boolean = True);
begin
inherited Create;
NotifyCount := 0;
NotifyShouldReturn := AShouldReturn;
WasCalled := False;
end;
function TMockSubscriber.Notify: Boolean;
begin
Inc(NotifyCount);
WasCalled := True;
Result := NotifyShouldReturn; // This mock subscriber returns what it's configured to.
end;
{ TTestMycLatch }
procedure TTestMycLatch.Setup;
begin
// Per-test setup code can go here (if any).
end;
procedure TTestMycLatch.TearDown;
begin
// Per-test teardown code can go here (if any).
end;
// --- Category 1: Basic Counter and State Functionality (Parameterized) ---
procedure TTestMycLatch.TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean);
var
latch: IMycEvent;
begin
// TMycLatch.CreateLatch returns TMycLatch.Null if InitialCount <= 0
latch := TEvent.CreateLatch(InitialCount);
Assert.AreEqual(ExpectedIsSet, latch.State.IsSet, 'Latch initial IsSet state mismatch for count ' + IntToStr(InitialCount) + '.');
end;
procedure TTestMycLatch.TestNotify_ReturnValueAndState_AfterOneNotify(
InitialCount: Integer;
ExpectedReturn: Boolean;
ExpectedIsSet: Boolean
);
var
latch: IMycEvent;
returnedValueFromNotify: Boolean;
begin
latch := TEvent.CreateLatch(InitialCount);
returnedValueFromNotify := latch.Notify; // This is IMycEvent (as IMycSubscriber).Notify
Assert.AreEqual(
ExpectedReturn,
returnedValueFromNotify,
'Unexpected return value from Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.'
);
Assert.AreEqual(
ExpectedIsSet,
latch.State.IsSet,
'Unexpected IsSet state after Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.'
);
end;
procedure TTestMycLatch.TestNotify_DecrementsCounter_StateChanges;
var
latch: IMycEvent;
begin
latch := TEvent.CreateLatch(1);
Assert.IsFalse(latch.State.IsSet, 'Latch should initially not be set (Count=1).');
latch.Notify; // Count becomes 0
Assert.IsTrue(latch.State.IsSet, 'Latch should be set after Notify (Count became 0).');
end;
procedure TTestMycLatch.TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet;
var
latch: IMycEvent;
begin
latch := TEvent.CreateLatch(1);
latch.Notify; // Count becomes 0, IsSet = True
Assert.IsTrue(latch.State.IsSet, 'Latch should be set after first Notify.');
latch.Notify; // Count becomes -1, IsSet should remain True
Assert.IsTrue(latch.State.IsSet, 'Latch should remain set after second Notify.');
end;
// --- Category 2: Notification to Subscribers ---
procedure TTestMycLatch.TestSubscriber_Single_IsNotifiedWhenLatchSet;
var
latch: IMycEvent;
mockSub: TMockSubscriber;
subscription: TState.TSubscription;
begin
latch := TEvent.CreateLatch(1);
mockSub := TMockSubscriber.Create;
subscription := latch.State.Subscribe(mockSub); // Subscribing to the Latch's state
Assert.IsFalse(latch.State.IsSet, 'Latch should initially not be set.');
Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not have been notified yet (before latch set).');
latch.Notify; // Latch becomes set, notifies subscribers
Assert.IsTrue(latch.State.IsSet, 'Latch should be set after its Notify is called.');
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified once when latch becomes set.');
end;
procedure TTestMycLatch.TestSubscriber_Multiple_AreNotifiedWhenLatchSet;
var
latch: IMycEvent;
mockSub1, mockSub2, mockSub3: TMockSubscriber;
sub1, sub2, sub3: TState.TSubscription; // Keep subscriptions in scope
begin
latch := TEvent.CreateLatch(1);
mockSub1 := TMockSubscriber.Create;
mockSub2 := TMockSubscriber.Create;
mockSub3 := TMockSubscriber.Create;
sub1 := latch.State.Subscribe(mockSub1);
sub2 := latch.State.Subscribe(mockSub2);
sub3 := latch.State.Subscribe(mockSub3);
latch.Notify; // Latch becomes set
Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notification count mismatch.');
Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notification count mismatch.');
Assert.AreEqual(1, mockSub3.NotifyCount, 'MockSub3 notification count mismatch.');
end;
procedure TTestMycLatch.TestSubscriber_NotNotified_BeforeLatchSet;
var
latch: IMycEvent;
mockSub: TMockSubscriber;
subscription: TState.TSubscription;
begin
latch := TEvent.CreateLatch(2); // Count = 2
mockSub := TMockSubscriber.Create;
subscription := latch.State.Subscribe(mockSub);
Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified upon subscription if latch is not set.');
latch.Notify; // Count becomes 1, still not set
Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should still not be notified as latch is not yet set (Count=1).');
end;
procedure TTestMycLatch.TestSubscriber_NotifiedOnlyOnce_AfterLatchSetAndUnadvised;
var
latch: IMycEvent;
mockSub: TMockSubscriber;
subscription: TState.TSubscription;
begin
latch := TEvent.CreateLatch(1);
mockSub := TMockSubscriber.Create;
subscription := latch.State.Subscribe(mockSub);
latch.Notify; // Sets latch, notifies MockSub. Subscribers are then unadvised by UnadviseAll.
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should be 1 after latch set.');
latch.Notify; // Call Notify on latch again (FCount becomes more negative).
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should remain 1 (not notified again as it was unadvised).');
end;
procedure TTestMycLatch.TestSubscribe_ToAlreadySetLatch_NotifiesImmediately;
var
latch: IMycEvent;
mockSub1, mockSub2: TMockSubscriber;
subscription1, subscription2: TState.TSubscription;
begin
latch := TEvent.CreateLatch(1); // Create with count 1
mockSub1 := TMockSubscriber.Create;
subscription1 := latch.State.Subscribe(mockSub1); // Subscribe before set
latch.Notify; // Latch becomes set. mockSub1 is notified.
Assert.IsTrue(latch.State.IsSet, 'Latch should be set after initial trigger.');
Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once.');
// Attempt to subscribe mockSub2 after the latch is already set.
mockSub2 := TMockSubscriber.Create;
subscription2 := latch.State.Subscribe(mockSub2); // TMycLatch.Subscribe (via TMycAbstractFlag) notifies immediately if already set.
Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should be notified immediately upon subscribing to an already set latch.');
latch.Notify; // Call Notify on latch again (FCount becomes more negative).
Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notify count should remain 1 (was unadvised).');
Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notify count should remain 1 (was only notified on subscribe, not added to list).');
end;
// --- Category 3: Chaining and Cascading Latches ---
procedure TTestMycLatch.TestChaining_A_Notifies_B;
var
latchA, latchB: IMycEvent;
mockSubForB: TMockSubscriber;
subHandle_B_listens_A, subHandle_Mock_listens_B: TState.TSubscription;
begin
latchA := TEvent.CreateLatch(1);
latchB := TEvent.CreateLatch(1); // latchB is an IMycSubscriber
mockSubForB := TMockSubscriber.Create;
subHandle_Mock_listens_B := latchB.State.Subscribe(mockSubForB);
// LatchA's state is an IMycSignal. LatchB (as IMycSubscriber) subscribes to it.
subHandle_B_listens_A := latchA.State.Subscribe(latchB);
Assert.IsFalse(latchA.State.IsSet, 'LatchA initially not set.');
Assert.IsFalse(latchB.State.IsSet, 'LatchB initially not set.');
Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB initially not notified.');
latchA.Notify; // Call Notify on LatchA (as IMycSubscriber)
Assert.IsTrue(latchA.State.IsSet, 'LatchA should be set after notify.');
Assert.IsTrue(latchB.State.IsSet, 'LatchB should be set after being notified by LatchA.');
Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by LatchB.');
end;
procedure TTestMycLatch.TestCascade_A_Notifies_B_Notifies_C;
var
latchA, latchB, latchC: IMycEvent;
mockSubForC: TMockSubscriber;
sub_Mock_C, sub_C_B, sub_B_A: TState.TSubscription;
begin
latchA := TEvent.CreateLatch(1);
latchB := TEvent.CreateLatch(1);
latchC := TEvent.CreateLatch(1);
mockSubForC := TMockSubscriber.Create;
sub_Mock_C := latchC.State.Subscribe(mockSubForC);
sub_C_B := latchB.State.Subscribe(latchC); // LatchC subscribes to LatchB's state
sub_B_A := latchA.State.Subscribe(latchB); // LatchB subscribes to LatchA's state
latchA.Notify; // Call Notify on LatchA
Assert.IsTrue(latchA.State.IsSet, 'LatchA state mismatch in cascade.');
Assert.IsTrue(latchB.State.IsSet, 'LatchB state mismatch in cascade.');
Assert.IsTrue(latchC.State.IsSet, 'LatchC state mismatch in cascade.');
Assert.AreEqual(1, mockSubForC.NotifyCount, 'MockSubForC notification count mismatch in cascade.');
end;
procedure TTestMycLatch.TestChaining_B_NeedsMultipleNotifiesFromA_WithSubscriberOnB;
var
latchA, latchB: IMycEvent;
mockSubForB: TMockSubscriber;
sub_Mock_B, sub_B_A: TState.TSubscription;
begin
// LatchA needs 2 notifies to become set. LatchB needs 1 notify to become set.
// LatchB subscribes to LatchA. So LatchB will be notified once LatchA becomes set.
latchA := TEvent.CreateLatch(2);
latchB := TEvent.CreateLatch(1);
mockSubForB := TMockSubscriber.Create;
sub_Mock_B := latchB.State.Subscribe(mockSubForB);
sub_B_A := latchA.State.Subscribe(latchB);
latchA.Notify; // First notify for LatchA (count becomes 1)
Assert.IsFalse(latchA.State.IsSet, 'LatchA should not be set after first notify of two.');
Assert.IsFalse(latchB.State.IsSet, 'LatchB should not have been triggered yet by LatchA.');
Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB should not have been notified yet.');
latchA.Notify; // Second notify for LatchA (count becomes 0, LatchA becomes set)
Assert.IsTrue(latchA.State.IsSet, 'LatchA should be set after second notify.');
// When LatchA becomes set, it notifies its subscribers (LatchB). LatchB.Notify is called.
Assert.IsTrue(latchB.State.IsSet, 'LatchB should now be set by LatchA.');
Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by LatchB.');
end;
// --- Category 4: Special Cases ---
procedure TTestMycLatch.TestInitiallySetLatch_NotifiesNewSubscriberImmediately;
var
latch: IMycEvent; // Will be TMycLatch.Null
mockSub: TMockSubscriber;
subscription: TState.TSubscription;
begin
latch := TEvent.CreateLatch(0); // Returns TMycLatch.Null which is set.
mockSub := TMockSubscriber.Create;
subscription := latch.State.Subscribe(mockSub); // TMycLatch.Subscribe notifies immediately if already set.
Assert.IsTrue(latch.State.IsSet, 'Latch IsSet property mismatch (should be true for Null latch).');
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should be notified immediately upon subscribing to an initially set latch.');
end;
procedure TTestMycLatch.TestUnsubscribe_SubscriberNotNotified;
var
latch: IMycEvent;
mockSub: TMockSubscriber;
subscription: TState.TSubscription;
begin
latch := TEvent.CreateLatch(1);
mockSub := TMockSubscriber.Create;
subscription := latch.State.Subscribe(mockSub);
subscription.Unsubscribe; // Explicitly call Unsubscribe on the record
latch.Notify; // Latch becomes set
Assert.AreEqual(0, mockSub.NotifyCount, 'Unsubscribed MockSub should not be notified.');
end;
// --- Category 5: Counter Integrity / State for Later Subscribers ---
procedure TTestMycLatch.TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber;
var
latch: IMycEvent;
mockSub: TMockSubscriber;
subscription: TState.TSubscription;
begin
latch := TEvent.CreateLatch(3);
latch.Notify; // Count -> 2
latch.Notify; // Count -> 1
Assert.IsFalse(latch.State.IsSet, 'Latch should not be set yet after partial notifies.');
mockSub := TMockSubscriber.Create;
subscription := latch.State.Subscribe(mockSub);
Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified on subscribe if latch not yet set.');
latch.Notify; // Count -> 0, Latch becomes set and notifies mockSub
Assert.IsTrue(latch.State.IsSet, 'Latch should now be set.');
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified when latch becomes set.');
end;
// --- Category 6: UnadviseAll Behavior on Latch Set ---
procedure TTestMycLatch.TestLatchSet_ClearsSubscribers_NoFurtherNotification;
var
latch: IMycEvent;
mockSub1, mockSub2: TMockSubscriber;
subscription1, subscription2: TState.TSubscription;
begin
latch := TEvent.CreateLatch(1);
mockSub1 := TMockSubscriber.Create;
mockSub2 := TMockSubscriber.Create;
subscription1 := latch.State.Subscribe(mockSub1);
subscription2 := latch.State.Subscribe(mockSub2);
Assert.AreEqual(0, mockSub1.NotifyCount, 'MockSub1 initial NotifyCount should be 0.');
Assert.AreEqual(0, mockSub2.NotifyCount, 'MockSub2 initial NotifyCount should be 0.');
latch.Notify; // FCount becomes 0. Subscribers notified, then UnadviseAll called.
Assert.IsTrue(latch.State.IsSet, 'Latch should be set.');
Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once when latch became set.');
Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should have been notified once when latch became set.');
latch.Notify; // Call Notify on the latch again (FCount becomes -1)
Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should NOT be notified again after UnadviseAll.');
Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should NOT be notified again after UnadviseAll.');
end;
procedure TTestMycLatch.TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll;
var
latch: IMycEvent;
mockSub: TMockSubscriber;
// 'subscription' will be declared in an inner scope to control its finalization
begin
latch := TEvent.CreateLatch(1);
mockSub := TMockSubscriber.Create;
var noException: Boolean := True;
try
begin // Inner block to control lifetime of 'subscription'
var subscription: TState.TSubscription; // Local to this block
subscription := latch.State.Subscribe(mockSub);
latch.Notify; // Sets the latch, mockSub is notified, UnadviseAll is called internally.
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub was notified when latch set.');
// When 'subscription' goes out of scope at the end of this 'begin..end' block,
// its Finalize operator will be called, which in turn calls subscription.Unsubscribe.
// This happens *after* UnadviseAll was triggered by latch.Notify.
end; // <-- subscription.Finalize (and thus Unsubscribe) is called here.
except
on E: Exception do
begin
noException := False;
// Optional: Log.Error('Exception in TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll: ' + E.Message);
end;
end;
Assert.IsTrue(noException, 'Finalizing subscription after Latch set (and UnadviseAll) should not raise an exception.');
// Further check: calling Notify on latch again should not affect the original mockSub
latch.Notify;
Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should not be notified after its subscription was finalized post UnadviseAll.');
end;
procedure TTestMycLatch.TestLatchSet_NewSubscriberToSetLatch_NotifiedImmediatelyButNotPersistedForLatchNotify;
var
latch: IMycEvent;
mockSubInitial, mockSubNew: TMockSubscriber;
subscriptionInitial, subscriptionNew: TState.TSubscription;
begin
latch := TEvent.CreateLatch(1);
mockSubInitial := TMockSubscriber.Create;
subscriptionInitial := latch.State.Subscribe(mockSubInitial);
// Set the latch; mockSubInitial is notified, UnadviseAll is called.
latch.Notify;
Assert.AreEqual(1, mockSubInitial.NotifyCount, 'Initial subscriber notified.');
// Subscribe a new subscriber to the already set latch
mockSubNew := TMockSubscriber.Create;
subscriptionNew := latch.State.Subscribe(mockSubNew);
// New subscriber should be notified immediately upon subscription to an already set latch
Assert.AreEqual(1, mockSubNew.NotifyCount, 'New subscriber should be notified immediately upon subscription.');
// The direct check of subscriptionNew.FTag was removed as FTag is private.
// The correct behavior (no persistent subscription) is verified by the next step.
// Call Notify on the latch again.
latch.Notify;
// mockSubInitial should not be notified again (was unadvised).
Assert.AreEqual(1, mockSubInitial.NotifyCount, 'Initial subscriber (unadvised) not notified again.');
// mockSubNew should also not be notified again, as it was only notified immediately
// and not persistently added to FSubscribers for subsequent Latch.Notify calls.
Assert
.AreEqual(1, mockSubNew.NotifyCount, 'New subscriber (notified once on subscribe) not notified by subsequent Latch.Notify calls.');
end;
initialization
TDUnitX.RegisterTestFixture(TTestMycLatch);
end.
-5
View File
@@ -257,11 +257,6 @@ begin
end;
end;
function FindNextFile(const Filename: String): String;
begin
end;
class destructor TDataSeries<T>.CreateClass;
begin
FCachedFiles := TDictionary<String, TCachedFile>.Create;
+72
View File
@@ -0,0 +1,72 @@
unit Myc.Trade.Ticker;
interface
uses
Myc.Lazy;
type
TMycLogger = reference to procedure(const Msg: String);
IMycTradeObject = interface
end;
TMycTradeObject = class(TInterfacedObject, IMycTradeObject)
private
FCaption: string;
FLog: TMycLogger;
function GetCaption: string;
public
constructor Create(const ACaption: string; ALog: TMycLogger);
property Caption: string read GetCaption;
property Log: TMycLogger read FLog;
end;
IMycTime = interface
{$REGION 'property access'}
function GetTimeStamp: TDateTime;
{$ENDREGION}
property TimeStamp: TDateTime read GetTimeStamp;
end;
IMycTick = interface
{$REGION 'property access'}
function GetAsk: Double;
function GetBid: Double;
function GetTime: IMycTime;
function GetVolume: Double;
{$ENDREGION}
property Ask: Double read GetAsk;
property Bid: Double read GetBid;
property Time: IMycTime read GetTime;
property Volume: Double read GetVolume;
end;
IMycTicker = interface(IMycTradeObject)
{$REGION 'property access'}
function GetLastTick: TLazy<IMycTick>;
function GetCurrentTime: IMycTime;
{$ENDREGION}
property LastTick: TLazy<IMycTick> read GetLastTick;
property CurrentTime: IMycTime read GetCurrentTime;
end;
IMycHistoryTicker = interface(IMycTicker)
end;
implementation
constructor TMycTradeObject.Create(const ACaption: string; ALog: TMycLogger);
begin
inherited Create;
FCaption := ACaption;
FLog := procedure(const Msg: String) begin ALog('[' + FCaption + '] ' + Msg); end;
end;
function TMycTradeObject.GetCaption: string;
begin
Result := FCaption;
end;
end.