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; 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; property Errors: TList 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.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; 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; 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.Create([TPair.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.Create([TPair.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; 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.Create( [ TPair.Create(TKeywordRegistry.Intern('Open'), 100), TPair.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.