unit Myc.Test.Signals.Dirty; interface uses DUnitX.TestFramework, System.SysUtils, Myc.Signals; // Unit under test type // Helper class to mock a subscriber (reuse or redefine if not in common unit) TMockSubscriber = class(TInterfacedObject, TSignal.ISubscriber) public NotifyCount: Integer; NotifyShouldReturn: Boolean; WasCalled: Boolean; LastReturnedValueFromSource: Boolean; // To store what the source's Notify returned constructor Create(AShouldReturn: Boolean = True); function Notify: Boolean; // TSignal.ISubscriber implementation. end; [TestFixture] TTestMycDirtyFlag = class(TObject) private // Helper to create a dirty flag and optionally reset it for a clean initial state function CreateAndPrepareDirtyFlag(StartDirty: Boolean = True): TFlag.IFlag; public [Setup] procedure Setup; [TearDown] procedure TearDown; // Category 1: Basic State, Notify (as Subscriber method), and Reset [Test] procedure TestCreate_InitialStateIsDirty; [Test] procedure TestReset_FromDirtyState_BecomesCleanAndReturnsTrue; [Test] procedure TestReset_FromCleanState_StaysCleanAndReturnsFalse; [Test] procedure TestNotify_OnCleanFlag_SetsDirtyAndReturnsTrue; [Test] procedure TestNotify_OnDirtyFlag_StaysDirtyAndReturnsFalse; // Category 2: Subscriber Notifications [Test] procedure TestSubscribe_ToAlreadyDirtyFlag_NotifiesSubscriberImmediately; [Test] procedure TestSubscribe_ToCleanFlag_DoesNotNotifySubscriberImmediately; [Test] procedure TestSubscriber_Notified_WhenFlagTransitionsToDirty; [Test] procedure TestSubscriber_NotNotified_WhenSettingAlreadyDirtyFlagViaNotify; [Test] procedure TestSubscriber_NotNotified_ByResetAction; [Test] procedure TestMultipleSubscribers_Notified_WhenFlagTransitionsToDirty; [Test] procedure TestUnsubscribe_SubscriberNotNotified; // Category 3: Chaining Dirty Flags [Test] procedure TestChain_A_Notifies_B_SimplePropagation; [Test] procedure TestChain_A_Notifies_B_Notifies_C_SimplePropagation; // Category 4: Chaining with Resets and Re-triggering (Consistency) [Test] procedure TestChain_A_B_ResetB_RetriggerA_BStaysCleanIfANotChanged; [Test] procedure TestChain_A_B_ResetA_ThenTriggerA_FullPropagationToB; [Test] procedure TestChain_A_B_C_IntermediateBReset_RetriggerA_PropagatesToC; [Test] procedure TestChain_A_B_C_AllReset_RetriggerA_FullPropagation; [Test] procedure TestChain_A_B_C_TriggerA_ResetA_TriggerA_EnsureCNotifiedCorrectly; end; implementation { TMockSubscriber } constructor TMockSubscriber.Create(AShouldReturn: Boolean = True); begin inherited Create; NotifyCount := 0; NotifyShouldReturn := AShouldReturn; WasCalled := False; LastReturnedValueFromSource := False; // Default end; function TMockSubscriber.Notify: Boolean; begin Inc(NotifyCount); WasCalled := True; // This return value is what this subscriber tells the source signal. // For testing, we usually want it to return false to signify it doesn't need more from this one event. Result := NotifyShouldReturn; end; { TTestMycDirtyFlag } function TTestMycDirtyFlag.CreateAndPrepareDirtyFlag(StartDirty: Boolean = True): TFlag.IFlag; begin Result := TFlag.CreateFlag(true); // Initially dirty if not StartDirty then Result.Reset; // Reset to make it clean Assert.AreEqual(StartDirty, Result.State.IsSet, 'CreateAndPrepareDirtyFlag initial state incorrect.'); end; procedure TTestMycDirtyFlag.Setup; begin // Per-test setup end; procedure TTestMycDirtyFlag.TearDown; begin // Per-test teardown end; // Category 1: Basic State, Notify (as Subscriber method), and Reset procedure TTestMycDirtyFlag.TestCreate_InitialStateIsDirty; var dirtyFlag: TFlag.IFlag; begin dirtyFlag := TFlag.CreateFlag(true); Assert.IsTrue(dirtyFlag.State.IsSet, 'Newly created dirty flag should be IsSet.'); dirtyFlag := TFlag.CreateFlag(false); Assert.IsFalse(dirtyFlag.State.IsSet, 'Newly created dirty flag should not be IsSet.'); end; procedure TTestMycDirtyFlag.TestReset_FromDirtyState_BecomesCleanAndReturnsTrue; var dirtyFlag: TFlag.IFlag; resetResult: Boolean; begin dirtyFlag := CreateAndPrepareDirtyFlag(True); // Start dirty resetResult := dirtyFlag.Reset; Assert.IsFalse(dirtyFlag.State.IsSet, 'Flag should be clean (not IsSet) after Reset.'); Assert.IsTrue(resetResult, 'Reset() should return True when resetting a dirty flag.'); end; procedure TTestMycDirtyFlag.TestReset_FromCleanState_StaysCleanAndReturnsFalse; var dirtyFlag: TFlag.IFlag; resetResult: Boolean; begin dirtyFlag := CreateAndPrepareDirtyFlag(False); // Start clean resetResult := dirtyFlag.Reset; Assert.IsFalse(dirtyFlag.State.IsSet, 'Flag should remain clean (not IsSet) after Reset on clean flag.'); Assert.IsFalse(resetResult, 'Reset() should return False when resetting an already clean flag.'); end; procedure TTestMycDirtyFlag.TestNotify_OnCleanFlag_SetsDirtyAndReturnsTrue; var dirtyFlag: TFlag.IFlag; notifyResult: Boolean; begin dirtyFlag := CreateAndPrepareDirtyFlag(False); // Start clean 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); end; procedure TTestMycDirtyFlag.TestNotify_OnDirtyFlag_StaysDirtyAndReturnsFalse; var dirtyFlag: TFlag.IFlag; notifyResult: Boolean; begin dirtyFlag := CreateAndPrepareDirtyFlag(True); // Start dirty notifyResult := dirtyFlag.Notify; // Call TSignal.ISubscriber.Notify again Assert.IsTrue(dirtyFlag.State.IsSet, 'Flag should remain dirty (IsSet).'); Assert.IsTrue(notifyResult); end; // Category 2: Subscriber Notifications procedure TTestMycDirtyFlag.TestSubscribe_ToAlreadyDirtyFlag_NotifiesSubscriberImmediately; var dirtyFlag: TFlag.IFlag; subscriber: TMockSubscriber; subscription: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(True); // Start dirty subscriber := TMockSubscriber.Create; subscription := dirtyFlag.State.Signal.Subscribe(subscriber); Assert.AreEqual(1, subscriber.NotifyCount, 'Subscriber should be notified immediately when subscribing to an already dirty flag.'); Assert.IsTrue(subscriber.WasCalled, 'Subscriber WasCalled should be true.'); end; procedure TTestMycDirtyFlag.TestSubscribe_ToCleanFlag_DoesNotNotifySubscriberImmediately; var dirtyFlag: TFlag.IFlag; subscriber: TMockSubscriber; subscription: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(False); // Start clean subscriber := TMockSubscriber.Create; subscription := dirtyFlag.State.Signal.Subscribe(subscriber); Assert.AreEqual(0, subscriber.NotifyCount, 'Subscriber should NOT be notified immediately when subscribing to a clean flag.'); Assert.IsFalse(subscriber.WasCalled, 'Subscriber WasCalled should be false.'); end; procedure TTestMycDirtyFlag.TestSubscriber_Notified_WhenFlagTransitionsToDirty; var dirtyFlag: TFlag.IFlag; subscriber: TMockSubscriber; subscription: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(False); // Start clean subscriber := TMockSubscriber.Create; subscription := dirtyFlag.State.Signal.Subscribe(subscriber); Assert.AreEqual(0, subscriber.NotifyCount, 'Subscriber initially not notified.'); dirtyFlag.Notify; // Make the flag dirty Assert.AreEqual(1, subscriber.NotifyCount, 'Subscriber should be notified once flag transitions to dirty.'); end; procedure TTestMycDirtyFlag.TestSubscriber_NotNotified_WhenSettingAlreadyDirtyFlagViaNotify; var dirtyFlag: TFlag.IFlag; subscriber: TMockSubscriber; subscription: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(True); // Start dirty subscriber := TMockSubscriber.Create; subscription := dirtyFlag.State.Signal.Subscribe(subscriber); // Gets initial notification Assert.AreEqual(1, subscriber.NotifyCount, 'Subscriber notified on subscribe to dirty flag.'); dirtyFlag.Notify; // Notify again, flag was already dirty Assert.AreEqual(1, subscriber.NotifyCount, 'Subscriber should NOT be notified again if flag was already dirty.'); end; procedure TTestMycDirtyFlag.TestSubscriber_NotNotified_ByResetAction; var dirtyFlag: TFlag.IFlag; subscriber: TMockSubscriber; subscription: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(True); // Start dirty subscriber := TMockSubscriber.Create; subscription := dirtyFlag.State.Signal.Subscribe(subscriber); // Gets initial notification Assert.AreEqual(1, subscriber.NotifyCount, 'Subscriber notified on subscribe.'); dirtyFlag.Reset; // Reset the flag Assert.IsFalse(dirtyFlag.State.IsSet, 'Flag is now clean.'); // Current TMycDirty.Reset does not notify subscribers. Assert.AreEqual(1, subscriber.NotifyCount, 'Subscriber should NOT be notified by Reset action.'); end; procedure TTestMycDirtyFlag.TestMultipleSubscribers_Notified_WhenFlagTransitionsToDirty; var dirtyFlag: TFlag.IFlag; sub1, sub2: TMockSubscriber; subscription1, subscription2: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(False); // Start clean sub1 := TMockSubscriber.Create; sub2 := TMockSubscriber.Create; subscription1 := dirtyFlag.State.Signal.Subscribe(sub1); subscription2 := dirtyFlag.State.Signal.Subscribe(sub2); dirtyFlag.Notify; // Make flag dirty Assert.AreEqual(1, sub1.NotifyCount, 'Subscriber 1 should be notified.'); Assert.AreEqual(1, sub2.NotifyCount, 'Subscriber 2 should be notified.'); end; procedure TTestMycDirtyFlag.TestUnsubscribe_SubscriberNotNotified; var dirtyFlag: TFlag.IFlag; subscriber: TMockSubscriber; subscription: TSignal.TSubscription; begin dirtyFlag := CreateAndPrepareDirtyFlag(False); // Start clean subscriber := TMockSubscriber.Create; subscription := dirtyFlag.State.Signal.Subscribe(subscriber); subscription.Unsubscribe; // Explicitly unsubscribe dirtyFlag.Notify; // Make flag dirty Assert.AreEqual(0, subscriber.NotifyCount, 'Unsubscribed subscriber should not be notified.'); end; // Category 3: Chaining Dirty Flags // In these tests, FlagX.Notify means calling the TSignal.ISubscriber.Notify method on FlagX. // This makes FlagX dirty and potentially notifies its subscribers (like FlagY). procedure TTestMycDirtyFlag.TestChain_A_Notifies_B_SimplePropagation; var flagA, flagB: TFlag.IFlag; subToB: TMockSubscriber; subscriptionA_B, subscriptionMock_B: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); // A starts clean flagB := CreateAndPrepareDirtyFlag(False); // B starts clean subToB := TMockSubscriber.Create; subscriptionMock_B := flagB.State.Signal.Subscribe(subToB); subscriptionA_B := flagA.State.Signal.Subscribe(flagB); // B subscribes to A's state changes (B's Notify will be called) flagA.Notify; // Make A dirty. This should trigger B.Notify Assert.IsTrue(flagA.State.IsSet, 'Flag A should be dirty.'); Assert.IsTrue(flagB.State.IsSet, 'Flag B should become dirty due to A.'); Assert.AreEqual(1, subToB.NotifyCount, 'Subscriber to B should be notified once.'); end; procedure TTestMycDirtyFlag.TestChain_A_Notifies_B_Notifies_C_SimplePropagation; var flagA, flagB, flagC: TFlag.IFlag; subToC: TMockSubscriber; subscriptionB_A, subscriptionC_B, subscriptionMock_C: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); flagB := CreateAndPrepareDirtyFlag(False); flagC := CreateAndPrepareDirtyFlag(False); subToC := TMockSubscriber.Create; subscriptionMock_C := flagC.State.Signal.Subscribe(subToC); subscriptionC_B := flagB.State.Signal.Subscribe(flagC); // C subscribes to B subscriptionB_A := flagA.State.Signal.Subscribe(flagB); // B subscribes to A flagA.Notify; // Make A dirty Assert.IsTrue(flagA.State.IsSet, 'Flag A should be dirty in cascade.'); Assert.IsTrue(flagB.State.IsSet, 'Flag B should be dirty in cascade.'); Assert.IsTrue(flagC.State.IsSet, 'Flag C should be dirty in cascade.'); Assert.AreEqual(1, subToC.NotifyCount, 'Subscriber to C should be notified once in cascade.'); end; // Category 4: Chaining with Resets and Re-triggering (Consistency) procedure TTestMycDirtyFlag.TestChain_A_B_ResetB_RetriggerA_BStaysCleanIfANotChanged; var flagA, flagB: TFlag.IFlag; subToB: TMockSubscriber; subscriptionA_B, subscriptionMock_B: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); flagB := CreateAndPrepareDirtyFlag(False); subToB := TMockSubscriber.Create; subscriptionMock_B := flagB.State.Signal.Subscribe(subToB); subscriptionA_B := flagA.State.Signal.Subscribe(flagB); flagA.Notify; // A dirty -> B dirty. subToB notified. Assert.IsTrue(flagB.State.IsSet, 'Flag B should be dirty initially.'); Assert.AreEqual(1, subToB.NotifyCount, 'Subscriber to B initially notified.'); flagB.Reset; // B becomes clean. A is still dirty. Assert.IsFalse(flagB.State.IsSet, 'Flag B should be clean after reset.'); Assert.AreEqual(1, subToB.NotifyCount, 'Subscriber to B not notified by B.Reset.'); // Notify A again. A was already dirty. So A.Notify() returns false, A does not call ProtectedNotifyOwnSubscribers. // Thus, B.Notify() is NOT called. B should remain clean. flagA.Notify; Assert.IsTrue(flagA.State.IsSet, 'Flag A remains dirty.'); Assert.IsFalse(flagB.State.IsSet, 'Flag B should remain clean if A did not transition to dirty.'); Assert.AreEqual(1, subToB.NotifyCount, 'Subscriber to B should not be notified again.'); end; procedure TTestMycDirtyFlag.TestChain_A_B_ResetA_ThenTriggerA_FullPropagationToB; var flagA, flagB: TFlag.IFlag; subToB: TMockSubscriber; subscriptionA_B, subscriptionMock_B: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); flagB := CreateAndPrepareDirtyFlag(False); subToB := TMockSubscriber.Create; subscriptionMock_B := flagB.State.Signal.Subscribe(subToB); subscriptionA_B := flagA.State.Signal.Subscribe(flagB); flagA.Notify; // A dirty -> B dirty. subToB notified. Assert.IsTrue(flagB.State.IsSet, 'Flag B should be dirty after A triggers.'); Assert.AreEqual(1, subToB.NotifyCount, 'subToB notified once.'); flagA.Reset; // A becomes clean. B is still dirty (state change of A to clean doesn't propagate as a "dirty" signal to B). Assert.IsFalse(flagA.State.IsSet, 'Flag A is clean after reset.'); Assert.IsTrue(flagB.State.IsSet, 'Flag B remains dirty.'); // B's state doesn't change because A becoming clean doesn't call B.Notify flagA.Notify; // A (was clean) becomes dirty again. A.Notify() returns true. A notifies B. // B.Notify() is called. B was dirty. B.Notify() sets FFlag to true (no change), returns false. // So B does NOT notify subToB again. Assert.IsTrue(flagA.State.IsSet, 'Flag A is dirty again.'); Assert.IsTrue(flagB.State.IsSet, 'Flag B is dirty (was already, and A.Notify called B.Notify which set it dirty again).'); Assert.AreEqual(1, subToB.NotifyCount, 'subToB should NOT be notified again as B did not transition from clean to dirty.'); end; procedure TTestMycDirtyFlag.TestChain_A_B_C_IntermediateBReset_RetriggerA_PropagatesToC; var flagA, flagB, flagC: TFlag.IFlag; subToC: TMockSubscriber; subscriptionB_A, subscriptionC_B, subscriptionMock_C: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); flagB := CreateAndPrepareDirtyFlag(False); flagC := CreateAndPrepareDirtyFlag(False); subToC := TMockSubscriber.Create; subscriptionMock_C := flagC.State.Signal.Subscribe(subToC); subscriptionC_B := flagB.State.Signal.Subscribe(flagC); subscriptionB_A := flagA.State.Signal.Subscribe(flagB); // Initial full propagation flagA.Notify; // A dirty -> B dirty -> C dirty. subToC notified. Assert.IsTrue(flagC.State.IsSet, 'Flag C should be dirty initially.'); Assert.AreEqual(1, subToC.NotifyCount, 'subToC initial count.'); // Intermediate reset flagB.Reset; // B becomes clean. A is dirty, C is dirty. Assert.IsFalse(flagB.State.IsSet, 'Flag B is clean.'); // Reset and re-trigger A flagA.Reset; // A becomes clean. Assert.IsFalse(flagA.State.IsSet, 'Flag A is clean.'); flagA.Notify; // A (was clean) -> A dirty. A notifies B. // B.Notify() called. B was clean -> B dirty. B notifies C. // C.Notify() called. C was dirty -> C set dirty (no change), C.Notify() returns false. // So subToC is NOT notified again. Assert.IsTrue(flagA.State.IsSet, 'Flag A is dirty again.'); Assert.IsTrue(flagB.State.IsSet, 'Flag B becomes dirty again.'); Assert.IsTrue(flagC.State.IsSet, 'Flag C is dirty (was already).'); Assert.AreEqual(1, subToC.NotifyCount, 'subToC should NOT be notified again.'); end; procedure TTestMycDirtyFlag.TestChain_A_B_C_AllReset_RetriggerA_FullPropagation; var flagA, flagB, flagC: TFlag.IFlag; subToC: TMockSubscriber; subscriptionB_A, subscriptionC_B, subscriptionMock_C: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); flagB := CreateAndPrepareDirtyFlag(False); flagC := CreateAndPrepareDirtyFlag(False); subToC := TMockSubscriber.Create; subscriptionMock_C := flagC.State.Signal.Subscribe(subToC); subscriptionC_B := flagB.State.Signal.Subscribe(flagC); subscriptionB_A := flagA.State.Signal.Subscribe(flagB); // All are clean. Trigger A. flagA.Notify; // A dirty -> B dirty -> C dirty. subToC notified. Assert.IsTrue(flagC.State.IsSet, 'Flag C dirty after first full propagation.'); Assert.AreEqual(1, subToC.NotifyCount, 'subToC notified once.'); // Reset all flagA.Reset; flagB.Reset; flagC.Reset; Assert.IsFalse(flagA.State.IsSet, 'Flag A clean.'); Assert.IsFalse(flagB.State.IsSet, 'Flag B clean.'); Assert.IsFalse(flagC.State.IsSet, 'Flag C clean.'); // subToC count is still 1, it's not notified by resets. // Re-trigger A flagA.Notify; // A (was clean) -> A dirty. A notifies B. // B.Notify() called. B was clean -> B dirty. B notifies C. // C.Notify() called. C was clean -> C dirty. C notifies subToC. Assert.IsTrue(flagA.State.IsSet, 'Flag A dirty on re-trigger.'); Assert.IsTrue(flagB.State.IsSet, 'Flag B dirty on re-trigger.'); Assert.IsTrue(flagC.State.IsSet, 'Flag C dirty on re-trigger.'); Assert.AreEqual(2, subToC.NotifyCount, 'subToC should be notified a second time.'); end; procedure TTestMycDirtyFlag.TestChain_A_B_C_TriggerA_ResetA_TriggerA_EnsureCNotifiedCorrectly; var flagA, flagB, flagC: TFlag.IFlag; subToC: TMockSubscriber; subscriptionB_A, subscriptionC_B, subscriptionMock_C: TSignal.TSubscription; begin flagA := CreateAndPrepareDirtyFlag(False); flagB := CreateAndPrepareDirtyFlag(False); flagC := CreateAndPrepareDirtyFlag(False); subToC := TMockSubscriber.Create; subscriptionMock_C := flagC.State.Signal.Subscribe(subToC); subscriptionC_B := flagB.State.Signal.Subscribe(flagC); subscriptionB_A := flagA.State.Signal.Subscribe(flagB); // 1. Trigger A flagA.Notify; // A, B, C become dirty. subToC notified (count = 1). Assert.IsTrue(flagC.State.IsSet, 'C is dirty after 1st trigger.'); Assert.AreEqual(1, subToC.NotifyCount, 'subToC count after 1st trigger.'); // 2. Reset A (B and C remain dirty) flagA.Reset; Assert.IsFalse(flagA.State.IsSet, 'A is clean.'); Assert.IsTrue(flagB.State.IsSet, 'B remains dirty.'); Assert.IsTrue(flagC.State.IsSet, 'C remains dirty.'); // 3. Trigger A again flagA.Notify; // A (was clean) becomes dirty. A notifies B. // B.Notify() is called. B was dirty. B.Notify sets FFlag to true (no change), returns false. // B does NOT notify C. C remains dirty. subToC is NOT notified again. Assert.IsTrue(flagA.State.IsSet, 'A is dirty after 2nd trigger.'); Assert.IsTrue(flagB.State.IsSet, 'B is still dirty (state did not flip to clean then dirty).'); Assert.IsTrue(flagC.State.IsSet, 'C is still dirty.'); Assert.AreEqual(1, subToC.NotifyCount, 'subToC count should remain 1.'); end; initialization TDUnitX.RegisterTestFixture(TTestMycDirtyFlag); end.