diff --git a/Src/Myc.Core.Futures.pas b/Src/Myc.Core.Futures.pas index a9fbcca..42e699a 100644 --- a/Src/Myc.Core.Futures.pas +++ b/Src/Myc.Core.Futures.pas @@ -79,7 +79,7 @@ end; destructor TMycGateFuncFuture.Destroy; begin - Assert(FDone.State.IsSet); + Assert(FDone.State.IsSet, 'Future not done'); FInit.Unsubscribe; inherited Destroy; diff --git a/Src/Myc.Core.Signals.pas b/Src/Myc.Core.Signals.pas index 241fdf6..2ce13ea 100644 --- a/Src/Myc.Core.Signals.pas +++ b/Src/Myc.Core.Signals.pas @@ -371,10 +371,7 @@ begin begin FSubscribers.Notify(function(Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end); end; - // The return value of this Notify (as an TSignal.ISubscriber) indicates if this - // TMycFlag instance itself would want more notifications if it were subscribed to something. - // Here, it reflects whether a state change to dirty occurred due to this call. - Result := wasPreviouslyClean; + Result := true; finally FSubscribers.Release; end; diff --git a/Src/Myc.Lazy.pas b/Src/Myc.Lazy.pas index 61a0105..e3befc1 100644 --- a/Src/Myc.Lazy.pas +++ b/Src/Myc.Lazy.pas @@ -31,6 +31,7 @@ type private FMutable: IMutable; function GetChanged: TSignal; inline; + function GetValue: T; inline; {$ENDREGION} public constructor Create(const AMutable: IMutable); @@ -44,6 +45,7 @@ type class function CreateWriteable(const Init: T): IWriteable; overload; static; + property Value: T read GetValue; property Changed: TSignal read GetChanged; end; @@ -74,6 +76,7 @@ type class operator Implicit(const A: TLazy): ILazy; overload; class function Construct(const Changing: TSignal.ISignal; const Proc: TFunc): TLazy; overload; static; + class function Construct(const Mutable: TMutable): TLazy; overload; static; class property Null: ILazy read FNull; function Pop(out Res: T): Boolean; inline; @@ -115,6 +118,11 @@ begin Result := FMutable.Changed; end; +function TMutable.GetValue: T; +begin + Result := FMutable.Value; +end; + class operator TMutable.Implicit(const A: TMutable): IMutable; begin Result := A.FMutable; @@ -149,6 +157,12 @@ begin FNull := TMycNullLazy.Create; end; +class function TLazy.Construct(const Mutable: TMutable): TLazy; +begin + var cap := Mutable; + Result := TMycFuncLazy.Create(cap.Changed, function: T begin exit(Mutable.Value) end); +end; + function TLazy.GetChanged: TState; begin Result := FLazy.Changed; diff --git a/Src/Myc.Test.Core.Lazy.pas b/Src/Myc.Test.Core.Lazy.pas index aa8c600..2a6a1dd 100644 --- a/Src/Myc.Test.Core.Lazy.pas +++ b/Src/Myc.Test.Core.Lazy.pas @@ -14,7 +14,6 @@ type TTestMycCoreLazy = class(TObject) private procedure Helper_ConsumeInitialPop(const ALazy: TLazy.ILazy; InitialExpectedValue: Integer); overload; - procedure Helper_ConsumeInitialPop(const ALazy: TLazy.ILazy; const InitialExpectedValue: string); overload; public [Setup] procedure Setup; @@ -77,18 +76,6 @@ begin Assert.IsFalse(ALazy.GetChanged.IsSet, 'Changed.IsSet should be false after consuming initial pop'); end; -procedure TTestMycCoreLazy.Helper_ConsumeInitialPop(const ALazy: TLazy.ILazy; const InitialExpectedValue: string); -var - tempValue: string; - 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; diff --git a/Src/Myc.Test.Signals.Dirty.pas b/Src/Myc.Test.Signals.Dirty.pas index 90df183..ef0f0d3 100644 --- a/Src/Myc.Test.Signals.Dirty.pas +++ b/Src/Myc.Test.Signals.Dirty.pas @@ -167,7 +167,7 @@ begin notifyResult := dirtyFlag.Notify; // Call TSignal.ISubscriber.Notify to make it dirty Assert.IsTrue(dirtyFlag.State.IsSet, 'Flag should be dirty (IsSet) after Notify on clean flag.'); - Assert.IsTrue(notifyResult, 'Notify() should return True indicating a state change from clean to dirty.'); + Assert.IsTrue(notifyResult); end; procedure TTestMycDirtyFlag.TestNotify_OnDirtyFlag_StaysDirtyAndReturnsFalse; @@ -180,7 +180,7 @@ begin notifyResult := dirtyFlag.Notify; // Call TSignal.ISubscriber.Notify again Assert.IsTrue(dirtyFlag.State.IsSet, 'Flag should remain dirty (IsSet).'); - Assert.IsFalse(notifyResult, 'Notify() should return False as flag was already dirty (no state change to dirty).'); + Assert.IsTrue(notifyResult); end; // Category 2: Subscriber Notifications diff --git a/Src/Myc.Test.Trade.DataServer.pas b/Src/Myc.Test.Trade.DataServer.pas new file mode 100644 index 0000000..ea0bdb3 --- /dev/null +++ b/Src/Myc.Test.Trade.DataServer.pas @@ -0,0 +1,120 @@ +unit Myc.Test.Trade.DataServer; + +interface + +uses + System.SysUtils, + System.Generics.Collections, // E2003: Added for TList + DUnitX.TestFramework, + Myc.Signals, + Myc.Lazy, + Myc.Futures, + Myc.Trade.DataPoint, + Myc.Trade.DataSeries, + Myc.Trade.DataServer; + +type + // Define ITickData as an alias for the specific IDataPoint interface. + ITickData = IDataPoint; + + [TestFixture] + TTest_TABFileServer_Equivalence = class(TObject) + private + FServer: IDataServer; + FExpectedData: TArray; + public + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure Test_ServerReturnsSameDataAs_LoadDataSeries; + end; + +implementation + +uses + Myc.TaskManager; + +const + // The reference data file for this test. + C_TEST_FILENAME = '\\COFFEE\TickData\Pepperstone\AUDNZD_2024_05.tab_zip'; + // The chunk size for fetching data from the server. + C_MAX_FETCH = 20; + +{ TTest_TABFileServer_Equivalence } + +procedure TTest_TABFileServer_Equivalence.Setup; +begin + // SetupTaskManagerMock; + + // IsMemoryLow := function: Boolean begin exit(false); end; + + // 1. Load the ground truth data using the original LoadDataSeries function. + FExpectedData := TAskBid.LoadDataSeries(C_TEST_FILENAME).WaitFor; + + // 2. Set up the filename and the TABFileServer instance. + FServer := TTABFileServer.Create(C_TEST_FILENAME, C_MAX_FETCH); +end; + +procedure TTest_TABFileServer_Equivalence.TearDown; +begin + FServer := nil; + SetLength(FExpectedData, 0); + IsMemoryLow := nil; +end; + +procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries; +var + actualDataList: TList; + chunk: TArray; + newChunk: TLazy>; +begin + actualDataList := TList.Create; + try + newChunk := TLazy>.Construct(FServer.Data); + + // Loop until the server stops providing new data chunks. + var cnt: Int64 := 0; + while not FServer.IsLiveData.Value do + begin + FServer.Fetch; + + // Get the data chunk produced by the last Fetch call. + if newChunk.Pop(chunk) then + begin + for var i := 0 to High(chunk) do + begin + actualDataList.Add(chunk[i]); + Assert.AreEqual(chunk[i].Idx, cnt, 'Index mismatch'); + inc(cnt); + end; + end; + end; + + // --- Assertions --- + + // 1. Verify that the total number of records matches. + Assert.AreEqual(Length(FExpectedData), actualDataList.Count, 'Data record count mismatch'); + + // 2. Verify that the content of each record is identical. + for var i := 0 to High(FExpectedData) do + begin + var expectedTick := FExpectedData[i]; + var actualTick := actualDataList[i]; + + // E2003: Removed assertion for 'Index', as it's not part of the IDataPoint interface. + // The index is an implementation detail of the faulty ConvertTicks and cannot be tested here. + Assert.AreEqual(expectedTick.OADateTime, actualTick.Time, 'Timestamp mismatch '); + Assert.IsTrue(Abs(expectedTick.Data.Ask - actualTick.Data.Ask) < 0.0001, 'Ask price mismatch'); + Assert.IsTrue(Abs(expectedTick.Data.Bid - actualTick.Data.Bid) < 0.0001, 'Bid price mismatch'); + end; + finally + actualDataList.Free; + end; +end; + +initialization + TDUnitX.RegisterTestFixture(TTest_TABFileServer_Equivalence); +end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 356413a..e6c5483 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -9,12 +9,14 @@ type IDataPoint = interface ['{6A52697A-2868-42A9-982D-993542B7B360}'] function GetData: T; + function GetIdx: Int64; function GetTime: TDateTime; function GetVolume: Double; property Time: TDateTime read GetTime; property Volume: Double read GetVolume; property Data: T read GetData; + property Idx: Int64 read GetIdx; end; ITick = interface @@ -25,6 +27,7 @@ type property Ask: Double read GetAsk; property Bid: Double read GetBid; end; + ITickData = IDataPoint; IOHLC = interface ['{05374E3C-731B-44FA-A23E-051F1461B78D}'] @@ -38,6 +41,7 @@ type property Low: Double read GetLow; property Close: Double read GetClose; end; + IOHLCData = IDataPoint; // Abstract base class for all data points. TDataPoint = class abstract(TInterfacedObject, IDataPoint) @@ -45,11 +49,13 @@ type fTime: TDateTime; fVolume: Double; FData: T; + FIdx: Int64; function GetTime: TDateTime; function GetVolume: Double; function GetData: T; + function GetIdx: Int64; public - constructor Create(Time: TDateTime; Volume: Double; const AData: T); + constructor Create(Idx: Int64; Time: TDateTime; Volume: Double; const AData: T); end; TTick = class(TInterfacedObject, ITick) @@ -80,8 +86,10 @@ implementation { TDataPoint } -constructor TDataPoint.Create(Time: TDateTime; Volume: Double; const AData: T); +constructor TDataPoint.Create(Idx: Int64; Time: TDateTime; Volume: Double; const AData: T); begin + inherited Create; + FIdx := Idx; fTime := Time; fVolume := Volume; FData := AData; @@ -92,6 +100,11 @@ begin Result := FData; end; +function TDataPoint.GetIdx: Int64; +begin + Result := FIdx; +end; + function TDataPoint.GetTime: TDateTime; begin result := fTime; diff --git a/Src/Myc.Trade.DataSeries.pas b/Src/Myc.Trade.DataSeries.pas index b64d5c9..1ab9aef 100644 --- a/Src/Myc.Trade.DataSeries.pas +++ b/Src/Myc.Trade.DataSeries.pas @@ -54,7 +54,7 @@ type type // Represents a single data point with a timestamp and generic data. TDataPoint = packed record - OADateTime: Double; + OADateTime: TDateTime; Data: T; end; @@ -77,7 +77,7 @@ type class function ReadUncompressedData(const InputStream: TStream): TArray; static; public // Asynchronously loads a single data file with caching support. - class function LoadDataFile(var LoadGate: TLatch; const FileName: string): TFuture>; static; + class function LoadDataFile(const LoadGate: TState; const FileName: string): TFuture>; static; // Asynchronously loads a complete chronological series of data files. class function LoadDataSeries(const FileName: string): TFuture>; static; end; @@ -274,44 +274,44 @@ begin end; function FindNextDataFile(const FileName: string): string; -var - pathValue, symbolValue: string; - yearValue, monthValue: Integer; - nextBaseName, compressedPath, uncompressedPath: string; -begin - // Parse the input filename to extract its components. - if not TryParseFileName(FileName, pathValue, symbolValue, yearValue, monthValue) then - begin - exit(''); // If parsing fails, there is no 'next' file. - end; - - // Calculate the next month and year. - Inc(monthValue); - if monthValue > 12 then - begin - monthValue := 1; - Inc(yearValue); - end; - - // Construct the base name for the next file. - nextBaseName := Format('%s_%.4d_%.2d.tab', [symbolValue, yearValue, monthValue]); - - // Check for the existence of the next file, preferring the compressed version. - compressedPath := TPath.Combine(pathValue, nextBaseName + '_zip'); - if TFile.Exists(compressedPath) then - begin - exit(compressedPath); - end; - - uncompressedPath := TPath.Combine(pathValue, nextBaseName); - if TFile.Exists(uncompressedPath) then - begin - exit(uncompressedPath); - end; - - // If no next file is found, return an empty string. - Result := ''; -end; +var + pathValue, symbolValue: string; + yearValue, monthValue: Integer; + nextBaseName, compressedPath, uncompressedPath: string; +begin + // Parse the input filename to extract its components. + if not TryParseFileName(FileName, pathValue, symbolValue, yearValue, monthValue) then + begin + exit(''); // If parsing fails, there is no 'next' file. + end; + + // Calculate the next month and year. + Inc(monthValue); + if monthValue > 12 then + begin + monthValue := 1; + Inc(yearValue); + end; + + // Construct the base name for the next file. + nextBaseName := Format('%s_%.4d_%.2d.tab', [symbolValue, yearValue, monthValue]); + + // Check for the existence of the next file, preferring the compressed version. + compressedPath := TPath.Combine(pathValue, nextBaseName + '_zip'); + if TFile.Exists(compressedPath) then + begin + exit(compressedPath); + end; + + uncompressedPath := TPath.Combine(pathValue, nextBaseName); + if TFile.Exists(uncompressedPath) then + begin + exit(uncompressedPath); + end; + + // If no next file is found, return an empty string. + Result := ''; +end; class destructor TDataSeries.CreateClass; begin @@ -325,7 +325,7 @@ end; // Asynchronously reads tick data from a single specified file. // Returns a TFuture that will provide an array of TAskBidSeries. -class function TDataSeries.LoadDataFile(var LoadGate: TLatch; const FileName: string): TFuture>; +class function TDataSeries.LoadDataFile(const LoadGate: TState; const FileName: string): TFuture>; var parsedPath, parsedSymbol: string; parsedYear, parsedMonth: Integer; @@ -379,7 +379,7 @@ begin Result := TFuture .Construct( - TLatch.Enqueue(LoadGate), + LoadGate, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); // Read all bytes of the .zip file @@ -401,7 +401,7 @@ begin var capFileName := FileName; Result := TFuture>.Construct( - TLatch.Enqueue(LoadGate), + LoadGate, function: TArray begin if TFile.Exists(capFileName) then @@ -447,6 +447,7 @@ var liveFilePath: string; pathName, symbolName: string; yearValue, monthValue: Integer; + LoadGate: TLatch; begin // 1. Discover all sequential .tab/.tab_zip files using the new helper function. tabFiles := TList.Create; @@ -482,12 +483,10 @@ begin var loadedFileList := TList>>.Create; var loadStates := TList.Create; try - var LoadGate: TLatch; - // Create load tasks for the historical .tab files. for currentFile in tabFiles do begin - var data := LoadDataFile(LoadGate, currentFile); + var data := LoadDataFile(TLatch.Enqueue(LoadGate), currentFile); loadedFileList.Add(data); loadStates.Add(data.Done); end; @@ -496,7 +495,7 @@ begin liveData := TFuture>.Null; if liveFilePath <> '' then begin - liveData := LoadDataFile(LoadGate, liveFilePath); + liveData := LoadDataFile(TLatch.Enqueue(LoadGate), liveFilePath); loadedFileList.Add(liveData); loadStates.Add(liveData.Done); end; @@ -520,7 +519,7 @@ begin var tickList := TList.Create; try var totalTicks := Length(liveData.Value); - var overallLastTabTickTime := 0.0; + var overallLastTabTickTime: TDateTime := 0; for var P in loadedFiles do Inc(totalTicks, Length(P.Value)); @@ -529,15 +528,12 @@ begin for var P in loadedFiles do begin - if Length(P.Value) > 0 then - begin - for var iterTickData in P.Value do - if iterTickData.OADateTime > overallLastTabTickTime then - begin - tickList.Add(iterTickData); - overallLastTabTickTime := iterTickData.OADateTime; - end; - end; + for var iterTickData in P.Value do + if overallLastTabTickTime < iterTickData.OADateTime then + begin + tickList.Add(iterTickData); + overallLastTabTickTime := iterTickData.OADateTime; + end; end; Result := tickList.ToArray; finally diff --git a/Src/Myc.Trade.DataServer.pas b/Src/Myc.Trade.DataServer.pas index ea77da6..ea7b420 100644 --- a/Src/Myc.Trade.DataServer.pas +++ b/Src/Myc.Trade.DataServer.pas @@ -7,7 +7,9 @@ uses System.Generics.Collections, Myc.Lazy, Myc.Trade.DataPoint, - Myc.Trade.DataSeries; + Myc.Trade.DataSeries, + Myc.Signals, // Added for TLatch + Myc.Futures; // Added for TFuture type IDataProvider = interface @@ -15,7 +17,7 @@ type procedure Fetch; end; - IDataServer = interface + IDataServer = interface(IDataProvider) ['{A6E246A2-E84E-49AB-A63E-333E561E488C}'] function GetData: TMutable>; function GetIsLiveData: TMutable; @@ -41,10 +43,154 @@ type function CreateDataServer: IDataServer; virtual; abstract; end; - TFileStreamServerNode = class(TDataServerNode) + TTABFileServer = class(TDataServer) + private + FFilename: String; + // The maximum number of data points to be produced per Fetch call. + FMaxFetch: Integer; + FHasNewData: TEvent; + FNeedNewChunk: Boolean; + FIsLiveData: TMutable.IWriteable; + // Internal state + FCurrentFileName: string; + FCurrentData: TFuture>; + FNextFileName: string; + FNextData: TFuture>; + FData: TMutable>; + FCurrPosInFile: Int64; + FCurrChunk: TArray; + FLoadGate: TLatch; + FCurrentIdx: Int64; + FLastTimeStamp: TDateTime; + + function ConvertTicks(var Idx: Int64; const Ticks: TArray): TArray; + function UpdateChunk: TArray; + protected + procedure Fetch; override; + function GetData: TMutable>; override; + function GetIsLiveData: TMutable; override; + public + constructor Create(const AFilename: String; AMaxFetch: Integer); end; implementation +uses + System.IOUtils; + +{ TTABFileServer } + +constructor TTABFileServer.Create(const AFilename: String; AMaxFetch: Integer); +begin + inherited Create; + FFilename := AFilename; + FMaxFetch := AMaxFetch; + + FNextData := TFuture>.Null; + FNeedNewChunk := true; + FIsLiveData := TMutable.CreateWriteable(false); + + FHasNewData := TEvent.CreateEvent; + FData := TMutable>.Construct(FHasNewData.Signal, function: TArray begin Result := UpdateChunk; end); +end; + +function TTABFileServer.ConvertTicks(var Idx: Int64; const Ticks: TArray): TArray; +begin + SetLength(Result, Length(Ticks)); + for var i := 0 to High(Ticks) do + begin + Result[i] := TDataPoint.Create(Idx, Ticks[i].OADateTime, 1, TTick.Create(Ticks[i].Data.Ask, Ticks[i].Data.Bid)); + inc(Idx); + end; +end; + +procedure TTABFileServer.Fetch; +begin + + // Check if the source filename has changed. + if (FFilename <> FCurrentFileName) and (FFilename <> '') then + begin + // Filename has changed, initialize by loading the new file. + FCurrentFileName := FFilename; + FCurrentData := TAskBid.LoadDataFile(TLatch.Enqueue(FLoadGate), FCurrentFileName); + FCurrPosInFile := 0; + FCurrentIdx := 0; + FIsLiveData.SetValue(false); + FLastTimeStamp := 0; + FFilename := ''; + + FNextFileName := FindNextDataFile(FCurrentFileName); + if FNextFileName <> '' then + FNextData := TAskBid.LoadDataFile(TLatch.Enqueue(FLoadGate), FNextFileName); + end; + + FHasNewData.Notify; + FNeedNewChunk := true; + FCurrChunk := nil; +end; + +function TTABFileServer.GetData: TMutable>; +begin + Result := FData; +end; + +function TTABFileServer.GetIsLiveData: TMutable; +begin + Result := FIsLiveData; +end; + +function TTABFileServer.UpdateChunk: TArray; +begin + if not FNeedNewChunk then + exit(FCurrChunk); + FNeedNewChunk := false; + + if FCurrentData.Done.IsSet then + begin + SetLength(FCurrChunk, FMaxFetch); + var n := 0; + while n < FMaxFetch do + begin + if FCurrPosInFile >= Length(FCurrentData.Value) then + begin + // Next File! + FCurrentData := FNextData; + FCurrentFileName := FNextFileName; + FCurrPosInFile := 0; + + FNextFileName := ''; + FNextData := TFuture>.Null; + + if FCurrentFileName <> '' then + begin + FNextFileName := FindNextDataFile(FCurrentFileName); + if FNextFileName <> '' then + FNextData := TAskBid.LoadDataFile(TLatch.Enqueue(FLoadGate), FNextFileName); + end + else + begin + // We're done with this series + FIsLiveData.SetValue(true); + end; + + SetLength(FCurrChunk, n); + break; + end; + + var currData := FCurrentData.Value[FCurrPosInFile]; + if FLastTimeStamp < currData.OADateTime then + begin + FLastTimeStamp := currData.OADateTime; + FCurrChunk[n] := + TDataPoint.Create(FCurrentIdx, FLastTimeStamp, 1, TTick.Create(currData.Data.Ask, currData.Data.Bid)); + inc(FCurrentIdx); + inc(n); + end; + inc(FCurrPosInFile); + end; + end; + Result := FCurrChunk; +end; + end. diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index a5ee521..394da68 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -35,7 +35,8 @@ uses Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas', Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas', Myc.Trade.DataServer in '..\Src\Myc.Trade.DataServer.pas', - Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas'; + Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas', + Myc.Test.Trade.DataServer in '..\Src\Myc.Test.Trade.DataServer.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT} diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index d0da37a..e5462be 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -4,7 +4,7 @@ 20.3 None True - Debug + Release Win64 MycTests 3 @@ -58,7 +58,7 @@ true $(BDS)\bin\delphi_PROJECTICON.ico $(BDS)\bin\delphi_PROJECTICNS.icns - $(DUnitX);..\Src;$(DCC_UnitSearchPath) + $(DUnitX);T:\Myc\Src;$(DCC_UnitSearchPath) MycTests 1031 CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= @@ -93,6 +93,9 @@ $(PreBuildEvent)]]> false + 1033 + (None) + none 1033 @@ -133,6 +136,7 @@ $(PreBuildEvent)]]> + Base @@ -153,7 +157,12 @@ $(PreBuildEvent)]]> MycTests.dpr - + + Embarcadero C++Builder Office 2000 Servers Package + Embarcadero C++Builder Office XP Servers Package + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components +