Units renamed & Chart refactored
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<ProjectVersion>20.3</ProjectVersion>
|
<ProjectVersion>20.3</ProjectVersion>
|
||||||
<FrameworkType>FMX</FrameworkType>
|
<FrameworkType>FMX</FrameworkType>
|
||||||
<Base>True</Base>
|
<Base>True</Base>
|
||||||
<Config Condition="'$(Config)'==''">Release</Config>
|
<Config Condition="'$(Config)'==''">Debug</Config>
|
||||||
<Platform Condition="'$(Platform)'==''">Win64</Platform>
|
<Platform Condition="'$(Platform)'==''">Win64</Platform>
|
||||||
<ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName>
|
<ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName>
|
||||||
<TargetedPlatforms>3</TargetedPlatforms>
|
<TargetedPlatforms>3</TargetedPlatforms>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ uses
|
|||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
System.Generics.Collections,
|
System.Generics.Collections,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.TaskManager,
|
Myc.TaskManager,
|
||||||
Myc.Trade.Types,
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ uses
|
|||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
Myc.Trade.DataProvider,
|
Myc.Trade.DataProvider,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.Signals.FMX,
|
Myc.Signals.FMX,
|
||||||
Myc.TaskManager,
|
Myc.TaskManager,
|
||||||
Myc.Aura.Module,
|
Myc.Aura.Module,
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ uses
|
|||||||
System.Generics.Collections,
|
System.Generics.Collections,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Futures,
|
Myc.Futures,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
Myc.Aura.Parameter;
|
Myc.Aura.Parameter;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
unit Myc.Core.Futures;
|
unit Myc.Core.Future;
|
||||||
|
|
||||||
interface
|
interface
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
unit Myc.Core.Lazy;
|
unit Myc.Core.Mutable;
|
||||||
|
|
||||||
interface
|
interface
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ uses
|
|||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
System.SyncObjs,
|
System.SyncObjs,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy;
|
Myc.Mutable;
|
||||||
|
|
||||||
type
|
type
|
||||||
TMycNullMutable<T> = class(TInterfacedObject, TMutable<T>.IMutable)
|
TMycNullMutable<T> = class(TInterfacedObject, TMutable<T>.IMutable)
|
||||||
@@ -47,7 +47,6 @@ type
|
|||||||
protected
|
protected
|
||||||
function GetChanged: TSignal;
|
function GetChanged: TSignal;
|
||||||
function GetValue: T;
|
function GetValue: T;
|
||||||
function Exchange(const Value: T): T;
|
|
||||||
public
|
public
|
||||||
constructor Create(const AValue: T);
|
constructor Create(const AValue: T);
|
||||||
procedure SetValue(const Value: T);
|
procedure SetValue(const Value: T);
|
||||||
@@ -58,7 +57,6 @@ type
|
|||||||
FWriteable: TWriteable<T>.IWriteable;
|
FWriteable: TWriteable<T>.IWriteable;
|
||||||
FLock: TLightweightMREW;
|
FLock: TLightweightMREW;
|
||||||
protected
|
protected
|
||||||
function Exchange(const Value: T): T;
|
|
||||||
function GetChanged: TSignal;
|
function GetChanged: TSignal;
|
||||||
function GetValue: T;
|
function GetValue: T;
|
||||||
public
|
public
|
||||||
@@ -118,12 +116,6 @@ begin
|
|||||||
FChanged := TEvent.CreateEvent;
|
FChanged := TEvent.CreateEvent;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TMycWriteableMutable<T>.Exchange(const Value: T): T;
|
|
||||||
begin
|
|
||||||
Result := FValue;
|
|
||||||
FValue := Value;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TMycWriteableMutable<T>.GetChanged: TSignal;
|
function TMycWriteableMutable<T>.GetChanged: TSignal;
|
||||||
begin
|
begin
|
||||||
Result := FChanged.Signal;
|
Result := FChanged.Signal;
|
||||||
@@ -162,16 +154,6 @@ begin
|
|||||||
FWriteable := AWriteable;
|
FWriteable := AWriteable;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TMycProtectedMutable<T>.Exchange(const Value: T): T;
|
|
||||||
begin
|
|
||||||
FLock.BeginWrite;
|
|
||||||
try
|
|
||||||
Result := FWriteable.Exchange(Value);
|
|
||||||
finally
|
|
||||||
FLock.EndWrite;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TMycProtectedMutable<T>.GetChanged: TSignal;
|
function TMycProtectedMutable<T>.GetChanged: TSignal;
|
||||||
begin
|
begin
|
||||||
Result := FWriteable.Changed;
|
Result := FWriteable.Changed;
|
||||||
@@ -8,7 +8,7 @@ uses
|
|||||||
System.UITypes,
|
System.UITypes,
|
||||||
FMX.Graphics,
|
FMX.Graphics,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.Trade.Types,
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataArray,
|
Myc.Trade.DataArray,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
|
|||||||
+52
-81
@@ -18,7 +18,7 @@ uses
|
|||||||
Myc.Trade.Types,
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy;
|
Myc.Mutable;
|
||||||
|
|
||||||
type
|
type
|
||||||
TMycChart = class(TStyledControl)
|
TMycChart = class(TStyledControl)
|
||||||
@@ -59,18 +59,14 @@ type
|
|||||||
constructor Create(AParent: TPanel);
|
constructor Create(AParent: TPanel);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TAxisLayer = class(TLayer)
|
TAxisLayer = class abstract(TLayer)
|
||||||
private
|
private
|
||||||
FOwner: TMycChart;
|
FOwner: TMycChart;
|
||||||
protected
|
protected
|
||||||
function GetOwner: TMycChart; override; final;
|
function GetOwner: TMycChart; override; final;
|
||||||
// Paint crosshair and other axis related overlays
|
// Paint crosshair and other axis related overlays
|
||||||
procedure Paint(
|
procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; ViewStartIndex, ViewCount: Int64; IdxAtMousePos: Integer); virtual;
|
||||||
const Canvas: TCanvas;
|
abstract;
|
||||||
const Viewport: TRectF;
|
|
||||||
ViewStartIndex, ViewCount: Int64;
|
|
||||||
const MousePos: TPointF
|
|
||||||
); virtual;
|
|
||||||
public
|
public
|
||||||
constructor Create(AOwner: TMycChart);
|
constructor Create(AOwner: TMycChart);
|
||||||
end;
|
end;
|
||||||
@@ -78,12 +74,7 @@ type
|
|||||||
TXAxisLayer = class(TAxisLayer)
|
TXAxisLayer = class(TAxisLayer)
|
||||||
protected
|
protected
|
||||||
// Paint crosshair and time caption
|
// Paint crosshair and time caption
|
||||||
procedure Paint(
|
procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; ViewStartIndex, ViewCount: Int64; IdxAtMousePos: Integer); override;
|
||||||
const Canvas: TCanvas;
|
|
||||||
const Viewport: TRectF;
|
|
||||||
ViewStartIndex, ViewCount: Int64;
|
|
||||||
const MousePos: TPointF
|
|
||||||
); override;
|
|
||||||
// This function delivers the text for the caption.
|
// This function delivers the text for the caption.
|
||||||
function GetCaption(Idx: Int64): String; virtual; abstract;
|
function GetCaption(Idx: Int64): String; virtual; abstract;
|
||||||
end;
|
end;
|
||||||
@@ -119,6 +110,11 @@ type
|
|||||||
property Weight: Single read FWeight write SetWeight;
|
property Weight: Single read FWeight write SetWeight;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
TDragPoint = record
|
||||||
|
Point: TPointF;
|
||||||
|
Idx: Int64;
|
||||||
|
end;
|
||||||
|
|
||||||
private
|
private
|
||||||
FPanelList: TObjectList<TPanel>;
|
FPanelList: TObjectList<TPanel>;
|
||||||
FXAxisSeries: TMycChart.TXAxisLayer;
|
FXAxisSeries: TMycChart.TXAxisLayer;
|
||||||
@@ -127,8 +123,8 @@ type
|
|||||||
FViewStartIndex: Int64;
|
FViewStartIndex: Int64;
|
||||||
FViewCount: Int64;
|
FViewCount: Int64;
|
||||||
FIsDragging: Boolean;
|
FIsDragging: Boolean;
|
||||||
FDragStartPoint: TPointF;
|
FMousePos: TDragPoint;
|
||||||
FMousePos: TPointF;
|
FDragStartPoint: TDragPoint;
|
||||||
FIsMouseInControl: Boolean;
|
FIsMouseInControl: Boolean;
|
||||||
FJumpButtonRect: TRectF;
|
FJumpButtonRect: TRectF;
|
||||||
FJumpButtonHot: Boolean;
|
FJumpButtonHot: Boolean;
|
||||||
@@ -139,6 +135,7 @@ type
|
|||||||
FHotResizePanelIndex: Integer;
|
FHotResizePanelIndex: Integer;
|
||||||
function GetPanel(Index: Integer): TPanel;
|
function GetPanel(Index: Integer): TPanel;
|
||||||
function GetPanelCount: Integer;
|
function GetPanelCount: Integer;
|
||||||
|
function CreateDragPoint(X, Y: Single): TDragPoint;
|
||||||
protected
|
protected
|
||||||
procedure Paint; override;
|
procedure Paint; override;
|
||||||
procedure DoIdle;
|
procedure DoIdle;
|
||||||
@@ -221,6 +218,15 @@ begin
|
|||||||
Repaint;
|
Repaint;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TMycChart.CreateDragPoint(X, Y: Single): TDragPoint;
|
||||||
|
begin
|
||||||
|
Result.Point.X := X;
|
||||||
|
Result.Point.Y := Y;
|
||||||
|
Result.Idx := Round((LocalRect.Right - X) * (FViewCount - 1) / LocalRect.Width);
|
||||||
|
if (Result.Idx < 0) or (Result.Idx >= FXAxisSeries.Series.Count) then
|
||||||
|
Result.Idx := -1;
|
||||||
|
end;
|
||||||
|
|
||||||
function TMycChart.GetPanel(Index: Integer): TPanel;
|
function TMycChart.GetPanel(Index: Integer): TPanel;
|
||||||
begin
|
begin
|
||||||
Result := FPanelList[Index];
|
Result := FPanelList[Index];
|
||||||
@@ -333,8 +339,12 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// Draw crosshair if mouse is in control
|
// Draw crosshair if mouse is in control
|
||||||
if FIsMouseInControl and Assigned(FXAxisSeries) and not FIsDragging and not FIsResizing then
|
if FIsMouseInControl and Assigned(FXAxisSeries) and (FMousePos.Idx >= 0 ) then
|
||||||
FXAxisSeries.Paint(Self.Canvas, rect, FViewStartIndex, FViewCount, FMousePos);
|
begin
|
||||||
|
// Do not draw crosshair if mouse is over the jump button
|
||||||
|
if not FJumpButtonRect.Contains(FMousePos.Point) then
|
||||||
|
FXAxisSeries.Paint(Self.Canvas, rect, FViewStartIndex, FViewCount, FViewStartIndex + FMousePos.Idx );
|
||||||
|
end;
|
||||||
|
|
||||||
// --- Draw the "jump to latest" button (code unchanged) ---
|
// --- Draw the "jump to latest" button (code unchanged) ---
|
||||||
isButtonVisible := (FViewStartIndex > 0);
|
isButtonVisible := (FViewStartIndex > 0);
|
||||||
@@ -380,6 +390,8 @@ end;
|
|||||||
|
|
||||||
procedure TMycChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
|
procedure TMycChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
|
||||||
begin
|
begin
|
||||||
|
inherited MouseDown(Button, Shift, X, Y);
|
||||||
|
|
||||||
// Check for button press first
|
// Check for button press first
|
||||||
if (Button = TMouseButton.mbLeft) and (FViewStartIndex > 0) and FJumpButtonRect.Contains(PointF(X, Y)) then
|
if (Button = TMouseButton.mbLeft) and (FViewStartIndex > 0) and FJumpButtonRect.Contains(PointF(X, Y)) then
|
||||||
begin
|
begin
|
||||||
@@ -393,16 +405,15 @@ begin
|
|||||||
begin
|
begin
|
||||||
FIsResizing := true;
|
FIsResizing := true;
|
||||||
FResizePanelIndex := FHotResizePanelIndex;
|
FResizePanelIndex := FHotResizePanelIndex;
|
||||||
FDragStartPoint := TPointF.Create(X, Y);
|
FDragStartPoint := CreateDragPoint(X, Y);
|
||||||
Capture;
|
Capture;
|
||||||
exit;
|
exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
inherited MouseDown(Button, Shift, X, Y);
|
|
||||||
if (Button = TMouseButton.mbLeft) then
|
if (Button = TMouseButton.mbLeft) then
|
||||||
begin
|
begin
|
||||||
FIsDragging := true;
|
FIsDragging := true;
|
||||||
FDragStartPoint := TPointF.Create(X, Y);
|
FDragStartPoint := CreateDragPoint(X, Y);
|
||||||
Capture;
|
Capture;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -427,10 +438,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// Stop panning
|
// Stop panning
|
||||||
if FIsDragging then
|
|
||||||
begin
|
|
||||||
FIsDragging := false;
|
FIsDragging := false;
|
||||||
end;
|
|
||||||
|
|
||||||
// Stop button press
|
// Stop button press
|
||||||
if FJumpButtonPressed then
|
if FJumpButtonPressed then
|
||||||
@@ -459,12 +467,14 @@ begin
|
|||||||
|
|
||||||
if not FIsMouseInControl then
|
if not FIsMouseInControl then
|
||||||
FIsMouseInControl := true;
|
FIsMouseInControl := true;
|
||||||
FMousePos := TPointF.Create(X, Y);
|
FMousePos := CreateDragPoint(X, Y);
|
||||||
|
|
||||||
// --- Panel Resizing Logic ---
|
// --- Panel Resizing Logic ---
|
||||||
if FIsResizing then
|
if FIsResizing then
|
||||||
begin
|
begin
|
||||||
dy := Y - FDragStartPoint.Y;
|
dy := Y - FDragStartPoint.Point.Y;
|
||||||
|
if Abs(dx) < 2 then // Threshold to avoid jitter
|
||||||
|
exit;
|
||||||
|
|
||||||
var panel1 := Panels[FResizePanelIndex];
|
var panel1 := Panels[FResizePanelIndex];
|
||||||
var panel2 := Panels[FResizePanelIndex + 1];
|
var panel2 := Panels[FResizePanelIndex + 1];
|
||||||
@@ -502,7 +512,7 @@ begin
|
|||||||
panel2.Weight := (newH2 / (newH1 + newH2)) * twoPanelWeight;
|
panel2.Weight := (newH2 / (newH1 + newH2)) * twoPanelWeight;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
FDragStartPoint.Y := Y; // Update start point for next move
|
FDragStartPoint := CreateDragPoint( FDragStartPoint.Point.X, Y ); // Update start point for next move
|
||||||
Repaint;
|
Repaint;
|
||||||
exit; // Do not continue with other mouse move logic
|
exit; // Do not continue with other mouse move logic
|
||||||
end;
|
end;
|
||||||
@@ -510,17 +520,17 @@ begin
|
|||||||
// --- Chart Panning Logic ---
|
// --- Chart Panning Logic ---
|
||||||
if FIsDragging then
|
if FIsDragging then
|
||||||
begin
|
begin
|
||||||
dx := X - FDragStartPoint.X;
|
dx := X - FDragStartPoint.Point.X;
|
||||||
if Abs(dx) < 2 then // Threshold to avoid jitter
|
if Abs(dx) < 2 then // Threshold to avoid jitter
|
||||||
begin
|
|
||||||
exit;
|
exit;
|
||||||
end;
|
|
||||||
|
|
||||||
if not Assigned(FXAxisSeries) or (FViewCount <= 0) then
|
if not Assigned(FXAxisSeries) or (FViewCount <= 0) then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
indexDelta := Round(dx / (Self.Width / FViewCount));
|
indexDelta := FMousePos.Idx - FDragStartPoint.Idx;
|
||||||
FViewStartIndex := FViewStartIndex + indexDelta;
|
FDragStartPoint.Idx := FDragStartPoint.Idx + indexDelta;
|
||||||
|
|
||||||
|
FViewStartIndex := FViewStartIndex - indexDelta;
|
||||||
|
|
||||||
// Clamp values
|
// Clamp values
|
||||||
maxIndex := FXAxisSeries.Series.Count - FViewCount;
|
maxIndex := FXAxisSeries.Series.Count - FViewCount;
|
||||||
@@ -531,8 +541,6 @@ begin
|
|||||||
if (FViewStartIndex > maxIndex) then
|
if (FViewStartIndex > maxIndex) then
|
||||||
FViewStartIndex := maxIndex;
|
FViewStartIndex := maxIndex;
|
||||||
|
|
||||||
FDragStartPoint.X := X;
|
|
||||||
FDragStartPoint.Y := Y;
|
|
||||||
Repaint;
|
Repaint;
|
||||||
exit;
|
exit;
|
||||||
end;
|
end;
|
||||||
@@ -574,12 +582,11 @@ end;
|
|||||||
|
|
||||||
procedure TMycChart.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
|
procedure TMycChart.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
|
||||||
var
|
var
|
||||||
mousePos: TPointF;
|
|
||||||
anchorIndex: Double;
|
|
||||||
zoomFactor: Double;
|
zoomFactor: Double;
|
||||||
newViewCount: Int64;
|
newViewCount: Int64;
|
||||||
ratio: Double;
|
|
||||||
begin
|
begin
|
||||||
|
inherited MouseWheel(Shift, WheelDelta, Handled);
|
||||||
|
|
||||||
Handled := true;
|
Handled := true;
|
||||||
|
|
||||||
// An X-axis series is required for zooming
|
// An X-axis series is required for zooming
|
||||||
@@ -590,11 +597,6 @@ begin
|
|||||||
if (xAxisCount = 0) or (Width <= 0) then
|
if (xAxisCount = 0) or (Width <= 0) then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
mousePos := ScreenToLocal(Screen.MousePos);
|
|
||||||
|
|
||||||
// Calculate the anchor index for a right-to-left axis
|
|
||||||
anchorIndex := FViewStartIndex + ((Self.Width - mousePos.X) / Self.Width) * FViewCount;
|
|
||||||
|
|
||||||
if (WheelDelta > 0) then
|
if (WheelDelta > 0) then
|
||||||
zoomFactor := 0.8 // Zoom In
|
zoomFactor := 0.8 // Zoom In
|
||||||
else
|
else
|
||||||
@@ -610,15 +612,12 @@ begin
|
|||||||
if (newViewCount = FViewCount) then
|
if (newViewCount = FViewCount) then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
// Adjust start index to keep anchor point stable
|
|
||||||
if (FViewCount > 0) then
|
|
||||||
ratio := (anchorIndex - FViewStartIndex) / FViewCount
|
|
||||||
else
|
|
||||||
ratio := 0;
|
|
||||||
|
|
||||||
FViewStartIndex := Round(anchorIndex - (ratio * newViewCount));
|
|
||||||
FViewCount := newViewCount;
|
FViewCount := newViewCount;
|
||||||
|
|
||||||
|
var oldIdx := FViewStartIndex + FMousePos.Idx;
|
||||||
|
FMousePos := CreateDragPoint( FMousePos.Point.X, FMousePos.Point.Y );
|
||||||
|
FViewStartIndex := oldIdx - FMousePos.Idx;
|
||||||
|
|
||||||
// Clamp start index
|
// Clamp start index
|
||||||
if (FViewStartIndex < 0) then
|
if (FViewStartIndex < 0) then
|
||||||
FViewStartIndex := 0;
|
FViewStartIndex := 0;
|
||||||
@@ -818,51 +817,23 @@ begin
|
|||||||
Result := FOwner;
|
Result := FOwner;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMycChart.TAxisLayer.Paint(
|
|
||||||
const Canvas: TCanvas;
|
|
||||||
const Viewport: TRectF;
|
|
||||||
ViewStartIndex, ViewCount: Int64;
|
|
||||||
const MousePos: TPointF
|
|
||||||
);
|
|
||||||
begin
|
|
||||||
// Do nothing in base class
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TMycChart.TXAxisLayer }
|
{ TMycChart.TXAxisLayer }
|
||||||
|
|
||||||
procedure TMycChart.TXAxisLayer.Paint(
|
procedure TMycChart.TXAxisLayer.Paint(const Canvas: TCanvas; const Viewport: TRectF; ViewStartIndex, ViewCount: Int64; IdxAtMousePos:
|
||||||
const Canvas: TCanvas;
|
Integer);
|
||||||
const Viewport: TRectF;
|
|
||||||
ViewStartIndex, ViewCount: Int64;
|
|
||||||
const MousePos: TPointF
|
|
||||||
);
|
|
||||||
var
|
var
|
||||||
idx: Int64;
|
|
||||||
caption: string;
|
caption: string;
|
||||||
captionRect: TRectF;
|
captionRect: TRectF;
|
||||||
padding: Single;
|
padding: Single;
|
||||||
indexFromMouse: Double;
|
|
||||||
candleX: Single;
|
candleX: Single;
|
||||||
textSize: TRectF;
|
textSize: TRectF;
|
||||||
begin
|
begin
|
||||||
// Do not draw crosshair if mouse is over the jump button
|
|
||||||
if Owner.JumpButtonRect.Contains(MousePos) then
|
|
||||||
exit;
|
|
||||||
|
|
||||||
// Do not draw if no data or view is invalid
|
// Do not draw if no data or view is invalid
|
||||||
if (not Assigned(Series)) or (Series.Count = 0) or (ViewCount <= 1) then
|
if (not Assigned(Series)) or (Series.Count = 0) or (ViewCount <= 1) then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
// 1. Calculate the data index from the mouse X position
|
|
||||||
indexFromMouse := ViewStartIndex + (Viewport.Right - MousePos.X) * (ViewCount - 1) / Viewport.Width;
|
|
||||||
idx := Round(indexFromMouse);
|
|
||||||
|
|
||||||
// Check if the calculated index is valid for the series
|
|
||||||
if (idx < 0) or (idx >= Series.Count) then
|
|
||||||
exit;
|
|
||||||
|
|
||||||
// Snap X to the candle's center
|
// Snap X to the candle's center
|
||||||
candleX := Viewport.Right - ((idx - ViewStartIndex) * (ViewCount - 1)) / (ViewCount - 1) * (Viewport.Width / (ViewCount - 1));
|
candleX := Viewport.Right - ((IdxAtMousePos - ViewStartIndex) * (ViewCount - 1)) / (ViewCount - 1) * (Viewport.Width / (ViewCount - 1));
|
||||||
|
|
||||||
// 2. Draw the vertical line at the snapped position
|
// 2. Draw the vertical line at the snapped position
|
||||||
Canvas.Stroke.Kind := TBrushKind.Solid;
|
Canvas.Stroke.Kind := TBrushKind.Solid;
|
||||||
@@ -871,7 +842,7 @@ begin
|
|||||||
Canvas.DrawLine(PointF(candleX, Viewport.Top), PointF(candleX, Viewport.Bottom), 1.0);
|
Canvas.DrawLine(PointF(candleX, Viewport.Top), PointF(candleX, Viewport.Bottom), 1.0);
|
||||||
|
|
||||||
// 3. Get the caption for the index
|
// 3. Get the caption for the index
|
||||||
caption := GetCaption(idx);
|
caption := GetCaption(IdxAtMousePos);
|
||||||
if caption.IsEmpty then
|
if caption.IsEmpty then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Myc.Core.Futures;
|
Myc.Core.Future;
|
||||||
|
|
||||||
constructor TFuture<T>.Create(const AFuture: IFuture);
|
constructor TFuture<T>.Create(const AFuture: IFuture);
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
unit Myc.Lazy;
|
unit Myc.Mutable;
|
||||||
|
|
||||||
interface
|
interface
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ type
|
|||||||
type
|
type
|
||||||
IWriteable = interface(TMutable<T>.IMutable)
|
IWriteable = interface(TMutable<T>.IMutable)
|
||||||
procedure SetValue(const Value: T);
|
procedure SetValue(const Value: T);
|
||||||
function Exchange(const Value: T): T;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{$REGION 'private'}
|
{$REGION 'private'}
|
||||||
@@ -67,7 +66,6 @@ type
|
|||||||
class function CreateWriteable(const Init: T): TWriteable<T>; overload; static;
|
class function CreateWriteable(const Init: T): TWriteable<T>; overload; static;
|
||||||
|
|
||||||
function Protect: TWriteable<T>;
|
function Protect: TWriteable<T>;
|
||||||
|
|
||||||
function AsMutable: TMutable<T>;
|
function AsMutable: TMutable<T>;
|
||||||
|
|
||||||
property Value: T read GetValue write SetValue;
|
property Value: T read GetValue write SetValue;
|
||||||
@@ -89,7 +87,7 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Myc.Core.Lazy;
|
Myc.Core.Mutable;
|
||||||
|
|
||||||
{ TMutable<T> }
|
{ TMutable<T> }
|
||||||
|
|
||||||
@@ -201,7 +199,6 @@ constructor TLazy<T>.Create(const AMutable: TMutable<T>);
|
|||||||
begin
|
begin
|
||||||
FMutable := AMutable;
|
FMutable := AMutable;
|
||||||
FChanged := TFlag.CreateFlag;
|
FChanged := TFlag.CreateFlag;
|
||||||
FChanged.Notify;
|
|
||||||
FChangeState := AMutable.Changed.Subscribe(FChanged);
|
FChangeState := AMutable.Changed.Subscribe(FChanged);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
unit Myc.Test.Core.Lazy;
|
|
||||||
|
|
||||||
interface
|
|
||||||
|
|
||||||
uses
|
|
||||||
System.SysUtils,
|
|
||||||
DUnitX.TestFramework,
|
|
||||||
Myc.Signals, // For IMycState, TState, IMycDirty
|
|
||||||
Myc.Lazy, // For IMycLazy<T>
|
|
||||||
Myc.Core.Lazy; // Unit to be tested
|
|
||||||
|
|
||||||
type
|
|
||||||
[TestFixture]
|
|
||||||
TTestMycCoreLazy = class(TObject)
|
|
||||||
private
|
|
||||||
procedure Helper_ConsumeInitialPop(const ALazy: TLazy<Integer>.ILazy; InitialExpectedValue: Integer); overload;
|
|
||||||
public
|
|
||||||
[Setup]
|
|
||||||
procedure Setup;
|
|
||||||
[TearDown]
|
|
||||||
procedure TearDown;
|
|
||||||
|
|
||||||
// Tests for TMycNullLazy<T>
|
|
||||||
[Test]
|
|
||||||
procedure TestNullLazy_GetChanged_IsAlwaysNullState;
|
|
||||||
[Test]
|
|
||||||
procedure TestNullLazy_Pop_ReturnsDefaultIntegerAndTrue;
|
|
||||||
[Test]
|
|
||||||
procedure TestNullLazy_Pop_ReturnsDefaultStringAndTrue;
|
|
||||||
[Test]
|
|
||||||
procedure TestNullLazy_Pop_ReturnsDefaultInterfaceAndTrue;
|
|
||||||
|
|
||||||
// Tests for TMycFuncLazy<T> reflecting "IsSet is true by design after creation"
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_GetChanged_IsAlwaysTrueAfterCreation;
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_FirstPop_AlwaysEvaluatesAndResetsChanged;
|
|
||||||
// Tests for behavior after the initial "changed" state is consumed
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_AfterFirstPop_IfNoSourceChange_GetChangedIsFalse;
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_AfterFirstPop_IfNoSourceChange_PopReturnsFalse_ValueUndefined;
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_AfterSourceChange_GetChanged_IsSet;
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_AfterSourceChange_Pop_ReturnsTrueAndProcResult_ResetsChanged;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_Pop_Twice_SourceUnchanged_SecondPopReturnsFalse_ValueUndefined;
|
|
||||||
// This test is now significantly changed to reflect TMycDirty's notification behavior
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_SourceInteraction_NotifyOnSetSourceDoesNotRetriggerLazy;
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_Destroy_UnsubscribesFromSource;
|
|
||||||
[Test]
|
|
||||||
procedure TestFuncLazy_SourceIsInitiallySet_PopBehavesCorrectly;
|
|
||||||
end;
|
|
||||||
|
|
||||||
implementation
|
|
||||||
|
|
||||||
uses
|
|
||||||
System.Rtti,
|
|
||||||
Myc.Core.Signals;
|
|
||||||
|
|
||||||
{ TTestMycCoreLazy Helper Methods }
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.Helper_ConsumeInitialPop(const ALazy: TLazy<Integer>.ILazy; InitialExpectedValue: Integer);
|
|
||||||
var
|
|
||||||
tempValue: Integer;
|
|
||||||
popResult: Boolean;
|
|
||||||
begin
|
|
||||||
Assert.IsTrue(ALazy.GetChanged.IsSet, 'Changed.IsSet should be true before consuming initial pop');
|
|
||||||
popResult := ALazy.Pop(tempValue);
|
|
||||||
Assert.IsTrue(popResult, 'Consuming initial Pop should return true');
|
|
||||||
Assert.AreEqual(InitialExpectedValue, tempValue, 'Value from initial Pop is unexpected');
|
|
||||||
Assert.IsFalse(ALazy.GetChanged.IsSet, 'Changed.IsSet should be false after consuming initial pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TTestMycCoreLazy Test Methods }
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.Setup;
|
|
||||||
begin
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TearDown;
|
|
||||||
begin
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TMycNullLazy<T> ==
|
|
||||||
procedure TTestMycCoreLazy.TestNullLazy_GetChanged_IsAlwaysNullState;
|
|
||||||
var
|
|
||||||
nullLazy: TLazy<Integer>.ILazy;
|
|
||||||
changedState: TState.IState;
|
|
||||||
begin
|
|
||||||
nullLazy := TMycNullLazy<Integer>.Create as TLazy<Integer>.ILazy;
|
|
||||||
Assert.IsNotNull(nullLazy, 'TMycNullLazy<Integer> instance should not be nil');
|
|
||||||
changedState := nullLazy.GetChanged;
|
|
||||||
Assert.AreSame(TState.Null, changedState, 'TMycNullLazy.GetChanged should return TState.Null');
|
|
||||||
Assert.IsTrue(changedState.IsSet, 'TState.Null should always be set');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestNullLazy_Pop_ReturnsDefaultIntegerAndTrue;
|
|
||||||
var
|
|
||||||
nullLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
result: Boolean;
|
|
||||||
begin
|
|
||||||
nullLazy := TMycNullLazy<Integer>.Create as TLazy<Integer>.ILazy;
|
|
||||||
Assert.IsNotNull(nullLazy, 'TMycNullLazy<Integer> instance should not be nil');
|
|
||||||
value := 123;
|
|
||||||
result := nullLazy.Pop(value);
|
|
||||||
Assert.IsTrue(result, 'TMycNullLazy.Pop should return true');
|
|
||||||
Assert.AreEqual(Default(Integer), value, 'Value should be Default(Integer) after Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestNullLazy_Pop_ReturnsDefaultStringAndTrue;
|
|
||||||
var
|
|
||||||
nullLazy: TLazy<String>.ILazy;
|
|
||||||
value: string;
|
|
||||||
result: Boolean;
|
|
||||||
begin
|
|
||||||
nullLazy := TMycNullLazy<string>.Create as TLazy<String>.ILazy;
|
|
||||||
Assert.IsNotNull(nullLazy, 'TMycNullLazy<string> instance should not be nil');
|
|
||||||
value := 'test';
|
|
||||||
result := nullLazy.Pop(value);
|
|
||||||
Assert.IsTrue(result, 'TMycNullLazy.Pop should return true');
|
|
||||||
Assert.AreEqual(Default(string), value, 'Value should be Default(string) after Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestNullLazy_Pop_ReturnsDefaultInterfaceAndTrue;
|
|
||||||
var
|
|
||||||
nullLazy: TLazy<TState.IState>.ILazy;
|
|
||||||
value: TState.IState;
|
|
||||||
result: Boolean;
|
|
||||||
begin
|
|
||||||
nullLazy := TMycNullLazy<TState.IState>.Create as TLazy<TState.IState>.ILazy;
|
|
||||||
Assert.IsNotNull(nullLazy, 'TMycNullLazy<TState.IState> instance should not be nil');
|
|
||||||
value := TState.Null;
|
|
||||||
result := nullLazy.Pop(value);
|
|
||||||
Assert.IsTrue(result, 'TMycNullLazy.Pop should return true');
|
|
||||||
Assert.IsNull(value, 'Value should be nil for an interface type after Pop from NullLazy');
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TMycFuncLazy<T> - Initial State by Design ==
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_GetChanged_IsAlwaysTrueAfterCreation;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
changedState: TState.IState;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State.Signal, function: Integer begin Result := 10; end);
|
|
||||||
Assert.IsNotNull(funcLazy, 'TMycFuncLazy<Integer> instance should not be nil');
|
|
||||||
changedState := funcLazy.GetChanged;
|
|
||||||
Assert.IsNotNull(changedState, 'funcLazy.GetChanged should return a valid TState.IState');
|
|
||||||
Assert.IsTrue(changedState.IsSet, 'By design, funcLazy.GetChanged.IsSet should be true immediately after creation');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_FirstPop_AlwaysEvaluatesAndResetsChanged;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
result: Boolean;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
procExecuted: Boolean;
|
|
||||||
expectedValue: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
procExecuted := False;
|
|
||||||
expectedValue := 50;
|
|
||||||
funcLazy :=
|
|
||||||
TMycFuncLazy<Integer>.Create(
|
|
||||||
sourceDirty.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
procExecuted := True;
|
|
||||||
Result := expectedValue;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
Assert.IsNotNull(funcLazy, 'TMycFuncLazy<Integer> instance should not be nil');
|
|
||||||
Assert.IsTrue(funcLazy.GetChanged.IsSet, 'Changed.IsSet should be true before the first Pop by design');
|
|
||||||
value := 0;
|
|
||||||
result := funcLazy.Pop(value);
|
|
||||||
Assert.IsTrue(result, 'The first Pop should return true by design, indicating evaluation');
|
|
||||||
Assert.IsTrue(procExecuted, 'FProc should have been executed on the first Pop');
|
|
||||||
Assert.AreEqual(expectedValue, value, 'Value should be the result from FProc after the first Pop');
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed.IsSet should be false after the first Pop, as Pop resets it');
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TMycFuncLazy<T> - Behavior After Initial Pop ==
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_AfterFirstPop_IfNoSourceChange_GetChangedIsFalse;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
initialProcValue: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
initialProcValue := 33;
|
|
||||||
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State.Signal, function: Integer begin Result := initialProcValue; end);
|
|
||||||
Helper_ConsumeInitialPop(funcLazy, initialProcValue);
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'After initial Pop and no source change, GetChanged.IsSet should be false');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_AfterFirstPop_IfNoSourceChange_PopReturnsFalse_ValueUndefined;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
result: Boolean;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
initialProcValue: Integer;
|
|
||||||
procExecutedCount: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
initialProcValue := 44;
|
|
||||||
procExecutedCount := 0;
|
|
||||||
funcLazy :=
|
|
||||||
TMycFuncLazy<Integer>.Create(
|
|
||||||
sourceDirty.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procExecutedCount);
|
|
||||||
Result := initialProcValue;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
Helper_ConsumeInitialPop(funcLazy, initialProcValue);
|
|
||||||
Assert.AreEqual(1, procExecutedCount, 'Proc should have executed once for initial pop');
|
|
||||||
result := funcLazy.Pop(value);
|
|
||||||
Assert.IsFalse(result, 'Second Pop (after initial, no source change) should return false');
|
|
||||||
Assert.AreEqual(1, procExecutedCount, 'Proc should not have executed again');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_AfterSourceChange_GetChanged_IsSet;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
changedState: TState.IState;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
initialProcValue: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
initialProcValue := 20;
|
|
||||||
funcLazy := TMycFuncLazy<Integer>.Create(sourceDirty.State.Signal, function: Integer begin Result := initialProcValue; end);
|
|
||||||
Helper_ConsumeInitialPop(funcLazy, initialProcValue);
|
|
||||||
sourceDirty.Notify;
|
|
||||||
changedState := funcLazy.GetChanged;
|
|
||||||
Assert.IsTrue(changedState.IsSet, 'After source change (post-initial pop), funcLazy.GetChanged.IsSet should be true');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_AfterSourceChange_Pop_ReturnsTrueAndProcResult_ResetsChanged;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
result: Boolean;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
procCallCount: Integer;
|
|
||||||
currentExpectedValue: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
procCallCount := 0;
|
|
||||||
funcLazy :=
|
|
||||||
TMycFuncLazy<Integer>.Create(
|
|
||||||
sourceDirty.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procCallCount);
|
|
||||||
if procCallCount = 1 then
|
|
||||||
Result := 30
|
|
||||||
else
|
|
||||||
Result := 300 + procCallCount;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
currentExpectedValue := 30;
|
|
||||||
Helper_ConsumeInitialPop(funcLazy, currentExpectedValue);
|
|
||||||
Assert.AreEqual(1, procCallCount, 'Proc executed for initial Pop');
|
|
||||||
sourceDirty.Notify;
|
|
||||||
Assert.IsTrue(funcLazy.GetChanged.IsSet, 'Changed state should be true after source notification');
|
|
||||||
value := 0;
|
|
||||||
result := funcLazy.Pop(value);
|
|
||||||
currentExpectedValue := 300 + 2;
|
|
||||||
Assert.IsTrue(result, 'Pop should return true after source changed');
|
|
||||||
Assert.AreEqual(2, procCallCount, 'Proc should have been executed again');
|
|
||||||
Assert.AreEqual(currentExpectedValue, value, 'Value should be the new result of FProc');
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed state should be false after Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_Pop_Twice_SourceUnchanged_SecondPopReturnsFalse_ValueUndefined;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
resultPop1, resultPop2: Boolean;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
procCallCount: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
procCallCount := 0;
|
|
||||||
funcLazy :=
|
|
||||||
TMycFuncLazy<Integer>.Create(
|
|
||||||
sourceDirty.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procCallCount);
|
|
||||||
Result := 40;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
resultPop1 := funcLazy.Pop(value);
|
|
||||||
Assert.IsTrue(resultPop1, 'First Pop should return true by design');
|
|
||||||
Assert.AreEqual(40, value, 'Value from first Pop');
|
|
||||||
Assert.AreEqual(1, procCallCount, 'Proc called for first Pop');
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed state should be false after first Pop');
|
|
||||||
resultPop2 := funcLazy.Pop(value);
|
|
||||||
Assert.IsFalse(resultPop2, 'Second Pop should return false as state was reset and not changed again by source');
|
|
||||||
Assert.AreEqual(1, procCallCount, 'Proc should not be called for second Pop if no source change');
|
|
||||||
end;
|
|
||||||
|
|
||||||
// TestFuncLazy_SourceChanges_Pop_SourceChangesAgain_PopAgain has been RENAMED and RESTRUCTURED
|
|
||||||
// to TestFuncLazy_SourceInteraction_NotifyOnSetSourceDoesNotRetriggerLazy
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_SourceInteraction_NotifyOnSetSourceDoesNotRetriggerLazy;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
result: Boolean;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
procCallCount: Integer;
|
|
||||||
initialValue, firstSourceChangeValue: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset; // sourceDirty.IsSet is FALSE
|
|
||||||
procCallCount := 0;
|
|
||||||
initialValue := 51;
|
|
||||||
firstSourceChangeValue := 52;
|
|
||||||
|
|
||||||
funcLazy :=
|
|
||||||
TMycFuncLazy<Integer>.Create(
|
|
||||||
sourceDirty.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procCallCount);
|
|
||||||
if procCallCount = 1 then
|
|
||||||
Result := initialValue // For initial pop
|
|
||||||
else if procCallCount = 2 then
|
|
||||||
Result := firstSourceChangeValue // For pop after first effective source change
|
|
||||||
else
|
|
||||||
Result := 999; // Should not be reached in this specific test logic
|
|
||||||
end
|
|
||||||
);
|
|
||||||
|
|
||||||
// 1. Initial Pop (consumes "by design" changed state)
|
|
||||||
Assert.IsTrue(funcLazy.GetChanged.IsSet, 'Changed state should be true before first Pop (by design)');
|
|
||||||
result := funcLazy.Pop(value); // procCallCount becomes 1
|
|
||||||
Assert.IsTrue(result, 'First Pop should return true');
|
|
||||||
Assert.AreEqual(initialValue, value, 'Value from first Pop');
|
|
||||||
Assert.AreEqual(1, procCallCount, 'Proc called for initial Pop');
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed state should be false after first Pop');
|
|
||||||
|
|
||||||
// 2. First effective source change (sourceDirty: false -> true)
|
|
||||||
sourceDirty.Notify; // sourceDirty.IsSet becomes TRUE, notifies funcLazy.FChanged, funcLazy.GetChanged.IsSet becomes TRUE
|
|
||||||
Assert.IsTrue(sourceDirty.State.IsSet, 'sourceDirty should be set after first Notify');
|
|
||||||
Assert.IsTrue(funcLazy.GetChanged.IsSet, 'Changed state should be true after first effective source notification');
|
|
||||||
result := funcLazy.Pop(value); // procCallCount becomes 2
|
|
||||||
Assert.IsTrue(result, 'Pop after first effective source change should return true');
|
|
||||||
Assert.AreEqual(firstSourceChangeValue, value, 'Value from Pop after first effective source change');
|
|
||||||
Assert.AreEqual(2, procCallCount, 'Proc called again');
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed state should be false after second Pop');
|
|
||||||
|
|
||||||
// 3. Notify sourceDirty again (sourceDirty is already TRUE)
|
|
||||||
Assert.IsTrue(sourceDirty.State.IsSet, 'sourceDirty is still set before second Notify attempt');
|
|
||||||
sourceDirty.Notify; // Since sourceDirty is already set, this does NOT notify funcLazy.FChanged.
|
|
||||||
// funcLazy.GetChanged.IsSet remains FALSE.
|
|
||||||
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed state should REMAIN false after Notify on an already-set source');
|
|
||||||
|
|
||||||
// 4. Attempt to Pop again
|
|
||||||
result := funcLazy.Pop(value); // procCallCount should remain 2
|
|
||||||
Assert.IsFalse(result, 'Pop after Notify on an already-set source should return false');
|
|
||||||
Assert.AreEqual(2, procCallCount, 'Proc should NOT have been called again');
|
|
||||||
// Value of 'value' is undefined here and not checked.
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_Destroy_UnsubscribesFromSource;
|
|
||||||
var
|
|
||||||
funcLazyObj: TMycFuncLazy<Integer>;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
tempVal: Integer;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag;
|
|
||||||
sourceDirty.Reset;
|
|
||||||
funcLazyObj := TMycFuncLazy<Integer>.Create(sourceDirty.State.Signal, function: Integer begin Result := 1; end);
|
|
||||||
Assert.IsTrue(funcLazyObj.Pop(tempVal), 'Initial Pop should succeed');
|
|
||||||
funcLazyObj.Destroy;
|
|
||||||
Assert.WillNotRaise(
|
|
||||||
procedure begin sourceDirty.Notify; end,
|
|
||||||
nil,
|
|
||||||
'Destroy test assumes TMycSubscription.Unsubscribe works. Verified by no crash on source notify post-destroy.'
|
|
||||||
);
|
|
||||||
sourceDirty := nil;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMycCoreLazy.TestFuncLazy_SourceIsInitiallySet_PopBehavesCorrectly;
|
|
||||||
var
|
|
||||||
funcLazy: TLazy<Integer>.ILazy;
|
|
||||||
value: Integer;
|
|
||||||
result: Boolean;
|
|
||||||
sourceDirty: TFlag.IFlag;
|
|
||||||
expectedValue: Integer;
|
|
||||||
procExecuted: Boolean;
|
|
||||||
begin
|
|
||||||
sourceDirty := TFlag.CreateFlag(true);
|
|
||||||
Assert.IsTrue(sourceDirty.State.IsSet, 'SourceDirty should be initially set for this test scenario');
|
|
||||||
expectedValue := 70;
|
|
||||||
procExecuted := False;
|
|
||||||
funcLazy :=
|
|
||||||
TMycFuncLazy<Integer>.Create(
|
|
||||||
sourceDirty.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
procExecuted := True;
|
|
||||||
Result := expectedValue;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
Assert.IsNotNull(funcLazy, 'TMycFuncLazy<Integer> instance should not be nil');
|
|
||||||
Assert.IsTrue(funcLazy.GetChanged.IsSet, 'funcLazy.GetChanged.IsSet should be true after creation (by design)');
|
|
||||||
value := 0;
|
|
||||||
result := funcLazy.Pop(value);
|
|
||||||
Assert.IsTrue(result, 'First Pop should return true');
|
|
||||||
Assert.IsTrue(procExecuted, 'FProc should have been executed on first Pop');
|
|
||||||
Assert.AreEqual(expectedValue, value, 'Value should be the result of FProc');
|
|
||||||
Assert.IsFalse(funcLazy.GetChanged.IsSet, 'Changed state should be false after the first Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
initialization
|
|
||||||
TDUnitX.RegisterTestFixture(TTestMycCoreLazy);
|
|
||||||
end.
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
unit Myc.Test.Lazy;
|
|
||||||
|
|
||||||
interface
|
|
||||||
|
|
||||||
uses
|
|
||||||
System.SysUtils,
|
|
||||||
DUnitX.TestFramework,
|
|
||||||
Myc.Signals, // For IMycState, TState, IMycDirty
|
|
||||||
Myc.Lazy; // The unit under test
|
|
||||||
|
|
||||||
type
|
|
||||||
[TestFixture]
|
|
||||||
TTestMyLazy = class(TObject)
|
|
||||||
private
|
|
||||||
FChangingSignal: TFlag.IFlag; // 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.
|
|
||||||
procedure ConsumeInitialPop(var ALazyRec: TLazy<Integer>; ExpectedInitialValue: Integer; const MsgPrefix: string);
|
|
||||||
public
|
|
||||||
[Setup]
|
|
||||||
procedure Setup;
|
|
||||||
[TearDown]
|
|
||||||
procedure TearDown;
|
|
||||||
|
|
||||||
// Tests for TLazy<T>.Create(nil) - Null Object Pattern
|
|
||||||
[Test]
|
|
||||||
procedure TestCreateWithNil_Changed_IsAlwaysTrue;
|
|
||||||
[Test]
|
|
||||||
procedure TestCreateWithNil_Pop_ReturnsTrueAndDefaultInteger;
|
|
||||||
[Test]
|
|
||||||
procedure TestCreateWithNil_Pop_ReturnsTrueAndDefaultString;
|
|
||||||
|
|
||||||
// Tests for TLazy<T>.Construct (creates a functional lazy object)
|
|
||||||
[Test]
|
|
||||||
procedure TestConstruct_InitialChanged_IsAlwaysTrue;
|
|
||||||
[Test]
|
|
||||||
procedure TestConstruct_FirstPop_SucceedsAndResetsChanged;
|
|
||||||
[Test]
|
|
||||||
procedure TestConstruct_StateInteraction_SignalTriggersChanged;
|
|
||||||
[Test]
|
|
||||||
procedure TestConstruct_StateInteraction_PopResetsChangedAfterSignal;
|
|
||||||
[Test]
|
|
||||||
procedure TestConstruct_StateInteraction_NotifyOnAlreadySetSource_DoesNotRetrigger;
|
|
||||||
[Test]
|
|
||||||
procedure TestConstruct_Destruction_UnsubscribesAndNoCrashOnSourceNotify;
|
|
||||||
|
|
||||||
// Tests for TLazy<T>.Create with a pre-existing (non-nil) IMycLazy
|
|
||||||
[Test]
|
|
||||||
procedure TestCreateWithExistingLazy_DelegatesChangedCorrectly;
|
|
||||||
[Test]
|
|
||||||
procedure TestCreateWithExistingLazy_DelegatesPopCorrectly;
|
|
||||||
|
|
||||||
// Tests for TLazy<T> implicit operators
|
|
||||||
[Test]
|
|
||||||
procedure TestImplicitOperator_FromInterfaceToRecord;
|
|
||||||
[Test]
|
|
||||||
procedure TestImplicitOperator_FromRecordToInterface;
|
|
||||||
|
|
||||||
// Tests for TLazy<T>.Pop specific behaviors (Res undefined)
|
|
||||||
[Test]
|
|
||||||
procedure TestPop_AfterInitialAndNoSignal_ReturnsFalseAndResUndefined;
|
|
||||||
|
|
||||||
end;
|
|
||||||
|
|
||||||
implementation
|
|
||||||
|
|
||||||
// No direct uses of Myc.Core.* units here
|
|
||||||
|
|
||||||
{ TTestMyLazy }
|
|
||||||
|
|
||||||
procedure TTestMyLazy.Setup;
|
|
||||||
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 := TFlag.CreateFlag;
|
|
||||||
FChangingSignal.Reset; // Start with a clean (not set) signal for predictable test starts
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TearDown;
|
|
||||||
begin
|
|
||||||
FChangingSignal := nil; // Release the common signal source
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.ConsumeInitialPop(var ALazyRec: TLazy<Integer>; ExpectedInitialValue: Integer; const MsgPrefix: string);
|
|
||||||
var
|
|
||||||
val: Integer;
|
|
||||||
popResult: Boolean;
|
|
||||||
begin
|
|
||||||
Assert.IsTrue(ALazyRec.Changed.IsSet, MsgPrefix + ': Changed.IsSet should be true before initial Pop');
|
|
||||||
popResult := ALazyRec.Pop(val);
|
|
||||||
Assert.IsTrue(popResult, MsgPrefix + ': Initial Pop should return true');
|
|
||||||
Assert.AreEqual(ExpectedInitialValue, val, MsgPrefix + ': Value from initial Pop mismatch');
|
|
||||||
Assert.IsFalse(ALazyRec.Changed.IsSet, MsgPrefix + ': Changed.IsSet should be false after initial Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TLazy<T>.Create(nil) - Null Object Pattern ==
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestCreateWithNil_Changed_IsAlwaysTrue;
|
|
||||||
var
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
begin
|
|
||||||
lazyRec := TLazy<Integer>.Create(nil); // This uses the internal FNull (TMycNullLazy)
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'For TLazy created with nil, Changed.IsSet should be true (TMycNullLazy behavior)');
|
|
||||||
// Second check to ensure it's consistently true
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'For TLazy created with nil, Changed.IsSet should remain true');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestCreateWithNil_Pop_ReturnsTrueAndDefaultInteger;
|
|
||||||
var
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
val: Integer;
|
|
||||||
popResult: Boolean;
|
|
||||||
begin
|
|
||||||
lazyRec := TLazy<Integer>.Create(nil);
|
|
||||||
val := 12345; // Pre-assign to check if Pop overwrites it with Default
|
|
||||||
popResult := lazyRec.Pop(val);
|
|
||||||
Assert.IsTrue(popResult, 'Pop on TLazy created with nil should return true');
|
|
||||||
Assert.AreEqual(Default(Integer), val, 'Pop on TLazy created with nil should set Res to Default(Integer)');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestCreateWithNil_Pop_ReturnsTrueAndDefaultString;
|
|
||||||
var
|
|
||||||
lazyRec: TLazy<string>;
|
|
||||||
val: string;
|
|
||||||
popResult: Boolean;
|
|
||||||
begin
|
|
||||||
lazyRec := TLazy<string>.Create(nil);
|
|
||||||
val := 'test'; // Pre-assign
|
|
||||||
popResult := lazyRec.Pop(val);
|
|
||||||
Assert.IsTrue(popResult, 'Pop on TLazy created with nil (string) should return true');
|
|
||||||
Assert.AreEqual(Default(string), val, 'Pop on TLazy created with nil (string) should set Res to Default(string)');
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TLazy<T>.Construct static method ==
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestConstruct_InitialChanged_IsAlwaysTrue;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
procExecuted: Boolean;
|
|
||||||
begin
|
|
||||||
procExecuted := False;
|
|
||||||
// FChangingSignal is reset in Setup
|
|
||||||
lazyIntf :=
|
|
||||||
TLazy<Integer>.Construct(
|
|
||||||
FChangingSignal.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
procExecuted := True;
|
|
||||||
Result := 10;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
Assert.IsNotNull(lazyIntf, 'TLazy.Construct should return a valid interface');
|
|
||||||
|
|
||||||
lazyRec := lazyIntf; // Implicit conversion
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'Constructed lazy object: Initial Changed.IsSet should be true by design');
|
|
||||||
Assert.IsFalse(procExecuted, 'Proc should not have been executed by Construct or by checking Changed state');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestConstruct_FirstPop_SucceedsAndResetsChanged;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
procExecuted: Boolean;
|
|
||||||
expectedValue: Integer;
|
|
||||||
begin
|
|
||||||
procExecuted := False;
|
|
||||||
expectedValue := 20;
|
|
||||||
lazyIntf :=
|
|
||||||
TLazy<Integer>.Construct(
|
|
||||||
FChangingSignal.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
procExecuted := True;
|
|
||||||
Result := expectedValue;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
lazyRec := lazyIntf;
|
|
||||||
|
|
||||||
ConsumeInitialPop(lazyRec, expectedValue, 'TestConstruct_FirstPop');
|
|
||||||
Assert.IsTrue(procExecuted, 'Proc should have been executed by the initial Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestConstruct_StateInteraction_SignalTriggersChanged;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
expectedValue: Integer;
|
|
||||||
begin
|
|
||||||
expectedValue := 30;
|
|
||||||
lazyIntf := TLazy<Integer>.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end);
|
|
||||||
lazyRec := lazyIntf;
|
|
||||||
|
|
||||||
ConsumeInitialPop(lazyRec, expectedValue, 'TestConstruct_StateInteraction_SignalTriggersChanged (Initial)');
|
|
||||||
Assert.IsFalse(lazyRec.Changed.IsSet, 'After initial Pop, Changed.IsSet should be false');
|
|
||||||
|
|
||||||
FChangingSignal.Notify; // Trigger the source signal
|
|
||||||
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'After source signal Notify, Changed.IsSet should become true');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestConstruct_StateInteraction_PopResetsChangedAfterSignal;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
val: Integer;
|
|
||||||
popResult: Boolean;
|
|
||||||
procCallCount: Integer;
|
|
||||||
begin
|
|
||||||
procCallCount := 0;
|
|
||||||
lazyIntf :=
|
|
||||||
TLazy<Integer>.Construct(
|
|
||||||
FChangingSignal.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procCallCount);
|
|
||||||
Result := 100 + procCallCount; // Value changes per call
|
|
||||||
end
|
|
||||||
);
|
|
||||||
lazyRec := lazyIntf;
|
|
||||||
|
|
||||||
ConsumeInitialPop(lazyRec, 101, 'TestConstruct_StateInteraction_PopResetsChangedAfterSignal (Initial)'); // procCallCount = 1
|
|
||||||
FChangingSignal.Notify;
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'Changed.IsSet should be true after signal');
|
|
||||||
|
|
||||||
popResult := lazyRec.Pop(val); // procCallCount = 2
|
|
||||||
Assert.IsTrue(popResult, 'Pop after signal should return true');
|
|
||||||
Assert.AreEqual(102, val, 'Value from Pop after signal mismatch');
|
|
||||||
Assert.AreEqual(2, procCallCount, 'Proc call count after second pop mismatch');
|
|
||||||
Assert.IsFalse(lazyRec.Changed.IsSet, 'Changed.IsSet should be false after Pop following signal');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestConstruct_StateInteraction_NotifyOnAlreadySetSource_DoesNotRetrigger;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
val: Integer;
|
|
||||||
popResult: Boolean;
|
|
||||||
procCallCount: Integer;
|
|
||||||
begin
|
|
||||||
procCallCount := 0;
|
|
||||||
lazyIntf :=
|
|
||||||
TLazy<Integer>.Construct(
|
|
||||||
FChangingSignal.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procCallCount);
|
|
||||||
Result := 200 + procCallCount;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
lazyRec := lazyIntf;
|
|
||||||
|
|
||||||
// 1. Initial Pop
|
|
||||||
ConsumeInitialPop(lazyRec, 201, 'TestConstruct_NotifyOnAlreadySetSource (Initial)'); // procCallCount = 1
|
|
||||||
Assert.IsFalse(FChangingSignal.State.IsSet, 'Source signal FChangingSignal should still be false (was reset in Setup)');
|
|
||||||
|
|
||||||
// 2. Trigger source, make it set, Pop
|
|
||||||
FChangingSignal.Notify; // FChangingSignal.IsSet becomes TRUE
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'Lazy state should be true after FChangingSignal.Notify');
|
|
||||||
popResult := lazyRec.Pop(val); // procCallCount = 2
|
|
||||||
Assert.IsTrue(popResult);
|
|
||||||
Assert.AreEqual(202, val);
|
|
||||||
Assert.IsFalse(lazyRec.Changed.IsSet, 'Lazy state should be false after second Pop');
|
|
||||||
|
|
||||||
// 3. Notify FChangingSignal again. It's already set.
|
|
||||||
// This should NOT re-notify subscribers (like the lazy object's internal trigger)
|
|
||||||
// because TMycDirty only notifies on a false -> true transition.
|
|
||||||
Assert.IsTrue(FChangingSignal.State.IsSet, 'FChangingSignal should still be true before redundant Notify');
|
|
||||||
FChangingSignal.Notify;
|
|
||||||
|
|
||||||
Assert.IsFalse(lazyRec.Changed.IsSet, 'Lazy state should REMAIN false after Notify on an already-set source');
|
|
||||||
|
|
||||||
// 4. Attempt to Pop again
|
|
||||||
popResult := lazyRec.Pop(val); // procCallCount should remain 2
|
|
||||||
Assert.IsFalse(popResult, 'Pop after Notify on an already-set source should return false');
|
|
||||||
Assert.AreEqual(2, procCallCount, 'Proc should not have been called for this Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestConstruct_Destruction_UnsubscribesAndNoCrashOnSourceNotify;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
localChangingSignal: TFlag.IFlag; // Use a local signal for this test to control its lifetime
|
|
||||||
begin
|
|
||||||
localChangingSignal := TFlag.CreateFlag;
|
|
||||||
localChangingSignal.Reset;
|
|
||||||
|
|
||||||
lazyIntf := TLazy<Integer>.Construct(localChangingSignal.State.Signal, function: Integer begin Result := 1; end);
|
|
||||||
Assert.IsNotNull(lazyIntf, 'Constructed lazy interface should not be nil');
|
|
||||||
|
|
||||||
// Simulate usage and release of the lazy object
|
|
||||||
var lazyRec: TLazy<Integer> := lazyIntf; // Wrap for initial pop
|
|
||||||
ConsumeInitialPop(lazyRec, 1, 'TestConstruct_Destruction (Initial)');
|
|
||||||
|
|
||||||
lazyIntf := nil; // Release the ILazy interface. ARC should destroy the TMycFuncLazy object.
|
|
||||||
// This should trigger its destructor, which should unsubscribe from localChangingSignal.
|
|
||||||
|
|
||||||
Assert.WillNotRaise(
|
|
||||||
procedure
|
|
||||||
begin
|
|
||||||
localChangingSignal.Notify; // Notify the source AFTER the lazy object is supposed to be gone.
|
|
||||||
end,
|
|
||||||
nil, // Default: any exception is a failure
|
|
||||||
'Notifying source after lazy object is freed should not crash, indicating unsubscription.'
|
|
||||||
);
|
|
||||||
|
|
||||||
localChangingSignal := nil; // Clean up the local signal itself.
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TLazy<T>.Create with a pre-existing (non-nil) ILazy ==
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestCreateWithExistingLazy_DelegatesChangedCorrectly;
|
|
||||||
var
|
|
||||||
originalLazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
wrappedLazyRec: TLazy<Integer>;
|
|
||||||
expectedValue: Integer;
|
|
||||||
begin
|
|
||||||
expectedValue := 60;
|
|
||||||
originalLazyIntf := TLazy<Integer>.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end);
|
|
||||||
// originalLazyIntf.Changed.IsSet is true by design
|
|
||||||
|
|
||||||
wrappedLazyRec := TLazy<Integer>.Create(originalLazyIntf);
|
|
||||||
Assert.IsTrue(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet should reflect original (initially true)');
|
|
||||||
|
|
||||||
ConsumeInitialPop(wrappedLazyRec, expectedValue, 'TestCreateWithExistingLazy_DelegatesChangedCorrectly (Initial)');
|
|
||||||
// Now wrappedLazyRec.Changed.IsSet is false, and so should originalLazyIntf.Changed.IsSet
|
|
||||||
|
|
||||||
Assert.IsFalse(originalLazyIntf.Changed.IsSet, 'Original lazy: Changed.IsSet should also be false after wrapped Pop');
|
|
||||||
|
|
||||||
FChangingSignal.Notify;
|
|
||||||
Assert.IsTrue(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet should be true after source signal');
|
|
||||||
Assert.IsTrue(originalLazyIntf.Changed.IsSet, 'Original lazy: Changed.IsSet should also be true after source signal');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestCreateWithExistingLazy_DelegatesPopCorrectly;
|
|
||||||
var
|
|
||||||
originalLazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
wrappedLazyRec: TLazy<Integer>;
|
|
||||||
val: Integer;
|
|
||||||
popResult: Boolean;
|
|
||||||
procCallCount: Integer;
|
|
||||||
begin
|
|
||||||
procCallCount := 0;
|
|
||||||
originalLazyIntf :=
|
|
||||||
TLazy<Integer>.Construct(
|
|
||||||
FChangingSignal.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
Inc(procCallCount);
|
|
||||||
Result := 70 + procCallCount;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
wrappedLazyRec := TLazy<Integer>.Create(originalLazyIntf);
|
|
||||||
|
|
||||||
// First Pop via wrapper (initial pop)
|
|
||||||
ConsumeInitialPop(wrappedLazyRec, 71, 'TestCreateWithExistingLazy_DelegatesPopCorrectly (Initial)'); // procCallCount = 1
|
|
||||||
Assert.AreEqual(1, procCallCount, 'Proc call count after wrapped initial Pop');
|
|
||||||
|
|
||||||
// Second Pop via wrapper (no source change yet)
|
|
||||||
popResult := wrappedLazyRec.Pop(val);
|
|
||||||
Assert.IsFalse(popResult, 'Second Pop via wrapper (no source change) should return false');
|
|
||||||
Assert.AreEqual(1, procCallCount, 'Proc call count should not change');
|
|
||||||
|
|
||||||
// Trigger source, Pop via wrapper
|
|
||||||
FChangingSignal.Notify;
|
|
||||||
Assert.IsTrue(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet true after source signal');
|
|
||||||
popResult := wrappedLazyRec.Pop(val); // procCallCount = 2
|
|
||||||
Assert.IsTrue(popResult, 'Pop via wrapper after source signal should return true');
|
|
||||||
Assert.AreEqual(72, val, 'Value from Pop via wrapper after signal');
|
|
||||||
Assert.AreEqual(2, procCallCount, 'Proc call count after signal and Pop');
|
|
||||||
Assert.IsFalse(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet false after Pop');
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TLazy<T> implicit operators ==
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestImplicitOperator_FromInterfaceToRecord;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
expectedValue: Integer;
|
|
||||||
begin
|
|
||||||
expectedValue := 80;
|
|
||||||
lazyIntf := TLazy<Integer>.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end);
|
|
||||||
Assert.IsNotNull(lazyIntf, 'Interface should be assigned');
|
|
||||||
|
|
||||||
lazyRec := lazyIntf; // Implicit conversion: ILazy<T> to TLazy<T>
|
|
||||||
|
|
||||||
// Verify by using the record
|
|
||||||
Assert.IsTrue(lazyRec.Changed.IsSet, 'Record (from intf): Initial Changed.IsSet should be true');
|
|
||||||
ConsumeInitialPop(lazyRec, expectedValue, 'TestImplicitOperator_FromInterfaceToRecord');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestMyLazy.TestImplicitOperator_FromRecordToInterface;
|
|
||||||
var
|
|
||||||
lazyIntfFromConstruct: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
lazyIntfFromRecord: TLazy<Integer>.ILazy;
|
|
||||||
val: Integer;
|
|
||||||
expectedValue: Integer;
|
|
||||||
begin
|
|
||||||
expectedValue := 90;
|
|
||||||
lazyIntfFromConstruct := TLazy<Integer>.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end);
|
|
||||||
lazyRec.Create(lazyIntfFromConstruct); // Explicitly create record
|
|
||||||
|
|
||||||
lazyIntfFromRecord := lazyRec; // Implicit conversion: TLazy<T> to ILazy<T>
|
|
||||||
|
|
||||||
// Verify by using the converted interface
|
|
||||||
Assert.AreSame(lazyIntfFromConstruct, lazyIntfFromRecord, 'Converted interface should be the same as the original wrapped one');
|
|
||||||
Assert.IsTrue(lazyIntfFromRecord.Changed.IsSet, 'Interface (from rec): Initial Changed.IsSet should be true');
|
|
||||||
var popResult := lazyIntfFromRecord.Pop(val); // This also tests if the interface is functional
|
|
||||||
Assert.IsTrue(popResult);
|
|
||||||
Assert.AreEqual(expectedValue, val);
|
|
||||||
Assert.IsFalse(lazyIntfFromRecord.Changed.IsSet);
|
|
||||||
end;
|
|
||||||
|
|
||||||
// == Tests for TLazy<T>.Pop specific behaviors (Res undefined) ==
|
|
||||||
procedure TTestMyLazy.TestPop_AfterInitialAndNoSignal_ReturnsFalseAndResUndefined;
|
|
||||||
var
|
|
||||||
lazyIntf: TLazy<Integer>.ILazy;
|
|
||||||
lazyRec: TLazy<Integer>;
|
|
||||||
val: Integer; // Value will not be checked as Pop returns false
|
|
||||||
popResult: Boolean;
|
|
||||||
procExecuted: Boolean;
|
|
||||||
begin
|
|
||||||
procExecuted := False;
|
|
||||||
lazyIntf :=
|
|
||||||
TLazy<Integer>.Construct(
|
|
||||||
FChangingSignal.State.Signal,
|
|
||||||
function: Integer
|
|
||||||
begin
|
|
||||||
procExecuted := True;
|
|
||||||
Result := 100;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
lazyRec := lazyIntf;
|
|
||||||
|
|
||||||
ConsumeInitialPop(lazyRec, 100, 'TestPop_AfterInitialAndNoSignal (Initial)');
|
|
||||||
Assert.IsTrue(procExecuted, 'Proc should have run for initial pop');
|
|
||||||
procExecuted := False; // Reset for next check
|
|
||||||
|
|
||||||
// FChangingSignal has not been notified again
|
|
||||||
Assert.IsFalse(lazyRec.Changed.IsSet, 'Changed.IsSet must be false before this Pop attempt');
|
|
||||||
popResult := lazyRec.Pop(val);
|
|
||||||
|
|
||||||
Assert.IsFalse(popResult, 'Pop when Changed.IsSet is false should return false');
|
|
||||||
Assert.IsFalse(procExecuted, 'Proc should NOT have run as Pop returned false');
|
|
||||||
// Do NOT check 'val' as its content is undefined when Pop returns false.
|
|
||||||
end;
|
|
||||||
|
|
||||||
initialization
|
|
||||||
TDUnitX.RegisterTestFixture(TTestMyLazy);
|
|
||||||
end.
|
|
||||||
@@ -6,6 +6,7 @@ uses
|
|||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
System.Classes,
|
System.Classes,
|
||||||
DUnitX.TestFramework,
|
DUnitX.TestFramework,
|
||||||
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataPoint;
|
Myc.Trade.DataPoint;
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ uses
|
|||||||
System.Generics.Collections,
|
System.Generics.Collections,
|
||||||
DUnitX.TestFramework,
|
DUnitX.TestFramework,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.Futures,
|
Myc.Futures,
|
||||||
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
Myc.Trade.DataStream;
|
Myc.Trade.DataStream;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ interface
|
|||||||
|
|
||||||
uses
|
uses
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
Myc.Trade.DataStream;
|
Myc.Trade.DataStream;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ uses
|
|||||||
System.IOUtils,
|
System.IOUtils,
|
||||||
Myc.Futures,
|
Myc.Futures,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Lazy,
|
Myc.Mutable,
|
||||||
Myc.Trade.DataPoint,
|
Myc.Trade.DataPoint,
|
||||||
Myc.Core.FileCache;
|
Myc.Core.FileCache;
|
||||||
|
|
||||||
|
|||||||
+6
-11
@@ -8,27 +8,20 @@ uses
|
|||||||
FastMM5,
|
FastMM5,
|
||||||
DUnitX.MemoryLeakMonitor.FastMM5,
|
DUnitX.MemoryLeakMonitor.FastMM5,
|
||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
{$IFDEF TESTINSIGHT}
|
{$IFDEF TESTINSIGHT}
|
||||||
TestInsight.DUnitX,
|
TestInsight.DUnitX,
|
||||||
{$ELSE}
|
{$ELSE}
|
||||||
DUnitX.Loggers.Console,
|
DUnitX.Loggers.Console,
|
||||||
{$ENDIF }
|
{$ENDIF }
|
||||||
DUnitX.TestFramework,
|
DUnitX.TestFramework,
|
||||||
TestNotifier in 'TestNotifier.pas',
|
TestNotifier in 'TestNotifier.pas',
|
||||||
TestNotifier_Threading in 'TestNotifier_Threading.pas' {/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',},
|
TestNotifier_Threading in 'TestNotifier_Threading.pas' {/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',},
|
||||||
TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',
|
TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',
|
||||||
Myc.Core.Tasks in '..\Src\Myc.Core.Tasks.pas',
|
|
||||||
TestTasks in 'TestTasks.pas',
|
TestTasks in 'TestTasks.pas',
|
||||||
Myc.Futures in '..\Src\Myc.Futures.pas',
|
Myc.Futures in '..\Src\Myc.Futures.pas',
|
||||||
TestCoreFutures in 'TestCoreFutures.pas',
|
TestCoreFutures in 'TestCoreFutures.pas',
|
||||||
Myc.TaskManager in '..\Src\Myc.TaskManager.pas',
|
Myc.TaskManager in '..\Src\Myc.TaskManager.pas',
|
||||||
Myc.Core.Futures in '..\Src\Myc.Core.Futures.pas',
|
|
||||||
Myc.Core.Signals in '..\Src\Myc.Core.Signals.pas',
|
|
||||||
TestFutures in 'TestFutures.pas',
|
TestFutures in 'TestFutures.pas',
|
||||||
Myc.Lazy in '..\Src\Myc.Lazy.pas',
|
|
||||||
Myc.Core.Lazy in '..\Src\Myc.Core.Lazy.pas',
|
|
||||||
Myc.Test.Core.Lazy in '..\Src\Myc.Test.Core.Lazy.pas',
|
|
||||||
Myc.Test.Lazy in '..\Src\Myc.Test.Lazy.pas',
|
|
||||||
Myc.Test.Core.Atomic in '..\Src\Myc.Test.Core.Atomic.pas',
|
Myc.Test.Core.Atomic in '..\Src\Myc.Test.Core.Atomic.pas',
|
||||||
Myc.Test.Signals.Latch in '..\Src\Myc.Test.Signals.Latch.pas',
|
Myc.Test.Signals.Latch in '..\Src\Myc.Test.Signals.Latch.pas',
|
||||||
Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas',
|
Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas',
|
||||||
@@ -37,7 +30,9 @@ uses
|
|||||||
Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas',
|
Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas',
|
||||||
Myc.Test.Trade.DataStream in '..\Src\Myc.Test.Trade.DataStream.pas',
|
Myc.Test.Trade.DataStream in '..\Src\Myc.Test.Trade.DataStream.pas',
|
||||||
Myc.Test.Trade.DataPoint in '..\Src\Myc.Test.Trade.DataPoint.pas',
|
Myc.Test.Trade.DataPoint in '..\Src\Myc.Test.Trade.DataPoint.pas',
|
||||||
Myc.Trade.DataProvider in '..\Src\Myc.Trade.DataProvider.pas';
|
Myc.Trade.DataProvider in '..\Src\Myc.Trade.DataProvider.pas',
|
||||||
|
Myc.Mutable in '..\Src\Myc.Mutable.pas',
|
||||||
|
Test.Core.Mutable in 'Test.Core.Mutable.pas';
|
||||||
|
|
||||||
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
|
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
|
||||||
{$IFNDEF TESTINSIGHT}
|
{$IFNDEF TESTINSIGHT}
|
||||||
|
|||||||
+2
-7
@@ -117,18 +117,11 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
|||||||
<Form>/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',</Form>
|
<Form>/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',</Form>
|
||||||
</DCCReference>
|
</DCCReference>
|
||||||
<DCCReference Include="TestNotifier_ChaosStress.pas"/>
|
<DCCReference Include="TestNotifier_ChaosStress.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Core.Tasks.pas"/>
|
|
||||||
<DCCReference Include="TestTasks.pas"/>
|
<DCCReference Include="TestTasks.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Futures.pas"/>
|
<DCCReference Include="..\Src\Myc.Futures.pas"/>
|
||||||
<DCCReference Include="TestCoreFutures.pas"/>
|
<DCCReference Include="TestCoreFutures.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.TaskManager.pas"/>
|
<DCCReference Include="..\Src\Myc.TaskManager.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Core.Futures.pas"/>
|
|
||||||
<DCCReference Include="..\Src\Myc.Core.Signals.pas"/>
|
|
||||||
<DCCReference Include="TestFutures.pas"/>
|
<DCCReference Include="TestFutures.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Lazy.pas"/>
|
|
||||||
<DCCReference Include="..\Src\Myc.Core.Lazy.pas"/>
|
|
||||||
<DCCReference Include="..\Src\Myc.Test.Core.Lazy.pas"/>
|
|
||||||
<DCCReference Include="..\Src\Myc.Test.Lazy.pas"/>
|
|
||||||
<DCCReference Include="..\Src\Myc.Test.Core.Atomic.pas"/>
|
<DCCReference Include="..\Src\Myc.Test.Core.Atomic.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Test.Signals.Latch.pas"/>
|
<DCCReference Include="..\Src\Myc.Test.Signals.Latch.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Test.Signals.Dirty.pas"/>
|
<DCCReference Include="..\Src\Myc.Test.Signals.Dirty.pas"/>
|
||||||
@@ -138,6 +131,8 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
|||||||
<DCCReference Include="..\Src\Myc.Test.Trade.DataStream.pas"/>
|
<DCCReference Include="..\Src\Myc.Test.Trade.DataStream.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Test.Trade.DataPoint.pas"/>
|
<DCCReference Include="..\Src\Myc.Test.Trade.DataPoint.pas"/>
|
||||||
<DCCReference Include="..\Src\Myc.Trade.DataProvider.pas"/>
|
<DCCReference Include="..\Src\Myc.Trade.DataProvider.pas"/>
|
||||||
|
<DCCReference Include="..\Src\Myc.Mutable.pas"/>
|
||||||
|
<DCCReference Include="Test.Core.Mutable.pas"/>
|
||||||
<BuildConfiguration Include="Base">
|
<BuildConfiguration Include="Base">
|
||||||
<Key>Base</Key>
|
<Key>Base</Key>
|
||||||
</BuildConfiguration>
|
</BuildConfiguration>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -10,7 +10,7 @@ uses
|
|||||||
Myc.Signals, // For IMycLatch, TMycLatch, IMycState, IMycSubscriber [cite: 62, 68, 53, 45]
|
Myc.Signals, // For IMycLatch, TMycLatch, IMycState, IMycSubscriber [cite: 62, 68, 53, 45]
|
||||||
Myc.Core.Tasks, // For IMycTaskFactory, TMycTaskFactory [cite: 97, 113]
|
Myc.Core.Tasks, // For IMycTaskFactory, TMycTaskFactory [cite: 97, 113]
|
||||||
Myc.Futures,
|
Myc.Futures,
|
||||||
Myc.Core.Futures; // For IMycFuture, TMycInitStateFuncFuture
|
Myc.Core.Future; // For IMycFuture, TMycInitStateFuncFuture
|
||||||
|
|
||||||
type
|
type
|
||||||
// Helper class to notify a latch when its Notify method is called.
|
// Helper class to notify a latch when its Notify method is called.
|
||||||
|
|||||||
Reference in New Issue
Block a user