using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// Tests for — the forward /// fullRef → live item-handle lookup the Galaxy writer uses to skip a redundant /// AddItem round-trip when an already-subscribed tag is written. /// public sealed class SubscriptionRegistryHandleResolveTests { /// /// Verifies that after registering a binding, TryResolveItemHandle returns the correct handle. /// [Fact] public void Register_ThenResolve_ReturnsHandle() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.TryResolveItemHandle("Tag.A").ShouldBe(5); } /// /// Verifies that TryResolveItemHandle is case-insensitive on the full reference. /// [Fact] public void Register_ThenResolve_IsCaseInsensitive() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.TryResolveItemHandle("tag.a").ShouldBe(5); } /// /// Verifies that a full reference that was never registered resolves to null. /// [Fact] public void NeverRegistered_ReturnsNull() { var registry = new SubscriptionRegistry(); registry.TryResolveItemHandle("Tag.NotHere").ShouldBeNull(); } /// /// Verifies that after Remove(), the forward lookup returns null. /// [Fact] public void Remove_ThenResolve_ReturnsNull() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.Remove(1); registry.TryResolveItemHandle("Tag.A").ShouldBeNull(); } /// /// Verifies that after Rebind() the forward lookup returns the new handle, not the old one. /// [Fact] public void Rebind_ThenResolve_ReturnsNewHandle() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.Rebind(1, [new TagBinding("Tag.A", 99)]); registry.TryResolveItemHandle("Tag.A").ShouldBe(99); } /// /// Verifies that a binding with ItemHandle <= 0 (gateway-rejected) is not resolvable. /// [Fact] public void FailedBinding_ZeroHandle_IsNotResolvable() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.Failed", 0)]); registry.TryResolveItemHandle("Tag.Failed").ShouldBeNull(); } /// /// Verifies that a binding with a negative ItemHandle is not resolvable. /// [Fact] public void FailedBinding_NegativeHandle_IsNotResolvable() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.Failed", -1)]); registry.TryResolveItemHandle("Tag.Failed").ShouldBeNull(); } /// /// Verifies the liveness guard: after the only subscriber of a handle is removed, /// the forward lookup returns null even if a stale forward-map entry lingers. /// [Fact] public void Remove_OnlySubscriber_LivenessGuard_ReturnsNull() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.Remove(1); // After removal the subscriber set for handle 5 is gone, so TryResolveItemHandle // must return null regardless of whether the forward entry was cleaned up. registry.TryResolveItemHandle("Tag.A").ShouldBeNull(); } /// /// A tag may legitimately appear in multiple driver subscriptions (separate OPC UA monitored /// items on the same Galaxy attribute) — they share one gw item handle. Removing ONE of them /// must keep the tag resolvable while another subscription still binds it, so the writer keeps /// borrowing instead of falling back to AddItem. /// [Fact] public void Remove_OneOfTwoSubscribersForSameRef_StillResolves() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.Register(2, [new TagBinding("Tag.A", 5)]); registry.Remove(1); // sub2 still binds Tag.A -> 5, so the borrow must still be offered. registry.TryResolveItemHandle("Tag.A").ShouldBe(5); } /// /// Authoritative resolution: if two DIFFERENT refs ever map to the same numeric handle and the /// subscription for one is removed, that ref must NOT resolve to the handle that now belongs to /// the other ref — a wrong-tag write would be the worst outcome. Resolution confirms a live /// subscription genuinely binds fullRef -> handle, not just that the handle is alive. /// [Fact] public void Remove_CrossRefSameHandle_DoesNotResolveToTheOtherRefsHandle() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tag.A", 5)]); registry.Register(2, [new TagBinding("Tag.B", 5)]); registry.Remove(1); // Tag.A's subscription is gone; handle 5 is still alive but now only Tag.B binds it. registry.TryResolveItemHandle("Tag.A").ShouldBeNull(); registry.TryResolveItemHandle("Tag.B").ShouldBe(5); } }