chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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"/> — the bookkeeping the EventPump
|
||||
/// uses to fan one OnDataChange event out to every driver subscription that
|
||||
/// observes the changed item handle.
|
||||
/// </summary>
|
||||
public sealed class SubscriptionRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void NextSubscriptionId_IsMonotonic()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.NextSubscriptionId().ShouldBe(1);
|
||||
registry.NextSubscriptionId().ShouldBe(2);
|
||||
registry.NextSubscriptionId().ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_OneSubscription_OneTag_ResolvesSingleSubscriber()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.Register(42, [new TagBindingAccess("Tank.Level", 100)]);
|
||||
|
||||
var subs = registry.ResolveSubscribers(100);
|
||||
subs.Count.ShouldBe(1);
|
||||
subs[0].SubscriptionId.ShouldBe(42);
|
||||
subs[0].FullReference.ShouldBe("Tank.Level");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_TwoSubscriptions_SameTag_FanOutToBoth()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.Register(1, [new TagBindingAccess("Tank.Level", 100)]);
|
||||
registry.Register(2, [new TagBindingAccess("Tank.Level", 100)]);
|
||||
|
||||
var subs = registry.ResolveSubscribers(100);
|
||||
subs.Count.ShouldBe(2);
|
||||
subs.Select(s => s.SubscriptionId).OrderBy(x => x).ShouldBe(new[] { 1L, 2L });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_FailedItemHandle_NotIndexedForFanOut()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.Register(1, [
|
||||
new TagBindingAccess("Good", 100),
|
||||
new TagBindingAccess("Bad", 0), // gw rejected this tag
|
||||
]);
|
||||
|
||||
registry.ResolveSubscribers(100).Count.ShouldBe(1);
|
||||
registry.ResolveSubscribers(0).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_DropsAllBindings_AndReturnsThemForUnsubscribe()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.Register(1, [
|
||||
new TagBindingAccess("A", 100),
|
||||
new TagBindingAccess("B", 200),
|
||||
]);
|
||||
|
||||
var removed = registry.Remove(1);
|
||||
|
||||
removed.ShouldNotBeNull();
|
||||
removed!.Count.ShouldBe(2);
|
||||
registry.ResolveSubscribers(100).ShouldBeEmpty();
|
||||
registry.ResolveSubscribers(200).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_OneOfTwoSubscriptions_LeavesOtherIntact()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.Register(1, [new TagBindingAccess("A", 100)]);
|
||||
registry.Register(2, [new TagBindingAccess("A", 100)]);
|
||||
|
||||
registry.Remove(1);
|
||||
|
||||
var subs = registry.ResolveSubscribers(100);
|
||||
subs.Count.ShouldBe(1);
|
||||
subs[0].SubscriptionId.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_UnknownSubscription_IsNullSentinel()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.Remove(999).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackedCounts_ReflectAdditionsAndRemovals()
|
||||
{
|
||||
var registry = new SubscriptionRegistryAccess();
|
||||
registry.TrackedSubscriptionCount.ShouldBe(0);
|
||||
|
||||
registry.Register(1, [new TagBindingAccess("A", 100)]);
|
||||
registry.Register(2, [new TagBindingAccess("A", 100), new TagBindingAccess("B", 200)]);
|
||||
registry.TrackedSubscriptionCount.ShouldBe(2);
|
||||
registry.TrackedItemHandleCount.ShouldBe(2);
|
||||
|
||||
registry.Remove(1);
|
||||
registry.TrackedSubscriptionCount.ShouldBe(1);
|
||||
registry.TrackedItemHandleCount.ShouldBe(2); // sub 2 still observes both handles
|
||||
|
||||
registry.Remove(2);
|
||||
registry.TrackedSubscriptionCount.ShouldBe(0);
|
||||
registry.TrackedItemHandleCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
// Internal types are accessed via friend assembly (InternalsVisibleTo); these
|
||||
// wrapper aliases keep the test code readable.
|
||||
private sealed class SubscriptionRegistryAccess
|
||||
{
|
||||
private readonly SubscriptionRegistry _inner = new();
|
||||
public int TrackedSubscriptionCount => _inner.TrackedSubscriptionCount;
|
||||
public int TrackedItemHandleCount => _inner.TrackedItemHandleCount;
|
||||
public long NextSubscriptionId() => _inner.NextSubscriptionId();
|
||||
public void Register(long id, IReadOnlyList<TagBindingAccess> bindings)
|
||||
=> _inner.Register(id, [.. bindings.Select(b => new TagBinding(b.FullReference, b.ItemHandle))]);
|
||||
public IReadOnlyList<TagBindingAccess>? Remove(long id)
|
||||
{
|
||||
var removed = _inner.Remove(id);
|
||||
return removed is null ? null : [.. removed.Select(b => new TagBindingAccess(b.FullReference, b.ItemHandle))];
|
||||
}
|
||||
public IReadOnlyList<(long SubscriptionId, string FullReference)> ResolveSubscribers(int handle)
|
||||
=> _inner.ResolveSubscribers(handle);
|
||||
}
|
||||
private sealed record TagBindingAccess(string FullReference, int ItemHandle);
|
||||
}
|
||||
Reference in New Issue
Block a user