diff --git a/Src/Myc.Core.Futures.pas b/Src/Myc.Core.Futures.pas index 3782a2a..2839612 100644 --- a/Src/Myc.Core.Futures.pas +++ b/Src/Myc.Core.Futures.pas @@ -37,7 +37,7 @@ constructor TMycInitStateFuncFuture.Create( const ATaskManager: IMycTaskManag begin inherited Create; - FDone := State.CreateLatch( 1 ); + FDone := TState.CreateLatch( 1 ); // Subscribe the job execution to AGate. // The job will run when AGate notifies the subscriber returned by Run. diff --git a/Src/Myc.Core.Tasks.pas b/Src/Myc.Core.Tasks.pas index ccb7395..04ce932 100644 --- a/Src/Myc.Core.Tasks.pas +++ b/Src/Myc.Core.Tasks.pas @@ -138,6 +138,8 @@ begin end; for var i := 0 to High(FWorkThreads) do FWorkThreads[i].Start; + + FWaitSemaphores.Push( TSemaphore.Create(nil, 0, 1, '') ); end; destructor TMycTaskFactory.Destroy; @@ -147,6 +149,9 @@ begin for var i := High(FWorkThreads) downto 0 do FWorkThreads[i].Free; + if Assigned(FException) then + FException.Free; + FWorkGate.Free; FWorkStack.Clear; inherited Destroy; diff --git a/Src/Myc.Futures.pas b/Src/Myc.Futures.pas index 0f3a66f..42d38fb 100644 --- a/Src/Myc.Futures.pas +++ b/Src/Myc.Futures.pas @@ -19,7 +19,7 @@ type property Done: IMycState read GetDone; end; - Future = record + TFuture = record strict private FFuture: IMycFuture; function GetDone: IMycState; inline; @@ -30,8 +30,8 @@ type public constructor Create(const AFuture: IMycFuture); - class operator Implicit(const A: IMycFuture): Future; overload; - class operator Implicit(const A: Future): IMycFuture; overload; + class operator Implicit(const A: IMycFuture): TFuture; overload; + class operator Implicit(const A: TFuture): IMycFuture; overload; class function Construct(const Proc: TFunc): IMycFuture; overload; static; class function Construct(const Gate: IMycState; const Proc: TFunc): IMycFuture; overload; static; @@ -49,64 +49,64 @@ implementation uses Myc.Core.Futures, Myc.Core.Tasks; -constructor Future.Create(const AFuture: IMycFuture); +constructor TFuture.Create(const AFuture: IMycFuture); begin FFuture := AFuture; end; -class constructor Future.CreateClass; +class constructor TFuture.CreateClass; begin TMycTaskFactory.AquireTaskManager; end; -class destructor Future.DestroyClass; +class destructor TFuture.DestroyClass; begin TMycTaskFactory.ReleaseTaskManager; end; -function Future.Chain(const Proc: TFunc): IMycFuture; +function TFuture.Chain(const Proc: TFunc): IMycFuture; begin var Cap := FFuture; - Result := Future.Construct( FFuture.Done, + Result := TFuture.Construct( FFuture.Done, function: S begin Result := Proc( Cap.Result ); end ); end; -class function Future.Construct(const Proc: TFunc): IMycFuture; +class function TFuture.Construct(const Proc: TFunc): IMycFuture; begin Result := TMycInitStateFuncFuture.Create( TaskManager, nil, Proc ); end; -class function Future.Construct(const Gate: IMycState; const Proc: TFunc): IMycFuture; +class function TFuture.Construct(const Gate: IMycState; const Proc: TFunc): IMycFuture; begin Result := TMycInitStateFuncFuture.Create( TaskManager, Gate, Proc ); end; -function Future.GetDone: IMycState; +function TFuture.GetDone: IMycState; begin Result := FFuture.Done; end; -function Future.GetResult: T; +function TFuture.GetResult: T; begin Result := FFuture.Result; end; -function Future.WaitFor: T; +function TFuture.WaitFor: T; begin TaskManager.WaitFor( FFuture.Done ); Result := FFuture.Result; end; -class operator Future.Implicit(const A: IMycFuture): Future; +class operator TFuture.Implicit(const A: IMycFuture): TFuture; begin Result.Create( A ); end; -class operator Future.Implicit(const A: Future): IMycFuture; +class operator TFuture.Implicit(const A: TFuture): IMycFuture; begin Result := A.FFuture; end; diff --git a/Src/Myc.Signals.pas b/Src/Myc.Signals.pas index c678548..d2c669f 100644 --- a/Src/Myc.Signals.pas +++ b/Src/Myc.Signals.pas @@ -7,7 +7,7 @@ uses type IMycSubscriber = interface - // Interface for an entity that can be notified by a State or State. + // Interface for an entity that can be notified by a TState or TState. // Returns true if this subscriber expects further notifications from the source, false otherwise. function Notify: Boolean; end; @@ -21,21 +21,21 @@ type function GetState: IMycState; public constructor Create( const AState: IMycState; ATag: Pointer ); - // Unsubscribes from the associated State. + // Unsubscribes from the associated TState. procedure Unsubscribe; class operator Initialize( out Dest: TMycSubscription ); - property State: IMycState read GetState; + property TState: IMycState read GetState; end; IMycState = interface - // Represents a subscribable state that can be queried. + // Represents a subscribable TState that can be queried. {$REGION 'property access'} function GetIsSet: Boolean; {$ENDREGION} - // Subscribes a given subscriber to this State. + // Subscribes a given subscriber to this TState. function Subscribe( Subscriber: IMycSubscriber ): TMycSubscription; procedure Unsubscribe( Tag: Pointer ); - // IsSet is true if the state has been reached or the condition is met. + // IsSet is true if the TState has been reached or the condition is met. property IsSet: Boolean read GetIsSet; end; @@ -48,25 +48,29 @@ type // Inherits IMycSubscriber and State property from IMycFlag. No new members. end; - // IMycDirty represents a resettable flag, typically indicating if a state is "dirty" (requiring attention) or "clean". - // It inherits from IMycFlag, meaning it has a state, can be notified, and subscribed to. + // IMycDirty represents a resettable flag, typically indicating if a State is "dirty" (requiring attention) or "clean". + // It inherits from IMycFlag, meaning it has a State, can be notified, and subscribed to. IMycDirty = interface( IMycSubscriber ) // Provides access to the IMycState interface of the flag. function GetState: IMycState; - // Resets the flag to its "clean" (not set / not dirty) state. + // Resets the flag to its "clean" (not set / not dirty) State. // Returns true if the flag was actually dirty before this reset, false otherwise. function Reset: Boolean; property State: IMycState read GetState; end; - State = record + TState = record private + FState: IMycState; class function GetNull: IMycState; static; public + constructor Create(const AState: IMycState); class function CreateLatch( Count: Integer ): IMycLatch; static; class function CreateDirty: IMycDirty; static; class function All( const States: TArray ): IMycState; static; class function Any( const States: TArray ): IMycState; static; + class operator Implicit(const A: IMycState): TState; overload; + class operator Implicit(const A: TState): IMycState; overload; class property Null: IMycState read GetNull; end; @@ -103,14 +107,21 @@ begin Dest.FTag := nil; end; -{ State } +{ TState } -class function State.CreateLatch( Count: Integer ): IMycLatch; +constructor TState.Create(const AState: IMycState); +begin + FState := AState; + if not Assigned(FState) then + FState := TMycState.Null; +end; + +class function TState.CreateLatch( Count: Integer ): IMycLatch; begin Result := TMycLatch.CreateLatch( Count ); end; -class function State.All( const States: TArray ): IMycState; +class function TState.All( const States: TArray ): IMycState; var Latch: IMycLatch; begin @@ -120,13 +131,13 @@ begin Result := Latch.State; end; -class function State.Any( const States: TArray ): IMycState; +class function TState.Any( const States: TArray ): IMycState; var Latch: IMycLatch; begin if Length( States ) = 0 then begin - Result := State.Null; + Result := TState.Null; end else begin @@ -137,14 +148,24 @@ begin end; end; -class function State.CreateDirty: IMycDirty; +class function TState.CreateDirty: IMycDirty; begin Result := TMycDirty.CreateDirty; end; -class function State.GetNull: IMycState; +class function TState.GetNull: IMycState; begin Result := TMycState.Null; end; +class operator TState.Implicit(const A: TState): IMycState; +begin + Result := A.FState; +end; + +class operator TState.Implicit(const A: IMycState): TState; +begin + Result.Create( A ); +end; + end. diff --git a/Src/Myc.TaskManager.pas b/Src/Myc.TaskManager.pas index 1b59796..c4336b4 100644 --- a/Src/Myc.TaskManager.pas +++ b/Src/Myc.TaskManager.pas @@ -8,6 +8,7 @@ uses type IMycTaskManager = interface + // TOD Dokumentation function CreateTask( const Gate: IMycState; const Proc: TProc ): TMycSubscription; // Waits for the operation associated with State to complete. @@ -21,6 +22,69 @@ type var TaskManager: IMycTaskManager; +procedure SetupTaskManagerMock; + implementation +uses + System.Generics.Collections; + +type + TMycExecMock = class(TInterfacedObject, IMycSubscriber) + private + FProc: TProc; + public + constructor Create(const AProc: TProc); + function Notify: Boolean; + end; + + TMycTaskManagerMock = class(TInterfacedObject, IMycTaskManager) + public + // IMycTaskManager + function CreateTask(const Gate: IMycState; const Proc: TProc): TMycSubscription; + procedure WaitFor(State: IMycState); + + constructor Create; + end; + +{ TMycExecMock } + +constructor TMycExecMock.Create(const AProc: TProc); +begin + inherited Create; + FProc := AProc; +end; + +function TMycExecMock.Notify: Boolean; +begin + if Assigned(FProc) then + begin + FProc(); + FProc := nil; + end; +end; + +{ TMycTaskManagerMock } + +constructor TMycTaskManagerMock.Create; +begin + inherited Create; +end; + +function TMycTaskManagerMock.CreateTask(const Gate: IMycState; const Proc: TProc): TMycSubscription; +begin + var s: IMycState := TState(Gate); + Result := s.Subscribe( TMycExecMock.Create( Proc ) ); +end; + +procedure TMycTaskManagerMock.WaitFor(State: IMycState); +begin + Assert( State.IsSet ); +end; + +procedure SetupTaskManagerMock; +begin + TaskManager := TMycTaskManagerMock.Create; +end; + end. diff --git a/Test/TestCoreFutures.pas b/Test/TestCoreFutures.pas index 68252bc..8906579 100644 --- a/Test/TestCoreFutures.pas +++ b/Test/TestCoreFutures.pas @@ -94,7 +94,7 @@ const CExpectedResult = 42; begin // Use TMycLatch.Null for an already set init state [cite: 71, 81, 206, 216, 324, 334] - LInitStateAsState := State.Null; + LInitStateAsState := TState.Null; LFuture := TMycInitStateFuncFuture.Create(FTaskFactory, LInitStateAsState, function: Integer @@ -120,7 +120,7 @@ var const CExpectedResult = 'ChainCompleted'; begin - LInitLatch := State.CreateLatch(1); // Create an init state that is not yet set [cite: 77, 212, 330] + LInitLatch := TState.CreateLatch(1); // Create an init state that is not yet set [cite: 77, 212, 330] LFuture := TMycInitStateFuncFuture.Create(FTaskFactory, LInitLatch.State, function: string @@ -155,7 +155,7 @@ const begin LExpectedExceptionRaisedByFactory := False; LLocalTaskFactory := TMycTaskFactory.Create; - LInitStateAsState := State.Null; // Immediate execution + LInitStateAsState := TState.Null; // Immediate execution LFuture := TMycInitStateFuncFuture.Create(LLocalTaskFactory, LInitStateAsState, function: Integer @@ -208,7 +208,7 @@ var LFuture: IMycFuture; LInitLatch: IMycLatch; begin - LInitLatch := State.CreateLatch(1); + LInitLatch := TState.CreateLatch(1); LFuture := TMycInitStateFuncFuture.Create(FTaskFactory, LInitLatch.State, function: Integer @@ -250,7 +250,7 @@ begin FProcExecutionCount := 0; // Reset for this test // Gate Latch: MainFuture waits for this latch, which needs 2 notifications. - LGateLatch := State.CreateLatch(2); // [cite: 77, 212, 330] + LGateLatch := TState.CreateLatch(2); // [cite: 77, 212, 330] // Create MainFuture, AInitState is the GateLatch's state. LMainFuture := TMycInitStateFuncFuture.Create(FTaskFactory, LGateLatch.State, @@ -262,7 +262,7 @@ begin // Setup Prerequisite Futures // PrerequisiteFuture1 - LInitStateP1 := State.CreateLatch(1); // Controllable init state for PF1 + LInitStateP1 := TState.CreateLatch(1); // Controllable init state for PF1 LPrerequisiteFuture1 := TMycInitStateFuncFuture.Create(FTaskFactory, LInitStateP1.State, function: Integer begin @@ -273,7 +273,7 @@ begin Subscriptions[1] := LPrerequisiteFuture1.Done.Subscribe(LSub1); // Subscribe to PF1's completion [cite: 56, 188, 306] // PrerequisiteFuture2 - LInitStateP2 := State.CreateLatch(1); // Controllable init state for PF2 + LInitStateP2 := TState.CreateLatch(1); // Controllable init state for PF2 LPrerequisiteFuture2 := TMycInitStateFuncFuture.Create(FTaskFactory, LInitStateP2.State, function: Integer begin @@ -322,7 +322,7 @@ begin LFlagFutureBRan := False; Self.FSharedCounter := 0; // Reset shared counter for this test - LTriggerLatch := State.CreateLatch(1); // Single trigger [cite: 77, 212, 330] + LTriggerLatch := TState.CreateLatch(1); // Single trigger [cite: 77, 212, 330] // Future A LFutureA := TMycInitStateFuncFuture.Create(FTaskFactory, LTriggerLatch.State, diff --git a/Test/TestFutures.pas b/Test/TestFutures.pas new file mode 100644 index 0000000..069fd3a --- /dev/null +++ b/Test/TestFutures.pas @@ -0,0 +1,553 @@ +unit TestFutures; // Or TestFutures as per your previous version + +interface + +uses + DUnitX.TestFramework, + System.SysUtils, + System.SyncObjs, // For TSemaphore, TEvent etc. + System.Classes, // For TThread, TThread.Sleep + Myc.Signals, + Myc.Core.Signals, // For State factory methods like State.CreateLatch, State.Null + Myc.TaskManager, // For the global TaskManager variable + Myc.Core.Tasks, // For TMycTaskFactory + Myc.Futures; // For Future + +type + + [TestFixture] + TTestFuture = class( TObject ) + public + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure TestConstructSimple; + [Test] + procedure TestConstructWithNilGate; + [Test] + procedure TestConstructWithPresetGate; + [Test] + procedure TestConstructWithDelayedGate; + [Test] + procedure TestChainSimple; + [Test] + procedure TestChainWithGate; + [Test] + procedure TestMultipleChains; + [Test] + [TestCase( 'StringFutureTest', 'Test String' )] + [TestCase( 'IntegerFutureTestForParam', '12345' )] + procedure TestConstructSimple_Parametric( const ParamValue: string ); + + [Test] + procedure TestStress_StateAll; + [Test] + procedure TestStress_StateAny; + + [Test] + procedure TestNestedFuture_Construct; // New test for Future> via Construct + [Test] + procedure TestNestedFuture_Chain; // New test for Future> via Chain + end; + +implementation + +{ TTestFuture } + +procedure TTestFuture.Setup; +begin +// SetupTaskManagerMock; +end; + +procedure TTestFuture.TearDown; +// Executed after each test +begin +end; + +[Test] +procedure TTestFuture.TestConstructSimple; +var + fut: TFuture; + resultValue: Integer; +begin + // Test construction with a simple function that returns an Integer + fut := TFuture.Construct( // [cite: 146] + function: Integer + begin + TThread.Sleep( 20 ); // Simulate some background work + Result := 42; + end + ); + + Assert.IsNotNull( fut.Done, 'Future.Done property should not be nil after construction.' ); // Static string [cite: 148] + fut.WaitFor( ); // Wait for the future to complete its execution [cite: 148] + + Assert.IsTrue( fut.Done.IsSet, 'Future.Done.IsSet should be true after completion.' ); // Static string [cite: 148] + resultValue := fut.Result; // Retrieve the result of the future [cite: 148] + Assert.AreEqual( 42, resultValue, 'The result of the future is not the expected value.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestConstructWithNilGate; +var + fut: TFuture; + resultValue: Integer; +begin + // Test construction with a nil gate, which should execute the task immediately + fut := TFuture.Construct( nil, // Explicitly providing a nil gate [cite: 147] + function: Integer + begin + TThread.Sleep( 20 ); // Simulate work + Result := 43; + end + ); + + Assert.IsNotNull( fut.Done, 'Future.Done should not be nil when constructed with a nil gate.' ); // Static string [cite: 148] + fut.WaitFor( ); // [cite: 148] + + Assert.IsTrue( fut.Done.IsSet, 'Future.Done.IsSet should be true for nil gate construct.' ); // Static string [cite: 148] + resultValue := fut.Result; // [cite: 148] + Assert.AreEqual( 43, resultValue, 'Future result is incorrect for nil gate construct.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestConstructWithPresetGate; +var + fut: TFuture; + presetGate: IMycState; + resultValue: Integer; +begin + presetGate := TState.Null; // State.Null is an IMycState that is always set [cite: 68] + Assert.IsTrue( presetGate.IsSet, 'The preset gate (State.Null) should be initially set.' ); // Static string [cite: 55] + + // Construct a future with a gate that is already set + fut := TFuture.Construct( presetGate, // [cite: 147] + function: Integer + begin + Result := 44; // This should execute quickly + end + ); + + Assert.IsNotNull( fut.Done, 'Future.Done should not be nil for preset gate construct.' ); // Static string [cite: 148] + fut.WaitFor( ); // [cite: 148] + + Assert.IsTrue( fut.Done.IsSet, 'Future.Done.IsSet should be true for preset gate construct.' ); // Static string [cite: 148] + resultValue := fut.Result; // [cite: 148] + Assert.AreEqual( 44, resultValue, 'Future result is incorrect for preset gate construct.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestConstructWithDelayedGate; +var + fut: TFuture; + delayedGate: IMycLatch; // IMycLatch implements IMycState [cite: 58] + resultValue: Integer; +begin + delayedGate := TState.CreateLatch( 1 ); // Create a latch that requires one notification to be set [cite: 66] + Assert.IsNotNull( delayedGate, 'The delayed gate (IMycLatch) should not be nil.' ); // Static string + Assert.IsFalse( delayedGate.State.IsSet, 'The delayed gate should not be initially set.' ); // Static string [cite: 59, 55] + + // Construct a future with a gate that is not yet set + fut := TFuture.Construct( delayedGate.State, // Get the IMycState interface from the latch [cite: 147, 59] + function: Integer + begin + Result := 45; + end + ); + + Assert.IsNotNull( fut.Done, 'Future.Done should not be nil for delayed gate construct.' ); // Static string [cite: 148] + + // Verify the future is not yet done as the gate is not set + TThread.Sleep( 50 ); // Allow some time for task scheduling + Assert.IsFalse( fut.Done.IsSet, 'Future.Done.IsSet should be false before the delayed gate is triggered.' ); + // Static string [cite: 148, 55] + + delayedGate.Notify; // Trigger the latch [cite: 46, 94] (IMycSubscriber.Notify) + + fut.WaitFor( ); // Wait for the future to complete now that the gate is set [cite: 148] + + Assert.IsTrue( delayedGate.State.IsSet, 'The delayed gate should be set after Notify.' ); // Static string [cite: 59, 55] + Assert.IsTrue( fut.Done.IsSet, 'Future.Done.IsSet should be true after the delayed gate is triggered.' ); + // Static string [cite: 148, 55] + resultValue := fut.Result; // [cite: 148] + Assert.AreEqual( 45, resultValue, 'Future result is incorrect for delayed gate construct.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestChainSimple; +var + fut1: TFuture; + fut2: TFuture; + resultValue: string; +begin + // Create an initial future + fut1 := TFuture.Construct( // [cite: 146] + function: Integer + begin + TThread.Sleep( 20 ); // Simulate work + Result := 100; + end + ); + + // Chain a second future that depends on the result of the first + fut2 := fut1.Chain( // [cite: 147] + function( Input: Integer ): string // This function receives the result of fut1 + begin + TThread.Sleep( 20 ); // Simulate further work + Result := 'Value: ' + Input.ToString; // Use ToString for converting Integer to String + end + ); + + Assert.IsNotNull( fut2.Done, 'Chained Future.Done should not be nil.' ); // Static string [cite: 148] + fut2.WaitFor( ); // Wait for the chained future to complete [cite: 148] + + Assert.IsTrue( fut2.Done.IsSet, 'Chained Future.Done.IsSet should be true after completion.' ); // Static string [cite: 148, 55] + resultValue := fut2.Result; // [cite: 148] + Assert.AreEqual( 'Value: 100', resultValue, 'Chained Future result is incorrect.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestChainWithGate; +var + fut1: TFuture; + fut2: TFuture; + delayedGate: IMycLatch; + resultValue: string; +begin + delayedGate := TState.CreateLatch( 1 ); // [cite: 66] + Assert.IsFalse( delayedGate.State.IsSet, 'The delayed gate for chain test should not be initially set.' ); + // Static string [cite: 59, 55] + + // First future depends on the delayedGate + fut1 := TFuture.Construct( delayedGate.State, // [cite: 147, 59] + function: Integer + begin + Result := 200; + end + ); + + // Second future is chained to the first + fut2 := fut1.Chain( // [cite: 147] + function( Input: Integer ): string + begin + Result := 'ChainVal: ' + Input.ToString; + end + ); + + Assert.IsNotNull( fut2.Done, 'Chained (with gate) Future.Done should not be nil.' ); // Static string [cite: 148] + + // Verify futures are not done yet + TThread.Sleep( 50 ); + Assert.IsFalse( fut1.Done.IsSet, 'Initial future (gated) should not be done before gate is set.' ); // Static string [cite: 148, 55] + Assert.IsFalse( fut2.Done.IsSet, 'Chained future (gated) should not be done before gate is set.' ); // Static string [cite: 148, 55] + + delayedGate.Notify; // Trigger the gate [cite: 46, 94] + + fut2.WaitFor( ); // Wait for the final chained future to complete [cite: 148] + + Assert.IsTrue( fut1.Done.IsSet, 'Initial future (gated) should be done after gate is set.' ); // Static string [cite: 148, 55] + Assert.IsTrue( fut2.Done.IsSet, 'Chained future (gated) should be done after gate is set.' ); // Static string [cite: 148, 55] + + resultValue := fut2.Result; // [cite: 148] + Assert.AreEqual( 'ChainVal: 200', resultValue, 'Chained future (gated) result is incorrect.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestMultipleChains; +var + futA: TFuture; + futB: TFuture; // Using Real for intermediate type + futC: TFuture; // Final result as string + finalResult: string; +begin + // Initial future A + futA := TFuture.Construct( // [cite: 146] + function: Integer + begin + TThread.Sleep( 10 ); + Result := 10; + end + ); + + // Future B, chained from A + futB := futA.Chain( // [cite: 147] + function( InputA: Integer ): Real + begin + TThread.Sleep( 10 ); + Result := InputA * 2.5; // Calculation: 10 * 2.5 = 25.0 + end + ); + + // Future C, chained from B + futC := futB.Chain( // [cite: 147] + function( InputB: Real ): string + begin + TThread.Sleep( 10 ); + Result := 'Final: ' + FloatToStr( InputB ); // Convert Real to String + end + ); + + Assert.IsNotNull( futC.Done, 'Multi-chained Future.Done should not be nil.' ); // Static string [cite: 148] + futC.WaitFor( ); // Wait for the last future in the chain [cite: 148] + + Assert.IsTrue( futA.Done.IsSet, 'Future A in chain should be done.' ); // Static string [cite: 148, 55] + Assert.IsTrue( futB.Done.IsSet, 'Future B in chain should be done.' ); // Static string [cite: 148, 55] + Assert.IsTrue( futC.Done.IsSet, 'Future C (multi-chained) should be done.' ); // Static string [cite: 148, 55] + + finalResult := futC.Result; // [cite: 148] + // Ensure FloatToStr conversion is consistent for comparison + Assert.AreEqual( 'Final: ' + FloatToStr( 25.0 ), finalResult, 'Multi-chained Future result is incorrect.' ); // Static string +end; + +[Test] +[TestCase( 'StringFutureTest', 'Test String' )] +[TestCase( 'IntegerFutureTestForParam', '12345' )] +procedure TTestFuture.TestConstructSimple_Parametric( const ParamValue: string ); +var + fut: TFuture; + resultValue: string; +begin + // Parametric test for Future + fut := TFuture.Construct( // [cite: 146] + function: string + begin + TThread.Sleep( 10 ); + Result := ParamValue; // Use the parameter in the future's function + end + ); + + Assert.IsNotNull( fut.Done, 'Parametric Future.Done should not be nil.' ); // Static string [cite: 148] + fut.WaitFor( ); // [cite: 148] + + Assert.IsTrue( fut.Done.IsSet, 'Parametric Future.Done.IsSet should be true.' ); // Static string [cite: 148, 55] + resultValue := fut.Result; // [cite: 148] + Assert.AreEqual( ParamValue, resultValue, 'Parametric Future result does not match input parameter.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestStress_StateAll; +const + StressTestFutureCount = 100; // Number of futures for the stress test +var + Futures: TArray>; + doneStates: TArray; + combinedStateAll: IMycState; + masterFuture: TFuture; + i: Integer; +begin + SetLength( Futures, StressTestFutureCount ); + SetLength( doneStates, StressTestFutureCount ); + Randomize; // Initialize random number generator for varied delays + + // Create multiple futures, each with a small random delay + for i := 0 to High( Futures ) do + begin + // Capture loop variable for use in anonymous method + Futures[i] := TFuture.Construct( // [cite: 146] + ( + function( captureIndex: Integer ): TFunc + begin + Result := function: Integer + var + delay: Integer; + begin + delay := 10 + Random( 40 ); // Random delay between 10ms and 49ms + TThread.Sleep( delay ); + Result := captureIndex; // Return the captured index + end; + end )( i ) + ); + doneStates[i] := Futures[i].Done; // [cite: 148] + end; + + combinedStateAll := TState.All( doneStates ); // [cite: 67] + Assert.IsNotNull( combinedStateAll, 'State.All should return a valid IMycState.' ); // Static string + + // Create a master future that waits on the combined State.All state + masterFuture := TFuture.Construct( combinedStateAll, // [cite: 147] + function: Boolean + begin + Result := True; // This function executes when combinedStateAll is set + end + ); + masterFuture.WaitFor( ); // Wait for all futures to complete via the master future [cite: 148] + + Assert.IsTrue( combinedStateAll.IsSet, 'Combined State.All should be set after masterFuture.WaitFor().' ); // Static string [cite: 55] + + // Verify all individual futures are done and their results are correct + for i := 0 to High( Futures ) do + begin + Assert.IsTrue( Futures[i].Done.IsSet, 'An individual future was not set after State.All completed.' ); + // Static string [cite: 148, 55] + if Futures[i].Done.IsSet then // Additional check to safely access Result + begin + Assert.AreEqual( i, Futures[i].Result, 'A future''s result was incorrect in State.All stress test.' ); + // Static string [cite: 148] + end; + end; +end; + +[Test] +procedure TTestFuture.TestStress_StateAny; +const + StressTestFutureCount = 50; // Number of futures for the stress test + QuickFutureIndex = StressTestFutureCount div 3; // Designate one future to be quicker +var + Futures: TArray>; + doneStates: TArray; + combinedStateAny: IMycState; + masterFuture: TFuture; + i: Integer; + isAtLeastOneSet: Boolean; +begin + SetLength( Futures, StressTestFutureCount ); + SetLength( doneStates, StressTestFutureCount ); + Randomize; // Initialize random number generator + + // Create multiple futures, one of which is designed to finish quickly + for i := 0 to High( Futures ) do + begin + // Capture loop variable + Futures[i] := TFuture.Construct( // [cite: 146] + ( + function( captureIndex: Integer ): TFunc + begin + Result := function: Integer + var + delay: Integer; + begin + if captureIndex = QuickFutureIndex then + delay := 5 // Short delay for the 'quick' future + else + delay := 50 + Random( 100 ); // Longer random delay (50-149ms) for others + TThread.Sleep( delay ); + Result := captureIndex; + end; + end )( i ) + ); + doneStates[i] := Futures[i].Done; // [cite: 148] + end; + + combinedStateAny := TState.Any( doneStates ); // [cite: 68] + Assert.IsNotNull( combinedStateAny, 'State.Any should return a valid IMycState.' ); // Static string + + // Create a master future that waits on the combined State.Any state + masterFuture := TFuture.Construct( combinedStateAny, // [cite: 147] + function: Boolean + begin + Result := True; // This function executes when combinedStateAny is set + end + ); + masterFuture.WaitFor( ); // Wait for at least one future to complete [cite: 148] + + Assert.IsTrue( combinedStateAny.IsSet, 'Combined State.Any should be set after masterFuture.WaitFor().' ); // Static string [cite: 55] + + // Verify that at least one of the original futures is now set + isAtLeastOneSet := False; + for i := 0 to High( Futures ) do + begin + if Futures[i].Done.IsSet then // [cite: 148, 55] + begin + isAtLeastOneSet := True; + end; + end; + Assert.IsTrue( isAtLeastOneSet, 'At least one underlying future should be set after State.Any completed.' ); // Static string + + // It's good practice to ensure all futures complete + for i := 0 to High( Futures ) do + begin + if not Futures[i].Done.IsSet then // [cite: 148, 55] + begin + Futures[i].WaitFor( ); // Wait for any remaining futures [cite: 148] + end; + end; +end; + +[Test] +procedure TTestFuture.TestNestedFuture_Construct; +var + outerFuture: TFuture>; + innerFuture: TFuture; + finalResult: Integer; +begin + // Create an outer future that, when resolved, produces another (inner) future. + outerFuture := TFuture < TFuture < Integer >>.Construct( // [cite: 146] + function: TFuture // This lambda returns a Future + begin + TThread.Sleep( 10 ); // Simulate work for the outer future to produce the inner one + Result := TFuture.Construct( // [cite: 146] + function: Integer + begin + TThread.Sleep( 10 ); // Simulate work for the inner future + Result := 123; // The final value + end + ); + end + ); + + Assert.IsNotNull( outerFuture.Done, 'Outer future''s Done state should not be nil.' ); // Static string [cite: 148] + outerFuture.WaitFor( ); // Wait for the outer future to complete and yield the inner future [cite: 148] + Assert.IsTrue( outerFuture.Done.IsSet, 'Outer future should be done after WaitFor.' ); // Static string [cite: 148, 55] + + innerFuture := outerFuture.Result; // Retrieve the inner future [cite: 148] + Assert.IsNotNull( innerFuture.Done, 'Inner future (from outer.Result) should have a non-nil Done state.' ); // Static string [cite: 148] + + innerFuture.WaitFor( ); // Wait for the inner future to complete and yield the final result [cite: 148] + Assert.IsTrue( innerFuture.Done.IsSet, 'Inner future should be done after its WaitFor.' ); // Static string [cite: 148, 55] + + finalResult := innerFuture.Result; // Retrieve the final integer result [cite: 148] + Assert.AreEqual( 123, finalResult, 'Nested future final result from Construct is incorrect.' ); // Static string +end; + +[Test] +procedure TTestFuture.TestNestedFuture_Chain; +var + initialFuture: TFuture; + outerChainedFuture: TFuture>; // Future where S is Future + innerStringFuture: TFuture; + finalResult: string; +begin + // Create an initial future + initialFuture := TFuture.Construct( // [cite: 146] + function: Integer + begin + TThread.Sleep( 10 ); + Result := 77; + end + ); + + // Chain it with a function that itself returns a new Future + outerChainedFuture := initialFuture.Chain < TFuture < string >> ( // [cite: 147] + function( Input: Integer ): TFuture // This lambda returns a Future + begin + TThread.Sleep( 10 ); // Simulate work in the chain function + // Input is the result of initialFuture (77) + Result := TFuture.Construct( // [cite: 146] + function: string + begin + TThread.Sleep( 10 ); // Simulate work for the inner-most future + Result := 'Value: ' + Input.ToString; // Input is captured (77) + end + ); + end + ); + + Assert.IsNotNull( outerChainedFuture.Done, 'Outer chained future''s Done state should not be nil.' ); // Static string [cite: 148] + outerChainedFuture.WaitFor( ); // Wait for initialFuture to complete AND the chain function to execute [cite: 148] + Assert.IsTrue( outerChainedFuture.Done.IsSet, 'Outer chained future should be done after WaitFor.' ); // Static string [cite: 148, 55] + + innerStringFuture := outerChainedFuture.Result; // Get the Future produced by the chain function [cite: 148] + Assert.IsNotNull( innerStringFuture.Done, 'Inner string future (from chain) should have a non-nil Done state.' ); + // Static string [cite: 148] + + innerStringFuture.WaitFor( ); // Wait for the inner Future to complete [cite: 148] + Assert.IsTrue( innerStringFuture.Done.IsSet, 'Inner string future should be done after its WaitFor.' ); // Static string [cite: 148, 55] + + finalResult := innerStringFuture.Result; // Get the final string result [cite: 148] + Assert.AreEqual( 'Value: 77', finalResult, 'Nested future (via Chain) final result is incorrect.' ); // Static string +end; + +end. diff --git a/Test/TestSignals_Dirty.pas b/Test/TestSignals_Dirty.pas index ed62c00..8d4408e 100644 --- a/Test/TestSignals_Dirty.pas +++ b/Test/TestSignals_Dirty.pas @@ -105,7 +105,7 @@ end; function TTestMycDirtyFlag.CreateAndPrepareDirtyFlag(StartDirty: Boolean = True): IMycDirty; begin - Result := State.CreateDirty; // Initially dirty + Result := TState.CreateDirty; // Initially dirty if not StartDirty then Result.Reset; // Reset to make it clean Assert.AreEqual(StartDirty, Result.State.IsSet, 'CreateAndPrepareDirtyFlag initial state incorrect.'); @@ -127,7 +127,7 @@ procedure TTestMycDirtyFlag.TestCreate_InitialStateIsDirty; var dirtyFlag: IMycDirty; begin - dirtyFlag := State.CreateDirty; + dirtyFlag := TState.CreateDirty; Assert.IsTrue(dirtyFlag.State.IsSet, 'Newly created dirty flag should be IsSet (dirty).'); end; diff --git a/Test/TestSignals_Latch.pas b/Test/TestSignals_Latch.pas index 1ab42a1..fcff1e9 100644 --- a/Test/TestSignals_Latch.pas +++ b/Test/TestSignals_Latch.pas @@ -124,7 +124,7 @@ var latch: IMycLatch; begin // TMycLatch.CreateLatch returns TMycLatch.Null if InitialCount <= 0 - latch := State.CreateLatch(InitialCount); + latch := TState.CreateLatch(InitialCount); Assert.AreEqual(ExpectedIsSet, latch.State.IsSet, 'Latch initial IsSet state mismatch for count ' + IntToStr(InitialCount) + '.'); end; @@ -133,7 +133,7 @@ var latch: IMycLatch; returnedValueFromNotify: Boolean; begin - latch := State.CreateLatch(InitialCount); + latch := TState.CreateLatch(InitialCount); returnedValueFromNotify := latch.Notify; // This is IMycLatch (as IMycSubscriber).Notify Assert.AreEqual(ExpectedReturn, returnedValueFromNotify, 'Unexpected return value from Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.'); @@ -144,7 +144,7 @@ procedure TTestMycLatch.TestNotify_DecrementsCounter_StateChanges; var latch: IMycLatch; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); Assert.IsFalse(latch.State.IsSet, 'Latch should initially not be set (Count=1).'); latch.Notify; // Count becomes 0 Assert.IsTrue(latch.State.IsSet, 'Latch should be set after Notify (Count became 0).'); @@ -154,7 +154,7 @@ procedure TTestMycLatch.TestNotify_MultipleNotifies_CounterBecomesNegativeAndSta var latch: IMycLatch; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); latch.Notify; // Count becomes 0, IsSet = True Assert.IsTrue(latch.State.IsSet, 'Latch should be set after first Notify.'); latch.Notify; // Count becomes -1, IsSet should remain True @@ -169,7 +169,7 @@ var mockSub: TMockSubscriber; subscription: TMycSubscription; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSub := TMockSubscriber.Create; subscription := latch.State.Subscribe(mockSub); // Subscribing to the Latch's state @@ -188,7 +188,7 @@ var mockSub1, mockSub2, mockSub3: TMockSubscriber; sub1, sub2, sub3: TMycSubscription; // Keep subscriptions in scope begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSub1 := TMockSubscriber.Create; mockSub2 := TMockSubscriber.Create; mockSub3 := TMockSubscriber.Create; @@ -210,7 +210,7 @@ var mockSub: TMockSubscriber; subscription: TMycSubscription; begin - latch := State.CreateLatch(2); // Count = 2 + latch := TState.CreateLatch(2); // Count = 2 mockSub := TMockSubscriber.Create; subscription := latch.State.Subscribe(mockSub); @@ -225,7 +225,7 @@ var mockSub: TMockSubscriber; subscription: TMycSubscription; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSub := TMockSubscriber.Create; subscription := latch.State.Subscribe(mockSub); @@ -242,7 +242,7 @@ var mockSub1, mockSub2: TMockSubscriber; subscription1, subscription2: TMycSubscription; begin - latch := State.CreateLatch(1); // Create with count 1 + latch := TState.CreateLatch(1); // Create with count 1 mockSub1 := TMockSubscriber.Create; subscription1 := latch.State.Subscribe(mockSub1); // Subscribe before set @@ -270,8 +270,8 @@ var mockSubForB: TMockSubscriber; subHandle_B_listens_A, subHandle_Mock_listens_B: TMycSubscription; begin - latchA := State.CreateLatch(1); - latchB := State.CreateLatch(1); // latchB is an IMycSubscriber + latchA := TState.CreateLatch(1); + latchB := TState.CreateLatch(1); // latchB is an IMycSubscriber mockSubForB := TMockSubscriber.Create; subHandle_Mock_listens_B := latchB.State.Subscribe(mockSubForB); @@ -295,9 +295,9 @@ var mockSubForC: TMockSubscriber; sub_Mock_C, sub_C_B, sub_B_A: TMycSubscription; begin - latchA := State.CreateLatch(1); - latchB := State.CreateLatch(1); - latchC := State.CreateLatch(1); + latchA := TState.CreateLatch(1); + latchB := TState.CreateLatch(1); + latchC := TState.CreateLatch(1); mockSubForC := TMockSubscriber.Create; sub_Mock_C := latchC.State.Subscribe(mockSubForC); @@ -320,8 +320,8 @@ var begin // LatchA needs 2 notifies to become set. LatchB needs 1 notify to become set. // LatchB subscribes to LatchA. So LatchB will be notified once LatchA becomes set. - latchA := State.CreateLatch(2); - latchB := State.CreateLatch(1); + latchA := TState.CreateLatch(2); + latchB := TState.CreateLatch(1); mockSubForB := TMockSubscriber.Create; sub_Mock_B := latchB.State.Subscribe(mockSubForB); @@ -347,7 +347,7 @@ var mockSub: TMockSubscriber; subscription: TMycSubscription; begin - latch := State.CreateLatch(0); // Returns TMycLatch.Null which is set. + latch := TState.CreateLatch(0); // Returns TMycLatch.Null which is set. mockSub := TMockSubscriber.Create; subscription := latch.State.Subscribe(mockSub); // TMycLatch.Subscribe notifies immediately if already set. @@ -362,7 +362,7 @@ var mockSub: TMockSubscriber; subscription: TMycSubscription; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSub := TMockSubscriber.Create; subscription := latch.State.Subscribe(mockSub); @@ -381,7 +381,7 @@ var mockSub: TMockSubscriber; subscription: TMycSubscription; begin - latch := State.CreateLatch(3); + latch := TState.CreateLatch(3); latch.Notify; // Count -> 2 latch.Notify; // Count -> 1 @@ -404,7 +404,7 @@ var mockSub1, mockSub2: TMockSubscriber; subscription1, subscription2: TMycSubscription; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSub1 := TMockSubscriber.Create; mockSub2 := TMockSubscriber.Create; @@ -432,7 +432,7 @@ var mockSub: TMockSubscriber; // 'subscription' will be declared in an inner scope to control its finalization begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSub := TMockSubscriber.Create; var noException: Boolean := True; @@ -468,7 +468,7 @@ var mockSubInitial, mockSubNew: TMockSubscriber; subscriptionInitial, subscriptionNew: TMycSubscription; begin - latch := State.CreateLatch(1); + latch := TState.CreateLatch(1); mockSubInitial := TMockSubscriber.Create; subscriptionInitial := latch.State.Subscribe(mockSubInitial);