feat(galaxy): SubscriptionRegistry.TryResolveItemHandle forward lookup
Add _itemHandleByFullRef (OrdinalIgnoreCase ConcurrentDictionary) maintained in lock-step with _subscribersByItemHandle across Register/Remove/Rebind. TryResolveItemHandle cross-checks the authoritative reverse map so a stale forward entry can never hand out a dead handle. Also wires the scaffolded _addItemCallCount increment in EnsureItemHandleAsync (field was declared but never assigned, causing a TreatWarningsAsErrors build failure on the branch). 8 new xUnit + Shouldly facts covering register/case-insensitive/remove/rebind/ failed-handle/liveness-guard paths.
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SubscriptionRegistry.TryResolveItemHandle"/> — 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.
|
||||
/// </summary>
|
||||
public sealed class SubscriptionRegistryHandleResolveTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that after registering a binding, TryResolveItemHandle returns the correct handle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_ThenResolve_ReturnsHandle()
|
||||
{
|
||||
var registry = new SubscriptionRegistry();
|
||||
registry.Register(1, [new TagBinding("Tag.A", 5)]);
|
||||
|
||||
registry.TryResolveItemHandle("Tag.A").ShouldBe(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that TryResolveItemHandle is case-insensitive on the full reference.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_ThenResolve_IsCaseInsensitive()
|
||||
{
|
||||
var registry = new SubscriptionRegistry();
|
||||
registry.Register(1, [new TagBinding("Tag.A", 5)]);
|
||||
|
||||
registry.TryResolveItemHandle("tag.a").ShouldBe(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a full reference that was never registered resolves to null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NeverRegistered_ReturnsNull()
|
||||
{
|
||||
var registry = new SubscriptionRegistry();
|
||||
|
||||
registry.TryResolveItemHandle("Tag.NotHere").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that after Remove(), the forward lookup returns null.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that after Rebind() the forward lookup returns the new handle, not the old one.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a binding with ItemHandle <= 0 (gateway-rejected) is not resolvable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FailedBinding_ZeroHandle_IsNotResolvable()
|
||||
{
|
||||
var registry = new SubscriptionRegistry();
|
||||
registry.Register(1, [new TagBinding("Tag.Failed", 0)]);
|
||||
|
||||
registry.TryResolveItemHandle("Tag.Failed").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a binding with a negative ItemHandle is not resolvable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FailedBinding_NegativeHandle_IsNotResolvable()
|
||||
{
|
||||
var registry = new SubscriptionRegistry();
|
||||
registry.Register(1, [new TagBinding("Tag.Failed", -1)]);
|
||||
|
||||
registry.TryResolveItemHandle("Tag.Failed").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user