unit Myc.Test.Lazy; interface uses System.SysUtils, DUnitX.TestFramework, Myc.Signals, // For IMycState, TState, IMycDirty Myc.Lazy; // The unit under test type [TestFixture] TTestMyLazy = class(TObject) private FChangingSignal: TFlag.IFlag; // Used as the 'Changing' state for functional lazy objects // Helper to consume the initial pop, which is always expected to succeed // for a TLazy wrapping a functional lazy object due to "Changed.IsSet initially true" design. procedure ConsumeInitialPop(var ALazyRec: TLazy; ExpectedInitialValue: Integer; const MsgPrefix: string); public [Setup] procedure Setup; [TearDown] procedure TearDown; // Tests for TLazy.Create(nil) - Null Object Pattern [Test] procedure TestCreateWithNil_Changed_IsAlwaysTrue; [Test] procedure TestCreateWithNil_Pop_ReturnsTrueAndDefaultInteger; [Test] procedure TestCreateWithNil_Pop_ReturnsTrueAndDefaultString; // Tests for TLazy.Construct (creates a functional lazy object) [Test] procedure TestConstruct_InitialChanged_IsAlwaysTrue; [Test] procedure TestConstruct_FirstPop_SucceedsAndResetsChanged; [Test] procedure TestConstruct_StateInteraction_SignalTriggersChanged; [Test] procedure TestConstruct_StateInteraction_PopResetsChangedAfterSignal; [Test] procedure TestConstruct_StateInteraction_NotifyOnAlreadySetSource_DoesNotRetrigger; [Test] procedure TestConstruct_Destruction_UnsubscribesAndNoCrashOnSourceNotify; // Tests for TLazy.Create with a pre-existing (non-nil) IMycLazy [Test] procedure TestCreateWithExistingLazy_DelegatesChangedCorrectly; [Test] procedure TestCreateWithExistingLazy_DelegatesPopCorrectly; // Tests for TLazy implicit operators [Test] procedure TestImplicitOperator_FromInterfaceToRecord; [Test] procedure TestImplicitOperator_FromRecordToInterface; // Tests for TLazy.Pop specific behaviors (Res undefined) [Test] procedure TestPop_AfterInitialAndNoSignal_ReturnsFalseAndResUndefined; end; implementation // No direct uses of Myc.Core.* units here { TTestMyLazy } procedure TTestMyLazy.Setup; begin // Create a common signal source for tests that need it. // TState.CreateDirty is from Myc.Signals.pas (interface part) // Its implementation might rely on Myc.Core.Signals, but that's an indirect usage. FChangingSignal := TFlag.CreateFlag; FChangingSignal.Reset; // Start with a clean (not set) signal for predictable test starts end; procedure TTestMyLazy.TearDown; begin FChangingSignal := nil; // Release the common signal source end; procedure TTestMyLazy.ConsumeInitialPop(var ALazyRec: TLazy; ExpectedInitialValue: Integer; const MsgPrefix: string); var val: Integer; popResult: Boolean; begin Assert.IsTrue(ALazyRec.Changed.IsSet, MsgPrefix + ': Changed.IsSet should be true before initial Pop'); popResult := ALazyRec.Pop(val); Assert.IsTrue(popResult, MsgPrefix + ': Initial Pop should return true'); Assert.AreEqual(ExpectedInitialValue, val, MsgPrefix + ': Value from initial Pop mismatch'); Assert.IsFalse(ALazyRec.Changed.IsSet, MsgPrefix + ': Changed.IsSet should be false after initial Pop'); end; // == Tests for TLazy.Create(nil) - Null Object Pattern == procedure TTestMyLazy.TestCreateWithNil_Changed_IsAlwaysTrue; var lazyRec: TLazy; begin lazyRec := TLazy.Create(nil); // This uses the internal FNull (TMycNullLazy) Assert.IsTrue(lazyRec.Changed.IsSet, 'For TLazy created with nil, Changed.IsSet should be true (TMycNullLazy behavior)'); // Second check to ensure it's consistently true Assert.IsTrue(lazyRec.Changed.IsSet, 'For TLazy created with nil, Changed.IsSet should remain true'); end; procedure TTestMyLazy.TestCreateWithNil_Pop_ReturnsTrueAndDefaultInteger; var lazyRec: TLazy; val: Integer; popResult: Boolean; begin lazyRec := TLazy.Create(nil); val := 12345; // Pre-assign to check if Pop overwrites it with Default popResult := lazyRec.Pop(val); Assert.IsTrue(popResult, 'Pop on TLazy created with nil should return true'); Assert.AreEqual(Default(Integer), val, 'Pop on TLazy created with nil should set Res to Default(Integer)'); end; procedure TTestMyLazy.TestCreateWithNil_Pop_ReturnsTrueAndDefaultString; var lazyRec: TLazy; val: string; popResult: Boolean; begin lazyRec := TLazy.Create(nil); val := 'test'; // Pre-assign popResult := lazyRec.Pop(val); Assert.IsTrue(popResult, 'Pop on TLazy created with nil (string) should return true'); Assert.AreEqual(Default(string), val, 'Pop on TLazy created with nil (string) should set Res to Default(string)'); end; // == Tests for TLazy.Construct static method == procedure TTestMyLazy.TestConstruct_InitialChanged_IsAlwaysTrue; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; procExecuted: Boolean; begin procExecuted := False; // FChangingSignal is reset in Setup lazyIntf := TLazy.Construct( FChangingSignal.State.Signal, function: Integer begin procExecuted := True; Result := 10; end ); Assert.IsNotNull(lazyIntf, 'TLazy.Construct should return a valid interface'); lazyRec := lazyIntf; // Implicit conversion Assert.IsTrue(lazyRec.Changed.IsSet, 'Constructed lazy object: Initial Changed.IsSet should be true by design'); Assert.IsFalse(procExecuted, 'Proc should not have been executed by Construct or by checking Changed state'); end; procedure TTestMyLazy.TestConstruct_FirstPop_SucceedsAndResetsChanged; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; procExecuted: Boolean; expectedValue: Integer; begin procExecuted := False; expectedValue := 20; lazyIntf := TLazy.Construct( FChangingSignal.State.Signal, function: Integer begin procExecuted := True; Result := expectedValue; end ); lazyRec := lazyIntf; ConsumeInitialPop(lazyRec, expectedValue, 'TestConstruct_FirstPop'); Assert.IsTrue(procExecuted, 'Proc should have been executed by the initial Pop'); end; procedure TTestMyLazy.TestConstruct_StateInteraction_SignalTriggersChanged; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; expectedValue: Integer; begin expectedValue := 30; lazyIntf := TLazy.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end); lazyRec := lazyIntf; ConsumeInitialPop(lazyRec, expectedValue, 'TestConstruct_StateInteraction_SignalTriggersChanged (Initial)'); Assert.IsFalse(lazyRec.Changed.IsSet, 'After initial Pop, Changed.IsSet should be false'); FChangingSignal.Notify; // Trigger the source signal Assert.IsTrue(lazyRec.Changed.IsSet, 'After source signal Notify, Changed.IsSet should become true'); end; procedure TTestMyLazy.TestConstruct_StateInteraction_PopResetsChangedAfterSignal; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; val: Integer; popResult: Boolean; procCallCount: Integer; begin procCallCount := 0; lazyIntf := TLazy.Construct( FChangingSignal.State.Signal, function: Integer begin Inc(procCallCount); Result := 100 + procCallCount; // Value changes per call end ); lazyRec := lazyIntf; ConsumeInitialPop(lazyRec, 101, 'TestConstruct_StateInteraction_PopResetsChangedAfterSignal (Initial)'); // procCallCount = 1 FChangingSignal.Notify; Assert.IsTrue(lazyRec.Changed.IsSet, 'Changed.IsSet should be true after signal'); popResult := lazyRec.Pop(val); // procCallCount = 2 Assert.IsTrue(popResult, 'Pop after signal should return true'); Assert.AreEqual(102, val, 'Value from Pop after signal mismatch'); Assert.AreEqual(2, procCallCount, 'Proc call count after second pop mismatch'); Assert.IsFalse(lazyRec.Changed.IsSet, 'Changed.IsSet should be false after Pop following signal'); end; procedure TTestMyLazy.TestConstruct_StateInteraction_NotifyOnAlreadySetSource_DoesNotRetrigger; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; val: Integer; popResult: Boolean; procCallCount: Integer; begin procCallCount := 0; lazyIntf := TLazy.Construct( FChangingSignal.State.Signal, function: Integer begin Inc(procCallCount); Result := 200 + procCallCount; end ); lazyRec := lazyIntf; // 1. Initial Pop ConsumeInitialPop(lazyRec, 201, 'TestConstruct_NotifyOnAlreadySetSource (Initial)'); // procCallCount = 1 Assert.IsFalse(FChangingSignal.State.IsSet, 'Source signal FChangingSignal should still be false (was reset in Setup)'); // 2. Trigger source, make it set, Pop FChangingSignal.Notify; // FChangingSignal.IsSet becomes TRUE Assert.IsTrue(lazyRec.Changed.IsSet, 'Lazy state should be true after FChangingSignal.Notify'); popResult := lazyRec.Pop(val); // procCallCount = 2 Assert.IsTrue(popResult); Assert.AreEqual(202, val); Assert.IsFalse(lazyRec.Changed.IsSet, 'Lazy state should be false after second Pop'); // 3. Notify FChangingSignal again. It's already set. // This should NOT re-notify subscribers (like the lazy object's internal trigger) // because TMycDirty only notifies on a false -> true transition. Assert.IsTrue(FChangingSignal.State.IsSet, 'FChangingSignal should still be true before redundant Notify'); FChangingSignal.Notify; Assert.IsFalse(lazyRec.Changed.IsSet, 'Lazy state should REMAIN false after Notify on an already-set source'); // 4. Attempt to Pop again popResult := lazyRec.Pop(val); // procCallCount should remain 2 Assert.IsFalse(popResult, 'Pop after Notify on an already-set source should return false'); Assert.AreEqual(2, procCallCount, 'Proc should not have been called for this Pop'); end; procedure TTestMyLazy.TestConstruct_Destruction_UnsubscribesAndNoCrashOnSourceNotify; var lazyIntf: TLazy.ILazy; localChangingSignal: TFlag.IFlag; // Use a local signal for this test to control its lifetime begin localChangingSignal := TFlag.CreateFlag; localChangingSignal.Reset; lazyIntf := TLazy.Construct(localChangingSignal.State.Signal, function: Integer begin Result := 1; end); Assert.IsNotNull(lazyIntf, 'Constructed lazy interface should not be nil'); // Simulate usage and release of the lazy object var lazyRec: TLazy := lazyIntf; // Wrap for initial pop ConsumeInitialPop(lazyRec, 1, 'TestConstruct_Destruction (Initial)'); lazyIntf := nil; // Release the ILazy interface. ARC should destroy the TMycFuncLazy object. // This should trigger its destructor, which should unsubscribe from localChangingSignal. Assert.WillNotRaise( procedure begin localChangingSignal.Notify; // Notify the source AFTER the lazy object is supposed to be gone. end, nil, // Default: any exception is a failure 'Notifying source after lazy object is freed should not crash, indicating unsubscription.' ); localChangingSignal := nil; // Clean up the local signal itself. end; // == Tests for TLazy.Create with a pre-existing (non-nil) ILazy == procedure TTestMyLazy.TestCreateWithExistingLazy_DelegatesChangedCorrectly; var originalLazyIntf: TLazy.ILazy; wrappedLazyRec: TLazy; expectedValue: Integer; begin expectedValue := 60; originalLazyIntf := TLazy.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end); // originalLazyIntf.Changed.IsSet is true by design wrappedLazyRec := TLazy.Create(originalLazyIntf); Assert.IsTrue(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet should reflect original (initially true)'); ConsumeInitialPop(wrappedLazyRec, expectedValue, 'TestCreateWithExistingLazy_DelegatesChangedCorrectly (Initial)'); // Now wrappedLazyRec.Changed.IsSet is false, and so should originalLazyIntf.Changed.IsSet Assert.IsFalse(originalLazyIntf.Changed.IsSet, 'Original lazy: Changed.IsSet should also be false after wrapped Pop'); FChangingSignal.Notify; Assert.IsTrue(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet should be true after source signal'); Assert.IsTrue(originalLazyIntf.Changed.IsSet, 'Original lazy: Changed.IsSet should also be true after source signal'); end; procedure TTestMyLazy.TestCreateWithExistingLazy_DelegatesPopCorrectly; var originalLazyIntf: TLazy.ILazy; wrappedLazyRec: TLazy; val: Integer; popResult: Boolean; procCallCount: Integer; begin procCallCount := 0; originalLazyIntf := TLazy.Construct( FChangingSignal.State.Signal, function: Integer begin Inc(procCallCount); Result := 70 + procCallCount; end ); wrappedLazyRec := TLazy.Create(originalLazyIntf); // First Pop via wrapper (initial pop) ConsumeInitialPop(wrappedLazyRec, 71, 'TestCreateWithExistingLazy_DelegatesPopCorrectly (Initial)'); // procCallCount = 1 Assert.AreEqual(1, procCallCount, 'Proc call count after wrapped initial Pop'); // Second Pop via wrapper (no source change yet) popResult := wrappedLazyRec.Pop(val); Assert.IsFalse(popResult, 'Second Pop via wrapper (no source change) should return false'); Assert.AreEqual(1, procCallCount, 'Proc call count should not change'); // Trigger source, Pop via wrapper FChangingSignal.Notify; Assert.IsTrue(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet true after source signal'); popResult := wrappedLazyRec.Pop(val); // procCallCount = 2 Assert.IsTrue(popResult, 'Pop via wrapper after source signal should return true'); Assert.AreEqual(72, val, 'Value from Pop via wrapper after signal'); Assert.AreEqual(2, procCallCount, 'Proc call count after signal and Pop'); Assert.IsFalse(wrappedLazyRec.Changed.IsSet, 'Wrapped lazy: Changed.IsSet false after Pop'); end; // == Tests for TLazy implicit operators == procedure TTestMyLazy.TestImplicitOperator_FromInterfaceToRecord; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; expectedValue: Integer; begin expectedValue := 80; lazyIntf := TLazy.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end); Assert.IsNotNull(lazyIntf, 'Interface should be assigned'); lazyRec := lazyIntf; // Implicit conversion: ILazy to TLazy // Verify by using the record Assert.IsTrue(lazyRec.Changed.IsSet, 'Record (from intf): Initial Changed.IsSet should be true'); ConsumeInitialPop(lazyRec, expectedValue, 'TestImplicitOperator_FromInterfaceToRecord'); end; procedure TTestMyLazy.TestImplicitOperator_FromRecordToInterface; var lazyIntfFromConstruct: TLazy.ILazy; lazyRec: TLazy; lazyIntfFromRecord: TLazy.ILazy; val: Integer; expectedValue: Integer; begin expectedValue := 90; lazyIntfFromConstruct := TLazy.Construct(FChangingSignal.State.Signal, function: Integer begin Result := expectedValue; end); lazyRec.Create(lazyIntfFromConstruct); // Explicitly create record lazyIntfFromRecord := lazyRec; // Implicit conversion: TLazy to ILazy // Verify by using the converted interface Assert.AreSame(lazyIntfFromConstruct, lazyIntfFromRecord, 'Converted interface should be the same as the original wrapped one'); Assert.IsTrue(lazyIntfFromRecord.Changed.IsSet, 'Interface (from rec): Initial Changed.IsSet should be true'); var popResult := lazyIntfFromRecord.Pop(val); // This also tests if the interface is functional Assert.IsTrue(popResult); Assert.AreEqual(expectedValue, val); Assert.IsFalse(lazyIntfFromRecord.Changed.IsSet); end; // == Tests for TLazy.Pop specific behaviors (Res undefined) == procedure TTestMyLazy.TestPop_AfterInitialAndNoSignal_ReturnsFalseAndResUndefined; var lazyIntf: TLazy.ILazy; lazyRec: TLazy; val: Integer; // Value will not be checked as Pop returns false popResult: Boolean; procExecuted: Boolean; begin procExecuted := False; lazyIntf := TLazy.Construct( FChangingSignal.State.Signal, function: Integer begin procExecuted := True; Result := 100; end ); lazyRec := lazyIntf; ConsumeInitialPop(lazyRec, 100, 'TestPop_AfterInitialAndNoSignal (Initial)'); Assert.IsTrue(procExecuted, 'Proc should have run for initial pop'); procExecuted := False; // Reset for next check // FChangingSignal has not been notified again Assert.IsFalse(lazyRec.Changed.IsSet, 'Changed.IsSet must be false before this Pop attempt'); popResult := lazyRec.Pop(val); Assert.IsFalse(popResult, 'Pop when Changed.IsSet is false should return false'); Assert.IsFalse(procExecuted, 'Proc should NOT have run as Pop returned false'); // Do NOT check 'val' as its content is undefined when Pop returns false. end; initialization TDUnitX.RegisterTestFixture(TTestMyLazy); end.