// TestSignals_Semapore.pas unit TestSignals_Semapore; interface uses DUnitX.TestFramework, Myc.Signals, // Unit under test System.SysUtils, System.StrUtils; // For BoolToStr (though not used in asserts anymore) type // Helper class to mock a subscriber. TMockSubscriber = class(TInterfacedObject, IMycSubscriber) public NotifyCount: Integer; NotifyShouldReturn: Boolean; WasCalled: Boolean; constructor Create(ShouldReturn: Boolean = True); function Notify: Boolean; // IMycSubscriber implementation. end; [TestFixture] TTestMycSemaphore = class(TObject) public [Setup] procedure Setup; [TearDown] procedure TearDown; // --- Category 1: Basic Counter and State Functionality (Parameterized) --- [TestCase('InitialPositive', '1,False')] [TestCase('InitialZero', '0,True')] [TestCase('InitialNegative', '-1,True')] [TestCase('InitialLargePositive', '5,False')] procedure TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean); [TestCase('DecrementPositiveToZero', '1,False,True')] [TestCase('DecrementPositiveToPositive', '2,True,False')] [TestCase('DecrementZeroToNegative', '0,False,True')] [TestCase('DecrementNegativeToNegative', '-1,False,True')] 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_IsNotifiedWhenSet; [Test] procedure TestSubscriber_Multiple_AreNotifiedWhenSet; [Test] procedure TestSubscriber_NotNotified_BeforeSet; [Test] procedure TestSubscriber_NotifiedOnlyOnce_AfterFinalization; [Test] procedure TestSubscribe_AfterFinalization_SubscriberNotNotified; // --- Category 3: Chaining and Cascading --- [Test] procedure TestChaining_A_Notifies_B; [Test] procedure TestCascade_A_Notifies_B_Notifies_C; [Test] procedure TestChaining_B_NeedsMultipleSignalsFromA_WithSubscriberOnB; // --- Category 4: Special Cases --- [Test] procedure TestInitiallySet_NotifiesNewSubscriberImmediately; [Test] procedure TestUnsubscribe_SubscriberNotNotified; // --- Category 5: Signal Loss / Counter Integrity --- [Test] procedure TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber; end; implementation { TMockSubscriber } constructor TMockSubscriber.Create(ShouldReturn: Boolean = True); begin inherited Create; NotifyCount := 0; NotifyShouldReturn := ShouldReturn; WasCalled := False; end; function TMockSubscriber.Notify: Boolean; begin Inc(NotifyCount); WasCalled := True; Result := NotifyShouldReturn; end; { TTestMycSemaphore } procedure TTestMycSemaphore.Setup; begin // Per-test setup code can go here (if any). end; procedure TTestMycSemaphore.TearDown; begin // Per-test teardown code can go here (if any). end; procedure TTestMycSemaphore.TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean); var semaphore: IMycSemaphore; begin semaphore := Signals.CreateSemaphore(InitialCount); Assert.AreEqual(ExpectedIsSet, semaphore.State.IsSet, 'Semaphore initial IsSet state mismatch.'); end; procedure TTestMycSemaphore.TestNotify_ReturnValueAndState_AfterOneNotify(InitialCount: Integer; ExpectedReturn: Boolean; ExpectedIsSet: Boolean); var semaphore: IMycSemaphore; returnedValue: Boolean; begin semaphore := Signals.CreateSemaphore(InitialCount); returnedValue := semaphore.Notify; Assert.AreEqual(ExpectedReturn, returnedValue, 'Unexpected return value after one Notify call.'); Assert.AreEqual(ExpectedIsSet, semaphore.State.IsSet, 'Unexpected IsSet state after one Notify call.'); end; procedure TTestMycSemaphore.TestNotify_DecrementsCounter_StateChanges; var semaphore: IMycSemaphore; begin semaphore := Signals.CreateSemaphore(1); Assert.IsFalse(semaphore.State.IsSet, 'Semaphore should initially not be set.'); semaphore.Notify; Assert.IsTrue(semaphore.State.IsSet, 'Semaphore should be set after Notify.'); end; procedure TTestMycSemaphore.TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet; var semaphore: IMycSemaphore; begin semaphore := Signals.CreateSemaphore(1); semaphore.Notify; Assert.IsTrue(semaphore.State.IsSet, 'Semaphore should be set after first Notify.'); semaphore.Notify; Assert.IsTrue(semaphore.State.IsSet, 'Semaphore should remain set after second Notify.'); end; procedure TTestMycSemaphore.TestSubscriber_Single_IsNotifiedWhenSet; var semaphore: IMycSemaphore; mockSub: TMockSubscriber; subscription: TMycSubscription; begin semaphore := Signals.CreateSemaphore(1); mockSub := TMockSubscriber.Create; subscription := semaphore.State.Subscribe(mockSub); Assert.IsFalse(semaphore.State.IsSet, 'Semaphore should initially not be set.'); Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not have been notified yet (before set).'); semaphore.Notify; Assert.IsTrue(semaphore.State.IsSet, 'Semaphore should be set after its Notify is called.'); Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified once when semaphore becomes set.'); end; procedure TTestMycSemaphore.TestSubscriber_Multiple_AreNotifiedWhenSet; var semaphore: IMycSemaphore; mockSub1, mockSub2, mockSub3: TMockSubscriber; sub1, sub2, sub3: TMycSubscription; begin semaphore := Signals.CreateSemaphore(1); mockSub1 := TMockSubscriber.Create; mockSub2 := TMockSubscriber.Create; mockSub3 := TMockSubscriber.Create; sub1 := semaphore.State.Subscribe(mockSub1); sub2 := semaphore.State.Subscribe(mockSub2); sub3 := semaphore.State.Subscribe(mockSub3); semaphore.Notify; 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 TTestMycSemaphore.TestSubscriber_NotNotified_BeforeSet; var semaphore: IMycSemaphore; mockSub: TMockSubscriber; subscription: TMycSubscription; begin semaphore := Signals.CreateSemaphore(2); mockSub := TMockSubscriber.Create; subscription := semaphore.State.Subscribe(mockSub); Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified upon subscription if semaphore is not set.'); semaphore.Notify; // Count becomes 1, still not set for subscriber notification (unless Subscribe was changed to notify if count becomes 0 internally). Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should still not be notified as semaphore is not yet set for subscribers.'); end; procedure TTestMycSemaphore.TestSubscriber_NotifiedOnlyOnce_AfterFinalization; var semaphore: IMycSemaphore; mockSub: TMockSubscriber; subscription: TMycSubscription; begin semaphore := Signals.CreateSemaphore(1); mockSub := TMockSubscriber.Create; subscription := semaphore.State.Subscribe(mockSub); semaphore.Notify; // Sets semaphore, notifies MockSub. Assumed TMycSignal's list is finalized by this action. Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should be 1 after first signal set.'); semaphore.Notify; // Trigger again. Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should remain 1 after re-triggering finalized signal.'); end; procedure TTestMycSemaphore.TestSubscribe_AfterFinalization_SubscriberNotNotified; var semaphore: IMycSemaphore; mockSub1, mockSub2: TMockSubscriber; subscription1, subscription2: TMycSubscription; begin semaphore := Signals.CreateSemaphore(1); mockSub1 := TMockSubscriber.Create; subscription1 := semaphore.State.Subscribe(mockSub1); semaphore.Notify; // Triggers semaphore. It becomes set. // Its TMycSignal part notifies mockSub1. // And its TMycSignal part is assumed to be finalized. Assert.IsTrue(semaphore.State.IsSet, 'Semaphore should be set after initial trigger.'); Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once.'); // Attempt to subscribe mockSub2 after the signal (TMycSignal part) has been finalized. mockSub2 := TMockSubscriber.Create; subscription2 := semaphore.State.Subscribe(mockSub2); // Current assertion in user-provided code expects mockSub2 to be notified. // This implies that Myc.Signals.Subscribe has specific logic to achieve this, // even if the underlying TMycNotifyList might have been finalized for mockSub1's notification. Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should be notified upon subscribing to an already set signal.'); semaphore.Notify; // Re-triggering the semaphore's count decrement. Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notify count should remain 1 (not notified again).'); // Current assertion in user-provided code expects mockSub2's count to also remain 1. Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notify count should remain 1 after re-triggering.'); end; procedure TTestMycSemaphore.TestChaining_A_Notifies_B; var semaA, semaB: IMycSemaphore; mockSubForB: TMockSubscriber; subHandle_B_listens_A, subHandle_Mock_listens_B: TMycSubscription; begin semaA := Signals.CreateSemaphore(1); semaB := Signals.CreateSemaphore(1); mockSubForB := TMockSubscriber.Create; subHandle_Mock_listens_B := semaB.State.Subscribe(mockSubForB); subHandle_B_listens_A := semaA.State.Subscribe(semaB); Assert.IsFalse(semaA.State.IsSet, 'SemaA initially not set.'); Assert.IsFalse(semaB.State.IsSet, 'SemaB initially not set.'); Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB initially not notified.'); semaA.Notify; Assert.IsTrue(semaA.State.IsSet, 'SemaA should be set after notify.'); Assert.IsTrue(semaB.State.IsSet, 'SemaB should be set after being notified by SemaA.'); Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by SemaB.'); end; procedure TTestMycSemaphore.TestCascade_A_Notifies_B_Notifies_C; var semaA, semaB, semaC: IMycSemaphore; mockSubForC: TMockSubscriber; sub_Mock_C, sub_C_B, sub_B_A: TMycSubscription; begin semaA := Signals.CreateSemaphore(1); semaB := Signals.CreateSemaphore(1); semaC := Signals.CreateSemaphore(1); mockSubForC := TMockSubscriber.Create; sub_Mock_C := semaC.State.Subscribe(mockSubForC); sub_C_B := semaB.State.Subscribe(semaC); sub_B_A := semaA.State.Subscribe(semaB); semaA.Notify; Assert.IsTrue(semaA.State.IsSet, 'SemaA state mismatch in cascade.'); Assert.IsTrue(semaB.State.IsSet, 'SemaB state mismatch in cascade.'); Assert.IsTrue(semaC.State.IsSet, 'SemaC state mismatch in cascade.'); Assert.AreEqual(1, mockSubForC.NotifyCount, 'MockSubForC notification count mismatch in cascade.'); end; procedure TTestMycSemaphore.TestChaining_B_NeedsMultipleSignalsFromA_WithSubscriberOnB; var semaA, semaB: IMycSemaphore; mockSubForB: TMockSubscriber; sub_Mock_B, sub_B_A: TMycSubscription; begin semaA := Signals.CreateSemaphore(2); semaB := Signals.CreateSemaphore(1); mockSubForB := TMockSubscriber.Create; sub_Mock_B := semaB.State.Subscribe(mockSubForB); sub_B_A := semaA.State.Subscribe(semaB); semaA.Notify; // First trigger for SemaA Assert.IsFalse(semaA.State.IsSet, 'SemaA should not be set after first trigger of two.'); Assert.IsFalse(semaB.State.IsSet, 'SemaB should not have been triggered yet by SemaA.'); Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB should not have been notified yet.'); semaA.Notify; // Second trigger for SemaA Assert.IsTrue(semaA.State.IsSet, 'SemaA should be set after second trigger.'); Assert.IsTrue(semaB.State.IsSet, 'SemaB should now be set by SemaA.'); Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by SemaB.'); end; procedure TTestMycSemaphore.TestInitiallySet_NotifiesNewSubscriberImmediately; var semaphore: IMycSemaphore; mockSub: TMockSubscriber; subscription: TMycSubscription; begin semaphore := Signals.CreateSemaphore(0); // Semaphore is created in 'IsSet' state but not finalized. mockSub := TMockSubscriber.Create; // Assumes TMycSignal.Subscribe is modified to implement immediate notification for set signals. subscription := semaphore.State.Subscribe(mockSub); Assert.IsTrue(semaphore.State.IsSet, 'Semaphore IsSet property mismatch after creation with count 0.'); Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should be notified immediately upon subscribing to an already set semaphore.'); end; procedure TTestMycSemaphore.TestUnsubscribe_SubscriberNotNotified; var semaphore: IMycSemaphore; mockSub: TMockSubscriber; subscription: TMycSubscription; begin semaphore := Signals.CreateSemaphore(1); mockSub := TMockSubscriber.Create; subscription := semaphore.State.Subscribe(mockSub); subscription := Default(TMycSubscription); // Simulates leaving scope / destruction, calls Finalize for unsubscription. semaphore.Notify; Assert.AreEqual(0, mockSub.NotifyCount, 'Unsubscribed MockSub should not be notified.'); end; procedure TTestMycSemaphore.TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber; var semaphore: IMycSemaphore; mockSub: TMockSubscriber; subscription: TMycSubscription; begin semaphore := Signals.CreateSemaphore(3); semaphore.Notify; // Count -> 2 semaphore.Notify; // Count -> 1 Assert.IsFalse(semaphore.State.IsSet, 'Semaphore should not be set yet after partial triggers.'); mockSub := TMockSubscriber.Create; subscription := semaphore.State.Subscribe(mockSub); // Assumes TMycSignal.Subscribe does not notify immediately if signal is not yet set. Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified on subscribe if semaphore not yet set.'); semaphore.Notify; // Count -> 0, Semaphore becomes set and notifies mockSub Assert.IsTrue(semaphore.State.IsSet, 'Semaphore should now be set.'); Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified when semaphore becomes set.'); end; initialization // Optional: Register test cases if not using automatic registration. // TDUnitX.RegisterTestFixture(TTestMycSemaphore); end.