unit Myc.Test.Signals.Latch; interface uses DUnitX.TestFramework, System.SysUtils, Myc.Signals; // Unit under test, using TMycLatch static members type // Helper class to mock a subscriber. TMockSubscriber = class(TInterfacedObject, TSignal.ISubscriber) public NotifyCount: Integer; NotifyShouldReturn: Boolean; // Determines what this mock's Notify method returns WasCalled: Boolean; constructor Create(AShouldReturn: Boolean = True); // Parameter name AShouldReturn for clarity function Notify: Boolean; // TSignal.ISubscriber implementation. end; [TestFixture] TTestMycLatch = class(TObject) public [Setup] procedure Setup; [TearDown] procedure TearDown; // --- Category 1: Basic Counter and State Functionality (Parameterized) --- [TestCase('InitialPositive', '1,False')] [TestCase('InitialZero_ReturnsNullLatch', '0,True')] [TestCase('InitialNegative_ReturnsNullLatch', '-1,True')] [TestCase('InitialLargePositive', '5,False')] procedure TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean); [TestCase('DecrementPositiveToZero', '1,False,True')] // InitialCount, ExpectedReturnFromLatchNotify, ExpectedIsSetAfterNotify [TestCase('DecrementPositiveToPositive', '2,True,False')] [TestCase('DecrementZeroToNegative', '0,False,True')] // Latch.Notify on count 0 returns false (0 > 0 is false) [TestCase('DecrementNegativeToNegative', '-1,False,True')] // Latch.Notify on count -1 returns false (-1 > 0 is false) procedure TestNotify_ReturnValueAndState_AfterOneNotify(InitialCount: Integer; ExpectedReturn: Boolean; ExpectedIsSet: Boolean); [Test] procedure TestNotify_DecrementsCounter_StateChanges; [Test] procedure TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet; // --- Category 2: Notification to Subscribers --- [Test] procedure TestSubscriber_Single_IsNotifiedWhenLatchSet; [Test] procedure TestSubscriber_Multiple_AreNotifiedWhenLatchSet; [Test] procedure TestSubscriber_NotNotified_BeforeLatchSet; [Test] procedure TestSubscriber_NotifiedOnlyOnce_AfterLatchSetAndUnadvised; // Clarified name [Test] procedure TestSubscribe_ToAlreadySetLatch_NotifiesImmediately; // --- Category 3: Chaining and Cascading Latches --- [Test] procedure TestChaining_A_Notifies_B; [Test] procedure TestCascade_A_Notifies_B_Notifies_C; [Test] procedure TestChaining_B_NeedsMultipleNotifiesFromA_WithSubscriberOnB; // --- Category 4: Special Cases --- [Test] procedure TestInitiallySetLatch_NotifiesNewSubscriberImmediately; // Using CreateLatch(0) [Test] procedure TestUnsubscribe_SubscriberNotNotified; // --- Category 5: Counter Integrity / State for Later Subscribers --- [Test] procedure TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber; // --- Category 6: UnadviseAll Behavior on Latch Set --- [Test] procedure TestLatchSet_ClearsSubscribers_NoFurtherNotification; [Test] procedure TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll; [Test] procedure TestLatchSet_NewSubscriberToSetLatch_NotifiedImmediatelyButNotPersistedForLatchNotify; end; implementation { TMockSubscriber } constructor TMockSubscriber.Create(AShouldReturn: Boolean = True); begin inherited Create; NotifyCount := 0; NotifyShouldReturn := AShouldReturn; WasCalled := False; end; function TMockSubscriber.Notify: Boolean; begin Inc(NotifyCount); WasCalled := True; Result := NotifyShouldReturn; // This mock subscriber returns what it's configured to. end; { TTestMycLatch } procedure TTestMycLatch.Setup; begin // Per-test setup code can go here (if any). end; procedure TTestMycLatch.TearDown; begin // Per-test teardown code can go here (if any). end; // --- Category 1: Basic Counter and State Functionality (Parameterized) --- procedure TTestMycLatch.TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean); var latch: TLatch.ILatch; begin // TMycLatch.CreateLatch returns TMycLatch.Null if InitialCount <= 0 latch := TLatch.CreateLatch(InitialCount); Assert.AreEqual(ExpectedIsSet, latch.State.IsSet, 'Latch initial IsSet state mismatch for count ' + IntToStr(InitialCount) + '.'); end; procedure TTestMycLatch.TestNotify_ReturnValueAndState_AfterOneNotify( InitialCount: Integer; ExpectedReturn: Boolean; ExpectedIsSet: Boolean ); var latch: TLatch.ILatch; returnedValueFromNotify: Boolean; begin latch := TLatch.CreateLatch(InitialCount); returnedValueFromNotify := latch.Notify; // This is TLatch.ILatch (as TSignal.ISubscriber).Notify Assert.AreEqual( ExpectedReturn, returnedValueFromNotify, 'Unexpected return value from Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.' ); Assert.AreEqual( ExpectedIsSet, latch.State.IsSet, 'Unexpected IsSet state after Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.' ); end; procedure TTestMycLatch.TestNotify_DecrementsCounter_StateChanges; var latch: TLatch.ILatch; begin latch := TLatch.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).'); end; procedure TTestMycLatch.TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet; var latch: TLatch.ILatch; begin latch := TLatch.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 Assert.IsTrue(latch.State.IsSet, 'Latch should remain set after second Notify.'); end; // --- Category 2: Notification to Subscribers --- procedure TTestMycLatch.TestSubscriber_Single_IsNotifiedWhenLatchSet; var latch: TLatch.ILatch; mockSub: TMockSubscriber; subscription: TSignal.TSubscription; begin latch := TLatch.CreateLatch(1); mockSub := TMockSubscriber.Create; subscription := latch.State.Signal.Subscribe(mockSub); // Subscribing to the Latch's state Assert.IsFalse(latch.State.IsSet, 'Latch should initially not be set.'); Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not have been notified yet (before latch set).'); latch.Notify; // Latch becomes set, notifies subscribers Assert.IsTrue(latch.State.IsSet, 'Latch should be set after its Notify is called.'); Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified once when latch becomes set.'); end; procedure TTestMycLatch.TestSubscriber_Multiple_AreNotifiedWhenLatchSet; var latch: TLatch.ILatch; mockSub1, mockSub2, mockSub3: TMockSubscriber; sub1, sub2, sub3: TSignal.TSubscription; // Keep subscriptions in scope begin latch := TLatch.CreateLatch(1); mockSub1 := TMockSubscriber.Create; mockSub2 := TMockSubscriber.Create; mockSub3 := TMockSubscriber.Create; sub1 := latch.State.Signal.Subscribe(mockSub1); sub2 := latch.State.Signal.Subscribe(mockSub2); sub3 := latch.State.Signal.Subscribe(mockSub3); latch.Notify; // Latch becomes set Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notification count mismatch.'); Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notification count mismatch.'); Assert.AreEqual(1, mockSub3.NotifyCount, 'MockSub3 notification count mismatch.'); end; procedure TTestMycLatch.TestSubscriber_NotNotified_BeforeLatchSet; var latch: TLatch.ILatch; mockSub: TMockSubscriber; subscription: TSignal.TSubscription; begin latch := TLatch.CreateLatch(2); // Count = 2 mockSub := TMockSubscriber.Create; subscription := latch.State.Signal.Subscribe(mockSub); Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified upon subscription if latch is not set.'); latch.Notify; // Count becomes 1, still not set Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should still not be notified as latch is not yet set (Count=1).'); end; procedure TTestMycLatch.TestSubscriber_NotifiedOnlyOnce_AfterLatchSetAndUnadvised; var latch: TLatch.ILatch; mockSub: TMockSubscriber; subscription: TSignal.TSubscription; begin latch := TLatch.CreateLatch(1); mockSub := TMockSubscriber.Create; subscription := latch.State.Signal.Subscribe(mockSub); latch.Notify; // Sets latch, notifies MockSub. Subscribers are then unadvised by UnadviseAll. Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should be 1 after latch set.'); latch.Notify; // Call Notify on latch again (FCount becomes more negative). Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should remain 1 (not notified again as it was unadvised).'); end; procedure TTestMycLatch.TestSubscribe_ToAlreadySetLatch_NotifiesImmediately; var latch: TLatch.ILatch; mockSub1, mockSub2: TMockSubscriber; subscription1, subscription2: TSignal.TSubscription; begin latch := TLatch.CreateLatch(1); // Create with count 1 mockSub1 := TMockSubscriber.Create; subscription1 := latch.State.Signal.Subscribe(mockSub1); // Subscribe before set latch.Notify; // Latch becomes set. mockSub1 is notified. Assert.IsTrue(latch.State.IsSet, 'Latch should be set after initial trigger.'); Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once.'); // Attempt to subscribe mockSub2 after the latch is already set. mockSub2 := TMockSubscriber.Create; subscription2 := latch.State.Signal.Subscribe(mockSub2); // TMycLatch.Subscribe (via TMycAbstractFlag) notifies immediately if already set. Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should be notified immediately upon subscribing to an already set latch.'); latch.Notify; // Call Notify on latch again (FCount becomes more negative). Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notify count should remain 1 (was unadvised).'); Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notify count should remain 1 (was only notified on subscribe, not added to list).'); end; // --- Category 3: Chaining and Cascading Latches --- procedure TTestMycLatch.TestChaining_A_Notifies_B; var latchA, latchB: TLatch.ILatch; mockSubForB: TMockSubscriber; subHandle_B_listens_A, subHandle_Mock_listens_B: TSignal.TSubscription; begin latchA := TLatch.CreateLatch(1); latchB := TLatch.CreateLatch(1); // latchB is an TSignal.ISubscriber mockSubForB := TMockSubscriber.Create; subHandle_Mock_listens_B := latchB.State.Signal.Subscribe(mockSubForB); // LatchA's state is an IMycSignal. LatchB (as TSignal.ISubscriber) subscribes to it. subHandle_B_listens_A := latchA.State.Signal.Subscribe(latchB); Assert.IsFalse(latchA.State.IsSet, 'LatchA initially not set.'); Assert.IsFalse(latchB.State.IsSet, 'LatchB initially not set.'); Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB initially not notified.'); latchA.Notify; // Call Notify on LatchA (as TSignal.ISubscriber) Assert.IsTrue(latchA.State.IsSet, 'LatchA should be set after notify.'); Assert.IsTrue(latchB.State.IsSet, 'LatchB should be set after being notified by LatchA.'); Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by LatchB.'); end; procedure TTestMycLatch.TestCascade_A_Notifies_B_Notifies_C; var latchA, latchB, latchC: TLatch.ILatch; mockSubForC: TMockSubscriber; sub_Mock_C, sub_C_B, sub_B_A: TSignal.TSubscription; begin latchA := TLatch.CreateLatch(1); latchB := TLatch.CreateLatch(1); latchC := TLatch.CreateLatch(1); mockSubForC := TMockSubscriber.Create; sub_Mock_C := latchC.State.Signal.Subscribe(mockSubForC); sub_C_B := latchB.State.Signal.Subscribe(latchC); // LatchC subscribes to LatchB's state sub_B_A := latchA.State.Signal.Subscribe(latchB); // LatchB subscribes to LatchA's state latchA.Notify; // Call Notify on LatchA Assert.IsTrue(latchA.State.IsSet, 'LatchA state mismatch in cascade.'); Assert.IsTrue(latchB.State.IsSet, 'LatchB state mismatch in cascade.'); Assert.IsTrue(latchC.State.IsSet, 'LatchC state mismatch in cascade.'); Assert.AreEqual(1, mockSubForC.NotifyCount, 'MockSubForC notification count mismatch in cascade.'); end; procedure TTestMycLatch.TestChaining_B_NeedsMultipleNotifiesFromA_WithSubscriberOnB; var latchA, latchB: TLatch.ILatch; mockSubForB: TMockSubscriber; sub_Mock_B, sub_B_A: TSignal.TSubscription; 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 := TLatch.CreateLatch(2); latchB := TLatch.CreateLatch(1); mockSubForB := TMockSubscriber.Create; sub_Mock_B := latchB.State.Signal.Subscribe(mockSubForB); sub_B_A := latchA.State.Signal.Subscribe(latchB); latchA.Notify; // First notify for LatchA (count becomes 1) Assert.IsFalse(latchA.State.IsSet, 'LatchA should not be set after first notify of two.'); Assert.IsFalse(latchB.State.IsSet, 'LatchB should not have been triggered yet by LatchA.'); Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB should not have been notified yet.'); latchA.Notify; // Second notify for LatchA (count becomes 0, LatchA becomes set) Assert.IsTrue(latchA.State.IsSet, 'LatchA should be set after second notify.'); // When LatchA becomes set, it notifies its subscribers (LatchB). LatchB.Notify is called. Assert.IsTrue(latchB.State.IsSet, 'LatchB should now be set by LatchA.'); Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by LatchB.'); end; // --- Category 4: Special Cases --- procedure TTestMycLatch.TestInitiallySetLatch_NotifiesNewSubscriberImmediately; var latch: TLatch.ILatch; // Will be TMycLatch.Null mockSub: TMockSubscriber; subscription: TSignal.TSubscription; begin latch := TLatch.CreateLatch(0); // Returns TMycLatch.Null which is set. mockSub := TMockSubscriber.Create; subscription := latch.State.Signal.Subscribe(mockSub); // TMycLatch.Subscribe notifies immediately if already set. Assert.IsTrue(latch.State.IsSet, 'Latch IsSet property mismatch (should be true for Null latch).'); Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should be notified immediately upon subscribing to an initially set latch.'); end; procedure TTestMycLatch.TestUnsubscribe_SubscriberNotNotified; var latch: TLatch.ILatch; mockSub: TMockSubscriber; subscription: TSignal.TSubscription; begin latch := TLatch.CreateLatch(1); mockSub := TMockSubscriber.Create; subscription := latch.State.Signal.Subscribe(mockSub); subscription.Unsubscribe; // Explicitly call Unsubscribe on the record latch.Notify; // Latch becomes set Assert.AreEqual(0, mockSub.NotifyCount, 'Unsubscribed MockSub should not be notified.'); end; // --- Category 5: Counter Integrity / State for Later Subscribers --- procedure TTestMycLatch.TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber; var latch: TLatch.ILatch; mockSub: TMockSubscriber; subscription: TSignal.TSubscription; begin latch := TLatch.CreateLatch(3); latch.Notify; // Count -> 2 latch.Notify; // Count -> 1 Assert.IsFalse(latch.State.IsSet, 'Latch should not be set yet after partial notifies.'); mockSub := TMockSubscriber.Create; subscription := latch.State.Signal.Subscribe(mockSub); Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified on subscribe if latch not yet set.'); latch.Notify; // Count -> 0, Latch becomes set and notifies mockSub Assert.IsTrue(latch.State.IsSet, 'Latch should now be set.'); Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified when latch becomes set.'); end; // --- Category 6: UnadviseAll Behavior on Latch Set --- procedure TTestMycLatch.TestLatchSet_ClearsSubscribers_NoFurtherNotification; var latch: TLatch.ILatch; mockSub1, mockSub2: TMockSubscriber; subscription1, subscription2: TSignal.TSubscription; begin latch := TLatch.CreateLatch(1); mockSub1 := TMockSubscriber.Create; mockSub2 := TMockSubscriber.Create; subscription1 := latch.State.Signal.Subscribe(mockSub1); subscription2 := latch.State.Signal.Subscribe(mockSub2); Assert.AreEqual(0, mockSub1.NotifyCount, 'MockSub1 initial NotifyCount should be 0.'); Assert.AreEqual(0, mockSub2.NotifyCount, 'MockSub2 initial NotifyCount should be 0.'); latch.Notify; // FCount becomes 0. Subscribers notified, then UnadviseAll called. Assert.IsTrue(latch.State.IsSet, 'Latch should be set.'); Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once when latch became set.'); Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should have been notified once when latch became set.'); latch.Notify; // Call Notify on the latch again (FCount becomes -1) Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should NOT be notified again after UnadviseAll.'); Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should NOT be notified again after UnadviseAll.'); end; procedure TTestMycLatch.TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll; var latch: TLatch.ILatch; mockSub: TMockSubscriber; // 'subscription' will be declared in an inner scope to control its finalization begin latch := TLatch.CreateLatch(1); mockSub := TMockSubscriber.Create; var noException: Boolean := True; try begin // Inner block to control lifetime of 'subscription' var subscription: TSignal.TSubscription; // Local to this block subscription := latch.State.Signal.Subscribe(mockSub); latch.Notify; // Sets the latch, mockSub is notified, UnadviseAll is called internally. Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub was notified when latch set.'); // When 'subscription' goes out of scope at the end of this 'begin..end' block, // its Finalize operator will be called, which in turn calls subscription.Unsubscribe. // This happens *after* UnadviseAll was triggered by latch.Notify. end; // <-- subscription.Finalize (and thus Unsubscribe) is called here. except on E: Exception do begin noException := False; // Optional: Log.Error('Exception in TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll: ' + E.Message); end; end; Assert.IsTrue(noException, 'Finalizing subscription after Latch set (and UnadviseAll) should not raise an exception.'); // Further check: calling Notify on latch again should not affect the original mockSub latch.Notify; Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should not be notified after its subscription was finalized post UnadviseAll.'); end; procedure TTestMycLatch.TestLatchSet_NewSubscriberToSetLatch_NotifiedImmediatelyButNotPersistedForLatchNotify; var latch: TLatch.ILatch; mockSubInitial, mockSubNew: TMockSubscriber; subscriptionInitial, subscriptionNew: TSignal.TSubscription; begin latch := TLatch.CreateLatch(1); mockSubInitial := TMockSubscriber.Create; subscriptionInitial := latch.State.Signal.Subscribe(mockSubInitial); // Set the latch; mockSubInitial is notified, UnadviseAll is called. latch.Notify; Assert.AreEqual(1, mockSubInitial.NotifyCount, 'Initial subscriber notified.'); // Subscribe a new subscriber to the already set latch mockSubNew := TMockSubscriber.Create; subscriptionNew := latch.State.Signal.Subscribe(mockSubNew); // New subscriber should be notified immediately upon subscription to an already set latch Assert.AreEqual(1, mockSubNew.NotifyCount, 'New subscriber should be notified immediately upon subscription.'); // The direct check of subscriptionNew.FTag was removed as FTag is private. // The correct behavior (no persistent subscription) is verified by the next step. // Call Notify on the latch again. latch.Notify; // mockSubInitial should not be notified again (was unadvised). Assert.AreEqual(1, mockSubInitial.NotifyCount, 'Initial subscriber (unadvised) not notified again.'); // mockSubNew should also not be notified again, as it was only notified immediately // and not persistently added to FSubscribers for subsequent Latch.Notify calls. Assert .AreEqual(1, mockSubNew.NotifyCount, 'New subscriber (notified once on subscribe) not notified by subsequent Latch.Notify calls.'); end; initialization TDUnitX.RegisterTestFixture(TTestMycLatch); end.