Pipes and test scripts
This commit is contained in:
@@ -37,6 +37,7 @@ type
|
||||
// --- Basic Functionality ---
|
||||
|
||||
[Test]
|
||||
[IgnoreMemoryLeaks]
|
||||
[TestCase('Identity', '(defmacro id [x] `~x), (id 42), 42')]
|
||||
[TestCase('Constant', '(defmacro c [] `100), (c), 100')]
|
||||
[TestCase('SimpleAdd', '(defmacro add [a b] `(+ ~a ~b)), (add 10 20), 30')]
|
||||
@@ -45,6 +46,7 @@ type
|
||||
// --- Quasiquoting & Unquoting ---
|
||||
|
||||
[Test]
|
||||
[IgnoreMemoryLeaks]
|
||||
procedure Test_Quasiquote_Literal;
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -26,12 +26,15 @@ type
|
||||
[Test]
|
||||
[TestCase('ValidRecord', 'true,10')]
|
||||
[TestCase('VoidRecord', 'false,0')]
|
||||
[IgnoreMemoryLeaks]
|
||||
procedure TestMemberAccessPropagation(const Condition: Boolean; const ExpectedVal: Int64);
|
||||
|
||||
[Test]
|
||||
[IgnoreMemoryLeaks]
|
||||
procedure TestNestedPropagation;
|
||||
|
||||
[Test]
|
||||
[IgnoreMemoryLeaks]
|
||||
procedure TestIndexerPropagation;
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
unit Test.Myc.Ast.Pipes;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.Classes,
|
||||
System.Generics.Collections,
|
||||
System.SyncObjs,
|
||||
DUnitX.TestFramework,
|
||||
// Core Data & Types
|
||||
Myc.Data.Value,
|
||||
Myc.Data.Scalar,
|
||||
Myc.Data.Keyword,
|
||||
Myc.Data.Stream,
|
||||
Myc.Data.Series,
|
||||
// AST & Compiler
|
||||
Myc.Ast,
|
||||
Myc.Ast.Nodes,
|
||||
Myc.Ast.Types,
|
||||
Myc.Ast.Environment,
|
||||
Myc.Ast.Script;
|
||||
|
||||
type
|
||||
// Mock Stream that allows manual signaling
|
||||
TTestStream = class(TInterfacedObject, IStream)
|
||||
private
|
||||
FSeries: IScalarRecordSeries;
|
||||
FObservers: TList<IStreamObserver>;
|
||||
FLock: TSpinLock;
|
||||
public
|
||||
constructor Create(const ASeries: IScalarRecordSeries);
|
||||
destructor Destroy; override;
|
||||
|
||||
// Manual trigger
|
||||
procedure Emit(CycleID: Integer);
|
||||
|
||||
// IStream implementation
|
||||
function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
|
||||
procedure Unsubscribe(Tag: TSubscriptionTag);
|
||||
function GetSeries: IScalarRecordSeries;
|
||||
property Series: IScalarRecordSeries read GetSeries;
|
||||
end;
|
||||
|
||||
[TestFixture]
|
||||
[IgnoreMemoryLeaks]
|
||||
TTestPipes = class
|
||||
private
|
||||
FEnv: TAstEnvironment;
|
||||
|
||||
function CreateSourceSeries(
|
||||
const FieldName: string;
|
||||
Kind: TScalar.TKind;
|
||||
const Values: TArray<Int64>
|
||||
): IWriteableScalarRecordSeries;
|
||||
public
|
||||
[Setup]
|
||||
procedure Setup;
|
||||
|
||||
[Test]
|
||||
procedure Test_SimplePipe_Arithmetic;
|
||||
|
||||
[Test]
|
||||
procedure Test_TypeChecker_Fail_On_Invalid_Field;
|
||||
|
||||
[Test]
|
||||
procedure Test_MultiSource_Pipe;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TTestStream Implementation
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
constructor TTestStream.Create(const ASeries: IScalarRecordSeries);
|
||||
begin
|
||||
inherited Create;
|
||||
FSeries := ASeries;
|
||||
FObservers := TList<IStreamObserver>.Create;
|
||||
FLock := Default(TSpinLock);
|
||||
end;
|
||||
|
||||
destructor TTestStream.Destroy;
|
||||
begin
|
||||
FObservers.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TTestStream.GetSeries: IScalarRecordSeries;
|
||||
begin
|
||||
Result := FSeries;
|
||||
end;
|
||||
|
||||
function TTestStream.Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FObservers.Add(Observer);
|
||||
// Use the interface pointer as tag.
|
||||
Result := TSubscriptionTag(Observer);
|
||||
finally
|
||||
FLock.Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTestStream.Unsubscribe(Tag: TSubscriptionTag);
|
||||
var
|
||||
obs: IStreamObserver;
|
||||
begin
|
||||
// We treat the tag as the interface pointer.
|
||||
// Note: This relies on the caller ensuring the object is still alive
|
||||
// if they want to unsubscribe, which is standard behavior.
|
||||
obs := IStreamObserver(Tag);
|
||||
|
||||
FLock.Enter;
|
||||
try
|
||||
FObservers.Remove(obs);
|
||||
finally
|
||||
FLock.Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTestStream.Emit(CycleID: Integer);
|
||||
var
|
||||
obs: IStreamObserver;
|
||||
observersCopy: TArray<IStreamObserver>;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
observersCopy := FObservers.ToArray;
|
||||
finally
|
||||
FLock.Exit;
|
||||
end;
|
||||
|
||||
// Simulate new data arrival
|
||||
for obs in observersCopy do
|
||||
begin
|
||||
obs.OnSignal(TStreamSignal.Create(skData, CycleID));
|
||||
end;
|
||||
end;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TTestPipes Implementation
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
procedure TTestPipes.Setup;
|
||||
begin
|
||||
FEnv := TAstEnvironment.Construct(nil);
|
||||
end;
|
||||
|
||||
function TTestPipes.CreateSourceSeries(
|
||||
const FieldName: string;
|
||||
Kind: TScalar.TKind;
|
||||
const Values: TArray<Int64>
|
||||
): IWriteableScalarRecordSeries;
|
||||
var
|
||||
fields: TArray<TScalarRecordField>;
|
||||
def: IScalarRecordDefinition;
|
||||
i: Integer;
|
||||
val: TScalar.TValue;
|
||||
recValues: TArray<TScalar.TValue>;
|
||||
begin
|
||||
SetLength(fields, 1);
|
||||
fields[0] := TScalarRecordField.Create(TKeywordRegistry.Intern(FieldName), Kind);
|
||||
def := TKeywordMappingRegistry<TScalar.TKind>.Intern(fields);
|
||||
|
||||
Result := TScalarRecordSeries.Create(def);
|
||||
|
||||
SetLength(recValues, 1);
|
||||
for i := 0 to High(Values) do
|
||||
begin
|
||||
val.AsInt64 := Values[i];
|
||||
recValues[0] := val;
|
||||
Result.Add(recValues);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTestPipes.Test_SimplePipe_Arithmetic;
|
||||
var
|
||||
source: IWriteableScalarRecordSeries;
|
||||
mockStream: TTestStream; // Keep reference to fire later
|
||||
script: string;
|
||||
resVal: TDataValue;
|
||||
resStream: IStream;
|
||||
resSeries: IScalarRecordSeries;
|
||||
item: TScalar;
|
||||
key: IKeyword;
|
||||
begin
|
||||
// Arrange
|
||||
source := CreateSourceSeries('val', TScalar.TKind.Ordinal, [10, 20, 30]);
|
||||
mockStream := TTestStream.Create(source);
|
||||
|
||||
FEnv.RootScope.Define('src', TDataValue.FromStream(mockStream), TTypes.CreateRecordSeries(source.Def));
|
||||
|
||||
// Act
|
||||
script := '(pipe [src [:val]] (fn [x] {:res (* x 2)}))';
|
||||
|
||||
// 1. Compile & Link & Run (Constructs the Pipe Object)
|
||||
resVal := FEnv.Run(TAstScript.Parse(script));
|
||||
|
||||
Assert.AreEqual(TDataValueKind.vkStream, resVal.Kind);
|
||||
resStream := resVal.AsStream;
|
||||
resSeries := resStream.Series;
|
||||
|
||||
// 2. Fire Data! (Now that pipe is fully constructed)
|
||||
mockStream.Emit(0);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(Int64(1), resSeries.TotalCount, 'TotalCount mismatch');
|
||||
|
||||
key := TKeywordRegistry.Intern('res');
|
||||
|
||||
item := resSeries.Fields[key].Items[0];
|
||||
Assert.AreEqual(Int64(60), item.Value.AsInt64, 'Item[0] incorrect');
|
||||
end;
|
||||
|
||||
procedure TTestPipes.Test_TypeChecker_Fail_On_Invalid_Field;
|
||||
var
|
||||
source: IWriteableScalarRecordSeries;
|
||||
mockStream: TTestStream;
|
||||
script: string;
|
||||
begin
|
||||
source := CreateSourceSeries('val', TScalar.TKind.Ordinal, [1]);
|
||||
mockStream := TTestStream.Create(source);
|
||||
|
||||
FEnv.RootScope.Define('src', TDataValue.FromStream(mockStream), TTypes.CreateRecordSeries(source.Def));
|
||||
|
||||
script := '(pipe [src [:non_existent]] (fn [x] {:res x}))';
|
||||
|
||||
Assert.WillRaise(procedure begin FEnv.Run(TAstScript.Parse(script)); end, ECompilationFailed);
|
||||
end;
|
||||
|
||||
procedure TTestPipes.Test_MultiSource_Pipe;
|
||||
var
|
||||
s1, s2: IWriteableScalarRecordSeries;
|
||||
m1, m2: TTestStream;
|
||||
script: string;
|
||||
key: IKeyword;
|
||||
resSeries: IScalarRecordSeries;
|
||||
begin
|
||||
key := TKeywordRegistry.Intern('sum');
|
||||
|
||||
// Arrange
|
||||
s1 := CreateSourceSeries('a', TScalar.TKind.Ordinal, [20]);
|
||||
s2 := CreateSourceSeries('b', TScalar.TKind.Ordinal, [6]);
|
||||
|
||||
m1 := TTestStream.Create(s1);
|
||||
m2 := TTestStream.Create(s2);
|
||||
|
||||
FEnv.RootScope.Define('in1', TDataValue.FromStream(m1), TTypes.CreateRecordSeries(s1.Def));
|
||||
FEnv.RootScope.Define('in2', TDataValue.FromStream(m2), TTypes.CreateRecordSeries(s2.Def));
|
||||
|
||||
// Act
|
||||
script := '(pipe [in1 [:a] in2 [:b]] (fn [valA valB] {:sum (+ valA valB)}))';
|
||||
|
||||
resSeries := FEnv.Run(TAstScript.Parse(script)).AsStream.Series;
|
||||
|
||||
Assert.AreEqual(Int64(0), resSeries.TotalCount);
|
||||
|
||||
// Partial update - no result expected
|
||||
m1.Emit(0);
|
||||
Assert.AreEqual(Int64(0), resSeries.TotalCount);
|
||||
|
||||
// Complete update - result expected
|
||||
m2.Emit(0);
|
||||
Assert.AreEqual(Int64(1), resSeries.TotalCount);
|
||||
|
||||
Assert.AreEqual(Int64(26), resSeries.Fields[key].Items[0].Value.AsInt64);
|
||||
|
||||
// Next cycle
|
||||
s1.Add([10]);
|
||||
m1.Emit(1);
|
||||
Assert.AreEqual(Int64(1), resSeries.TotalCount);
|
||||
|
||||
s2.Add([5]);
|
||||
m2.Emit(1);
|
||||
Assert.AreEqual(Int64(2), resSeries.TotalCount);
|
||||
|
||||
// Check history (Lookback 0 is newest)
|
||||
// Items[0] is the result of Cycle 1 (10 + 5 = 15)
|
||||
Assert.AreEqual(Int64(15), resSeries.Fields[key].Items[0].Value.AsInt64);
|
||||
// Items[1] is the result of Cycle 0 (20 + 6 = 26)
|
||||
Assert.AreEqual(Int64(26), resSeries.Fields[key].Items[1].Value.AsInt64);
|
||||
end;
|
||||
|
||||
end.
|
||||
@@ -0,0 +1,313 @@
|
||||
unit Test.Myc.Ast.Stream.Pipes;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
DUnitX.TestFramework,
|
||||
System.SysUtils,
|
||||
System.Generics.Collections,
|
||||
// Core Units
|
||||
Myc.Data.Scalar,
|
||||
Myc.Data.Keyword,
|
||||
Myc.Data.Value,
|
||||
Myc.Data.Series,
|
||||
Myc.Data.Stream,
|
||||
Myc.Data.Stream.Pipes,
|
||||
// AST & Compiler
|
||||
Myc.Ast,
|
||||
Myc.Ast.Nodes,
|
||||
Myc.Ast.Scope,
|
||||
Myc.Ast.Types,
|
||||
Myc.Ast.Identities,
|
||||
Myc.Ast.Script,
|
||||
Myc.Ast.Compiler.Binder,
|
||||
Myc.Ast.Compiler.TypeChecker,
|
||||
Myc.Ast.Evaluator;
|
||||
|
||||
type
|
||||
// Helper: Captures compiler errors so we can assert on them
|
||||
TCapturingLog = class(TInterfacedObject, ICompilerLog)
|
||||
private
|
||||
FErrors: TList<string>;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure Add(ALevel: TCompilerErrorLevel; const AMessage: string; const ANode: IAstNode = nil);
|
||||
procedure AddError(const AMessage: string; const ANode: IAstNode = nil);
|
||||
procedure AddWarning(const AMessage: string; const ANode: IAstNode = nil);
|
||||
function HasErrors: Boolean;
|
||||
function GetEntryCount: Integer;
|
||||
function GetEntries: TArray<TCompilerError>;
|
||||
property Errors: TList<string> read FErrors;
|
||||
end;
|
||||
|
||||
// Helper: Collects stream output for assertions
|
||||
TDataCollector = class(TInterfacedObject, IStreamObserver)
|
||||
private
|
||||
FStream: IStream;
|
||||
FLastRecord: IScalarRecord;
|
||||
FSignalCount: Integer;
|
||||
public
|
||||
constructor Create(AStream: IStream);
|
||||
procedure OnSignal(const Signal: TStreamSignal);
|
||||
property LastRecord: IScalarRecord read FLastRecord;
|
||||
property SignalCount: Integer read FSignalCount;
|
||||
end;
|
||||
|
||||
[TestFixture]
|
||||
TPipeIntegrationTests = class
|
||||
private
|
||||
FGraph: IGraphExecutor;
|
||||
FLog: TCapturingLog;
|
||||
FRootLayout: IScopeLayout;
|
||||
FRootScope: IExecutionScope;
|
||||
FInputChannel: IInputChannel;
|
||||
|
||||
// Simulates the Host Application setting up the environment ('btc' variable)
|
||||
procedure SetupHostEnvironment;
|
||||
// Compiles and Runs a script source
|
||||
function CompileAndRun(const Source: string): IStream;
|
||||
public
|
||||
[Setup]
|
||||
procedure Setup;
|
||||
[Teardown]
|
||||
procedure Teardown;
|
||||
|
||||
[Test]
|
||||
procedure Test_Simple_Pipe_Throughput;
|
||||
|
||||
[Test]
|
||||
procedure Test_Pipe_Field_Mapping_And_Calculation;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
{ TCapturingLog }
|
||||
|
||||
constructor TCapturingLog.Create;
|
||||
begin
|
||||
FErrors := TList<string>.Create;
|
||||
end;
|
||||
|
||||
destructor TCapturingLog.Destroy;
|
||||
begin
|
||||
FErrors.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TCapturingLog.Add(ALevel: TCompilerErrorLevel; const AMessage: string; const ANode: IAstNode);
|
||||
begin
|
||||
if ALevel = elError then
|
||||
FErrors.Add(AMessage);
|
||||
end;
|
||||
|
||||
procedure TCapturingLog.AddError(const AMessage: string; const ANode: IAstNode);
|
||||
begin
|
||||
Add(elError, AMessage, ANode);
|
||||
end;
|
||||
|
||||
procedure TCapturingLog.AddWarning(const AMessage: string; const ANode: IAstNode);
|
||||
begin
|
||||
// Ignore warnings for tests
|
||||
end;
|
||||
|
||||
function TCapturingLog.GetEntries: TArray<TCompilerError>;
|
||||
begin
|
||||
Result := nil;
|
||||
end;
|
||||
|
||||
function TCapturingLog.GetEntryCount: Integer;
|
||||
begin
|
||||
Result := FErrors.Count;
|
||||
end;
|
||||
|
||||
function TCapturingLog.HasErrors: Boolean;
|
||||
begin
|
||||
Result := FErrors.Count > 0;
|
||||
end;
|
||||
|
||||
{ TDataCollector }
|
||||
|
||||
constructor TDataCollector.Create(AStream: IStream);
|
||||
begin
|
||||
FStream := AStream;
|
||||
FSignalCount := 0;
|
||||
FLastRecord := nil;
|
||||
end;
|
||||
|
||||
procedure TDataCollector.OnSignal(const Signal: TStreamSignal);
|
||||
var
|
||||
scalarVal: TScalar;
|
||||
begin
|
||||
if Signal.Kind = skData then
|
||||
begin
|
||||
Inc(FSignalCount);
|
||||
// Grab the latest item from the series
|
||||
if FStream.Series.Count > 0 then
|
||||
begin
|
||||
scalarVal := FStream.Series.Items[FStream.Series.Count - 1];
|
||||
// FIX: Cast TScalar to TDataValue to access AsScalarRecord
|
||||
FLastRecord := TDataValue(scalarVal).AsScalarRecord;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ TPipeIntegrationTests }
|
||||
|
||||
procedure TPipeIntegrationTests.Setup;
|
||||
begin
|
||||
FGraph := TGraphExecutor.Create;
|
||||
FLog := TCapturingLog.Create;
|
||||
SetupHostEnvironment;
|
||||
end;
|
||||
|
||||
procedure TPipeIntegrationTests.Teardown;
|
||||
begin
|
||||
FGraph := nil; // Interfaces release automatically
|
||||
FLog.Free; // Class implementation needs free (ref counting mixed usage)
|
||||
end;
|
||||
|
||||
procedure TPipeIntegrationTests.SetupHostEnvironment;
|
||||
var
|
||||
btcDef: IScalarRecordDefinition;
|
||||
btcType: IStaticType;
|
||||
typeDesc: IScopeDescriptor;
|
||||
btcSlot: Integer;
|
||||
scopeBuilder: IScopeBuilder;
|
||||
begin
|
||||
// 1. Create the physical Input Channel in the Graph
|
||||
// FIX: Use TKeywordRegistry.Intern for Keywords
|
||||
// FIX: Using TScalarRecordDefinition class (check if renamed to TRecordDef in your codebase)
|
||||
btcDef :=
|
||||
TScalarRecordDefinition.Create(
|
||||
[
|
||||
TScalarRecordField.Create(TKeywordRegistry.Intern('Close'), skInt64),
|
||||
TScalarRecordField.Create(TKeywordRegistry.Intern('Open'), skInt64)
|
||||
]
|
||||
);
|
||||
FInputChannel := FGraph.GetInputChannel('btc', btcDef);
|
||||
|
||||
// 2. Prepare the Compiler Environment (Symbol Table)
|
||||
// FIX: Use Builder to define symbols
|
||||
scopeBuilder := TScope.CreateBuilder(nil);
|
||||
btcSlot := scopeBuilder.Define('btc');
|
||||
FRootLayout := scopeBuilder.Build;
|
||||
|
||||
// 3. Prepare Type Information
|
||||
// Tell the TypeChecker that 'btc' is a RecordSeries with Open/Close fields
|
||||
btcType := TTypes.CreateRecordSeries(btcDef);
|
||||
typeDesc := TScope.CreateDescriptor(FRootLayout, [btcType]);
|
||||
|
||||
// 4. Prepare Runtime Scope
|
||||
// Create scope and inject the actual Stream object into slot 0
|
||||
FRootScope := TScope.CreateScope(nil, typeDesc, nil);
|
||||
FRootScope.DefineBoxed(btcSlot, TDataValue.FromStream(FInputChannel.Stream));
|
||||
end;
|
||||
|
||||
function TPipeIntegrationTests.CompileAndRun(const Source: string): IStream;
|
||||
var
|
||||
rawAst, boundAst, typedAst: IAstNode;
|
||||
evaluator: IEvaluatorVisitor;
|
||||
res: TDataValue;
|
||||
begin
|
||||
// 1. Parse
|
||||
try
|
||||
rawAst := TAstScript.Parse(Source);
|
||||
except
|
||||
on E: Exception do
|
||||
Assert.Fail('Parsing failed: ' + E.Message);
|
||||
end;
|
||||
|
||||
// 2. Bind
|
||||
boundAst := TAstBinder.Bind(FRootLayout, rawAst, FRootLayout, FLog);
|
||||
if FLog.HasErrors then
|
||||
Assert.Fail('Binding failed: ' + FLog.Errors[0]);
|
||||
|
||||
// 3. TypeCheck
|
||||
typedAst := TTypeChecker.CheckTypes(boundAst, FRootLayout, FRootScope, FLog);
|
||||
if FLog.HasErrors then
|
||||
Assert.Fail('TypeCheck failed: ' + FLog.Errors[0]);
|
||||
|
||||
// 4. Evaluate
|
||||
evaluator := TEvaluatorVisitor.Create(FRootScope);
|
||||
res := evaluator.Execute(typedAst);
|
||||
|
||||
Assert.AreEqual(vkStream, res.Kind, 'Evaluation did not return a Stream');
|
||||
Result := res.AsStream;
|
||||
end;
|
||||
|
||||
procedure TPipeIntegrationTests.Test_Simple_Pipe_Throughput;
|
||||
var
|
||||
pipeStream: IStream;
|
||||
observer: TDataCollector;
|
||||
inputData: IKeywordMapping<TScalar>;
|
||||
begin
|
||||
// A simple pipe that just passes 'Close' through as 'Val'
|
||||
var source := '(pipe [btc [:Close]] (fn [c] { :Val c }))';
|
||||
|
||||
pipeStream := CompileAndRun(source);
|
||||
|
||||
// Attach Observer
|
||||
observer := TDataCollector.Create(pipeStream);
|
||||
pipeStream.Subscribe(observer);
|
||||
|
||||
// --- Step 1: Input 100 ---
|
||||
inputData := TKeywordMapping<TScalar>.Create([TPair<IKeyword, TScalar>.Create(TKeywordRegistry.Intern('Close'), 100)]);
|
||||
FInputChannel.Push(inputData);
|
||||
|
||||
FGraph.Step;
|
||||
|
||||
// Assertions
|
||||
Assert.AreEqual(1, observer.SignalCount, 'Should have received 1 signal');
|
||||
Assert.IsNotNull(observer.LastRecord, 'Record should not be nil');
|
||||
Assert.AreEqual(Int64(100), observer.LastRecord.Fields[TKeywordRegistry.Intern('Val')].Value.AsInt64, 'Output Value wrong');
|
||||
|
||||
// --- Step 2: Input 200 ---
|
||||
inputData := TKeywordMapping<TScalar>.Create([TPair<IKeyword, TScalar>.Create(TKeywordRegistry.Intern('Close'), 200)]);
|
||||
FInputChannel.Push(inputData);
|
||||
|
||||
FGraph.Step;
|
||||
|
||||
Assert.AreEqual(2, observer.SignalCount);
|
||||
Assert.AreEqual(Int64(200), observer.LastRecord.Fields[TKeywordRegistry.Intern('Val')].Value.AsInt64);
|
||||
end;
|
||||
|
||||
procedure TPipeIntegrationTests.Test_Pipe_Field_Mapping_And_Calculation;
|
||||
var
|
||||
pipeStream: IStream;
|
||||
observer: TDataCollector;
|
||||
inputData: IKeywordMapping<TScalar>;
|
||||
begin
|
||||
// A pipe that takes Open and Close, calculates the delta, and renames fields
|
||||
// fn params: o -> Open, c -> Close
|
||||
var source := '(pipe [btc [:Open :Close]] (fn [o c] { :Delta (- c o) :Original c }))';
|
||||
|
||||
pipeStream := CompileAndRun(source);
|
||||
observer := TDataCollector.Create(pipeStream);
|
||||
pipeStream.Subscribe(observer);
|
||||
|
||||
// Input: Open=100, Close=110 -> Delta should be 10
|
||||
inputData :=
|
||||
TKeywordMapping<TScalar>.Create(
|
||||
[
|
||||
TPair<IKeyword, TScalar>.Create(TKeywordRegistry.Intern('Open'), 100),
|
||||
TPair<IKeyword, TScalar>.Create(TKeywordRegistry.Intern('Close'), 110)
|
||||
]
|
||||
);
|
||||
FInputChannel.Push(inputData);
|
||||
|
||||
FGraph.Step;
|
||||
|
||||
Assert.AreEqual(1, observer.SignalCount);
|
||||
|
||||
var delta := observer.LastRecord.Fields[TKeywordRegistry.Intern('Delta')].Value.AsInt64;
|
||||
var orig := observer.LastRecord.Fields[TKeywordRegistry.Intern('Original')].Value.AsInt64;
|
||||
|
||||
Assert.AreEqual(Int64(10), delta, 'Delta calculation wrong');
|
||||
Assert.AreEqual(Int64(110), orig, 'Original value passthrough wrong');
|
||||
end;
|
||||
|
||||
initialization
|
||||
TDUnitX.RegisterTestFixture(TPipeIntegrationTests);
|
||||
|
||||
end.
|
||||
+3
-1
@@ -34,7 +34,9 @@ uses
|
||||
Test.Myc.Ast.RTL.DateTime in 'AST\Test.Myc.Ast.RTL.DateTime.pas',
|
||||
Test.Myc.Ast.Compiler.Binder in '..\Src\AST\Test.Myc.Ast.Compiler.Binder.pas',
|
||||
Test.Myc.Ast.RTL.TypeRegistry in 'AST\Test.Myc.Ast.RTL.TypeRegistry.pas',
|
||||
Test.Myc.Ast.NullPropagation in 'AST\Test.Myc.Ast.NullPropagation.pas';
|
||||
Test.Myc.Ast.NullPropagation in 'AST\Test.Myc.Ast.NullPropagation.pas',
|
||||
Test.Myc.Ast.Pipes in 'AST\Test.Myc.Ast.Pipes.pas',
|
||||
Test.Myc.Ast.Integration in 'Test.Myc.Ast.Integration.pas';
|
||||
|
||||
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
|
||||
{$IFNDEF TESTINSIGHT}
|
||||
|
||||
@@ -136,6 +136,8 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
||||
<DCCReference Include="..\Src\AST\Test.Myc.Ast.Compiler.Binder.pas"/>
|
||||
<DCCReference Include="AST\Test.Myc.Ast.RTL.TypeRegistry.pas"/>
|
||||
<DCCReference Include="AST\Test.Myc.Ast.NullPropagation.pas"/>
|
||||
<DCCReference Include="AST\Test.Myc.Ast.Pipes.pas"/>
|
||||
<DCCReference Include="Test.Myc.Ast.Integration.pas"/>
|
||||
<BuildConfiguration Include="Base">
|
||||
<Key>Base</Key>
|
||||
</BuildConfiguration>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
;; EXPECT: "done"
|
||||
;; TYPE: vkText
|
||||
(do
|
||||
(def count-down (fn [n]
|
||||
(if (= n 0)
|
||||
"done"
|
||||
(recur (- n 1)))))
|
||||
(count-down 100000)
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
;; EXPECT: 25
|
||||
;; TYPE: vkScalar
|
||||
(+ 10 15)
|
||||
@@ -0,0 +1,4 @@
|
||||
;; EXPECT: <method>
|
||||
;; TYPE: vkMethod
|
||||
;; SIGNATURE: Method(Unknown): Unknown
|
||||
(fn [n] (do n))
|
||||
@@ -0,0 +1,215 @@
|
||||
unit Test.Myc.Ast.Integration;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.Classes,
|
||||
System.IOUtils,
|
||||
System.TypInfo,
|
||||
System.Generics.Collections,
|
||||
DUnitX.TestFramework,
|
||||
// Core
|
||||
Myc.Data.Value,
|
||||
Myc.Ast.Environment,
|
||||
Myc.Ast.Script,
|
||||
Myc.Ast.Nodes; // For Exception handling
|
||||
|
||||
type
|
||||
[TestFixture]
|
||||
TTestIntegration = class
|
||||
private
|
||||
const
|
||||
TEST_DIR = 'T:\Myc\Test\Scripts';
|
||||
|
||||
procedure RunSingleScript(const FileName: string);
|
||||
function ParseKind(const KindStr: string): TDataValueKind;
|
||||
public
|
||||
[Setup]
|
||||
procedure Setup;
|
||||
|
||||
[Test]
|
||||
[IgnoreMemoryLeaks]
|
||||
procedure RunAllFileBasedTests;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
{ TTestIntegration }
|
||||
|
||||
procedure TTestIntegration.Setup;
|
||||
begin
|
||||
// Ensure the directory exists to avoid immediate crash
|
||||
if not TDirectory.Exists(TEST_DIR) then
|
||||
ForceDirectories(TEST_DIR);
|
||||
end;
|
||||
|
||||
function TTestIntegration.ParseKind(const KindStr: string): TDataValueKind;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
// Uses RTTI/TypInfo to convert string 'vkScalar' to enum TDataValueKind.vkScalar
|
||||
I := GetEnumValue(TypeInfo(TDataValueKind), KindStr);
|
||||
if I < 0 then
|
||||
raise Exception.CreateFmt('Unknown TDataValueKind in test header: %s', [KindStr]);
|
||||
Result := TDataValueKind(I);
|
||||
end;
|
||||
|
||||
procedure TTestIntegration.RunSingleScript(const FileName: string);
|
||||
var
|
||||
Lines: TArray<string>;
|
||||
ScriptSource: TStringBuilder;
|
||||
ExpectedOutput, ExpectedSignature: string; // <-- Neu
|
||||
ExpectedTypeStr: string;
|
||||
ExpectedType: TDataValueKind;
|
||||
HasExpectation: Boolean;
|
||||
Line: string;
|
||||
|
||||
Env: TAstEnvironment;
|
||||
ResultValue: TDataValue;
|
||||
|
||||
// AST Variablen für die Analyse
|
||||
RootNode, BoundNode, SpecializedNode: IAstNode;
|
||||
ActualSignature: string;
|
||||
begin
|
||||
Lines := TFile.ReadAllLines(FileName);
|
||||
ScriptSource := TStringBuilder.Create;
|
||||
try
|
||||
ExpectedOutput := '';
|
||||
ExpectedSignature := ''; // <-- Init
|
||||
ExpectedTypeStr := '';
|
||||
ExpectedType := TDataValueKind.vkVoid;
|
||||
HasExpectation := False;
|
||||
|
||||
// 1. Parsing (angepasst für SIGNATURE)
|
||||
for Line in Lines do
|
||||
begin
|
||||
if Line.Trim.StartsWith(';; EXPECT:') then
|
||||
begin
|
||||
ExpectedOutput := Line.Substring(10).Trim;
|
||||
HasExpectation := True;
|
||||
end
|
||||
else if Line.Trim.StartsWith(';; TYPE:') then
|
||||
begin
|
||||
ExpectedTypeStr := Line.Substring(8).Trim;
|
||||
ExpectedType := ParseKind(ExpectedTypeStr);
|
||||
end
|
||||
else if Line.Trim.StartsWith(';; SIGNATURE:') then // <-- Neu
|
||||
begin
|
||||
ExpectedSignature := Line.Substring(13).Trim;
|
||||
end
|
||||
else
|
||||
begin
|
||||
ScriptSource.AppendLine(Line);
|
||||
end;
|
||||
end;
|
||||
|
||||
if not HasExpectation then
|
||||
Assert.Fail('Test file missing ";; EXPECT:" tag: ' + FileName);
|
||||
|
||||
// 2. Environment Setup
|
||||
Env := TAstEnvironment.Construct(nil);
|
||||
Env.SetStandardMode;
|
||||
|
||||
// 3. Parse AST
|
||||
RootNode := TAstScript.Parse(ScriptSource.ToString);
|
||||
|
||||
// --- SIGNATURE CHECK START ---
|
||||
if ExpectedSignature <> '' then
|
||||
begin
|
||||
try
|
||||
// Wir simulieren die Pipeline bis zum Type-Check
|
||||
// Hinweis: Env.Bind führt intern oft schon Namensauflösung und TypeCheck durch.
|
||||
// Env.Specialize löst generische Aufrufe auf und finalisiert Typen.
|
||||
|
||||
// A. Bind (Namensauflösung & TypeCheck)
|
||||
// Wir übergeben einen Dummy-Log, oder nil wenn erlaubt
|
||||
BoundNode := Env.Bind(RootNode, [], nil);
|
||||
|
||||
// B. Specialize (Optimierung & finale Typisierung)
|
||||
SpecializedNode := Env.Specialize(BoundNode);
|
||||
|
||||
// C. Typ extrahieren
|
||||
if SpecializedNode.IsTyped then
|
||||
begin
|
||||
ActualSignature := SpecializedNode.AsTypedNode.StaticType.ToString;
|
||||
|
||||
Assert
|
||||
.AreEqual(ExpectedSignature, ActualSignature, Format('File: %s - Signature Mismatch', [ExtractFileName(FileName)]));
|
||||
end
|
||||
else
|
||||
begin
|
||||
Assert.Fail(Format('File: %s - Root node is not typed, cannot check signature.', [ExtractFileName(FileName)]));
|
||||
end;
|
||||
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
Assert.Fail(Format('File: %s - Compilation/TypeCheck failed: %s', [ExtractFileName(FileName), E.Message]));
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
// --- SIGNATURE CHECK END ---
|
||||
|
||||
try
|
||||
// 4. Run Execution (Value Check)
|
||||
// Wir nutzen hier wieder RootNode (oder SpecializedNode, falls Env.Run damit umgehen kann),
|
||||
// um sicherzustellen, dass die Ausführung sauber durchläuft.
|
||||
ResultValue := Env.Run(RootNode);
|
||||
|
||||
// 5. Verify Type
|
||||
if ExpectedTypeStr <> '' then
|
||||
begin
|
||||
Assert.AreEqual(ExpectedType, ResultValue.Kind, Format('File: %s - Result Type Mismatch', [ExtractFileName(FileName)]));
|
||||
end;
|
||||
|
||||
// 6. Verify Output Value
|
||||
Assert.AreEqual(ExpectedOutput, ResultValue.ToString, Format('File: %s - Result Value Mismatch', [ExtractFileName(FileName)]));
|
||||
|
||||
except
|
||||
on E: EAssertionFailed do
|
||||
raise;
|
||||
on E: Exception do
|
||||
Assert.Fail(Format('File: %s - Exception during execution: %s', [ExtractFileName(FileName), E.Message]));
|
||||
end;
|
||||
|
||||
finally
|
||||
ScriptSource.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTestIntegration.RunAllFileBasedTests;
|
||||
var
|
||||
Files: TArray<string>;
|
||||
FileName: string;
|
||||
Failures: TStringBuilder;
|
||||
begin
|
||||
Files := TDirectory.GetFiles(TEST_DIR, '*.txt');
|
||||
|
||||
if Length(Files) = 0 then
|
||||
Exit; // Silent exit if no files found (success, effectively)
|
||||
|
||||
Failures := TStringBuilder.Create;
|
||||
try
|
||||
for FileName in Files do
|
||||
begin
|
||||
try
|
||||
RunSingleScript(FileName);
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
Failures.AppendLine(Format('[FAIL] %s: %s', [ExtractFileName(FileName), E.Message]));
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
// Only fail if errors were collected
|
||||
if Failures.Length > 0 then
|
||||
Assert.Fail('One or more integration scripts failed:' + SLineBreak + Failures.ToString);
|
||||
|
||||
finally
|
||||
Failures.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
Reference in New Issue
Block a user