Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GalaxyTelemetryTests.cs
T
Joseph Doherty 560b327ee1
v2-ci / build (push) Failing after 33s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
refactor(galaxy): migrate to ZB.MOM.WW.MxGateway.* nupkg packages
Imports the freshly-rebuilt ZB.MOM.WW.MxGateway.Client + ZB.MOM.WW.MxGateway.Contracts
nupkgs (0.1.0) from /tmp/mxgw-dist. Replaces the vendored libs/ DLLs and the
pre-restructure MxGateway.* namespaces across the runtime Galaxy driver,
Galaxy.Browser, and their tests.

Key changes:
- nuget-packages/ added as a local feed via NuGet.config; .gitignore exempts it
  from the *.nupkg rule so the packages are tracked
- Directory.Packages.props pins both packages at 0.1.0
- 4 csprojs swap <Reference HintPath="libs/...dll"/> for <PackageReference/>
- 36 .cs files renamed `using MxGateway.*` -> `using ZB.MOM.WW.MxGateway.*`
- libs/ removed (vendored DLLs + README.md)

GalaxyBrowseSession rewritten around the new lazy API:
- RootAsync calls GalaxyRepositoryClient.BrowseAsync (returns LazyBrowseNodes)
  and caches them by TagName instead of bulk-fetching the whole hierarchy
- ExpandAsync looks up the cached LazyBrowseNode and calls its ExpandAsync,
  giving true one-wire-call-per-click instead of in-memory parent/child scan
- _byGobjectId + _hasChildrenSet dropped (LazyBrowseNode carries HasChildrenHint)
- AttributesAsync unchanged (already uses DiscoverHierarchyAsync MaxDepth=0)

Tests: Galaxy.Tests 245/245, Galaxy.Browser.Tests 10/10, AdminUI.Tests 66/66.
Pre-existing 12 solution errors unchanged (test sinks + Cli XML comments).
2026-05-29 07:14:18 -04:00

192 lines
7.8 KiB
C#

using System.Diagnostics;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
/// <summary>
/// PR 6.1 — pins that every gw-facing call produces a span on the
/// <c>ZB.MOM.WW.OtOpcUa.Driver.Galaxy</c> ActivitySource. We listen via
/// <see cref="ActivityListener"/> rather than asserting on internal state, so the
/// tests double as documentation of the listener-side contract.
/// </summary>
public sealed class GalaxyTelemetryTests
{
/// <summary>Subscribes an ActivityListener for the test, captures each spawned activity.</summary>
private static (ActivityListener Listener, List<Activity> Captured) StartCapture()
{
var captured = new List<Activity>();
var listener = new ActivityListener
{
ShouldListenTo = src => src.Name == GalaxyTelemetry.ActivitySourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity => captured.Add(activity),
};
ActivitySource.AddActivityListener(listener);
return (listener, captured);
}
/// <summary>Verifies that TracedGalaxySubscriber emits a subscribe_bulk span with tag count.</summary>
[Fact]
public async Task TracedGalaxySubscriber_emits_subscribe_bulk_span_with_tag_count()
{
var (listener, captured) = StartCapture();
try
{
var inner = new FakeSubscriber();
var sut = new TracedGalaxySubscriber(inner, "OtOpcUa-Test");
await sut.SubscribeBulkAsync(["A", "B", "C"], 500, CancellationToken.None);
var span = captured.ShouldHaveSingleItem();
span.OperationName.ShouldBe("galaxy.subscribe_bulk");
span.GetTagItem("galaxy.client").ShouldBe("OtOpcUa-Test");
span.GetTagItem("galaxy.tag_count").ShouldBe(3);
span.GetTagItem("galaxy.buffered_interval_ms").ShouldBe(500);
span.GetTagItem("galaxy.success_count").ShouldBe(3);
}
finally { listener.Dispose(); }
}
/// <summary>Verifies that TracedGalaxySubscriber records error and rethrows on failure.</summary>
[Fact]
public async Task TracedGalaxySubscriber_records_error_and_rethrows_on_failure()
{
var (listener, captured) = StartCapture();
try
{
var sut = new TracedGalaxySubscriber(new ThrowingSubscriber(), "OtOpcUa-Test");
await Should.ThrowAsync<InvalidOperationException>(() =>
sut.SubscribeBulkAsync(["A"], 0, CancellationToken.None));
var span = captured.ShouldHaveSingleItem();
span.Status.ShouldBe(ActivityStatusCode.Error);
span.GetTagItem("exception.type").ShouldBe(typeof(InvalidOperationException).FullName);
}
finally { listener.Dispose(); }
}
/// <summary>Verifies that TracedGalaxyDataWriter tags the secured write count.</summary>
[Fact]
public async Task TracedGalaxyDataWriter_tags_secured_write_count()
{
var (listener, captured) = StartCapture();
try
{
var inner = new RecordingWriter();
var sut = new TracedGalaxyDataWriter(inner, "OtOpcUa-Test");
var requests = new[]
{
new WriteRequest("FreeTag", 1.0),
new WriteRequest("OperateTag", 2.0),
new WriteRequest("TuneTag", 3.0),
new WriteRequest("ConfigTag", 4.0),
};
SecurityClassification Resolver(string fullRef) => fullRef switch
{
"FreeTag" => SecurityClassification.FreeAccess,
"OperateTag" => SecurityClassification.Operate,
"TuneTag" => SecurityClassification.Tune,
"ConfigTag" => SecurityClassification.Configure,
_ => SecurityClassification.FreeAccess,
};
await sut.WriteAsync(requests, Resolver, CancellationToken.None);
var span = captured.ShouldHaveSingleItem();
span.OperationName.ShouldBe("galaxy.write");
span.GetTagItem("galaxy.tag_count").ShouldBe(4);
span.GetTagItem("galaxy.secured_write_count").ShouldBe(2); // Tune + Configure
}
finally { listener.Dispose(); }
}
/// <summary>Verifies that TracedGalaxyHierarchySource tags the object count.</summary>
[Fact]
public async Task TracedGalaxyHierarchySource_tags_object_count()
{
var (listener, captured) = StartCapture();
try
{
var sut = new TracedGalaxyHierarchySource(new FakeHierarchy(), "OtOpcUa-Test");
var hierarchy = await sut.GetHierarchyAsync(CancellationToken.None);
hierarchy.Count.ShouldBe(2);
var span = captured.ShouldHaveSingleItem();
span.OperationName.ShouldBe("galaxy.get_hierarchy");
span.GetTagItem("galaxy.object_count").ShouldBe(2);
}
finally { listener.Dispose(); }
}
private sealed class FakeSubscriber : IGalaxySubscriber
{
/// <inheritdoc />
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<SubscribeResult>>(
fullReferences.Select((r, i) => new SubscribeResult
{
TagAddress = r,
ItemHandle = i + 1,
WasSuccessful = true,
}).ToList());
/// <inheritdoc />
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
=> Task.CompletedTask;
/// <inheritdoc />
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;
yield break;
}
}
private sealed class ThrowingSubscriber : IGalaxySubscriber
{
/// <inheritdoc />
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
=> throw new InvalidOperationException("gw down");
/// <inheritdoc />
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
=> Task.CompletedTask;
/// <inheritdoc />
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;
yield break;
}
}
private sealed class RecordingWriter : IGalaxyDataWriter
{
/// <inheritdoc />
public Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes,
Func<string, SecurityClassification> securityResolver,
CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<WriteResult>>(
writes.Select(_ => new WriteResult(0u)).ToList());
}
private sealed class FakeHierarchy : IGalaxyHierarchySource
{
/// <inheritdoc />
public Task<IReadOnlyList<ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject>> GetHierarchyAsync(
CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject>>(
[new(), new()]);
}
}