using System.Diagnostics; using 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; /// /// PR 6.1 — pins that every gw-facing call produces a span on the /// ZB.MOM.WW.OtOpcUa.Driver.Galaxy ActivitySource. We listen via /// rather than asserting on internal state, so the /// tests double as documentation of the listener-side contract. /// public sealed class GalaxyTelemetryTests { /// Subscribes an ActivityListener for the test, captures each spawned activity. private static (ActivityListener Listener, List Captured) StartCapture() { var captured = new List(); var listener = new ActivityListener { ShouldListenTo = src => src.Name == GalaxyTelemetry.ActivitySourceName, Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, ActivityStopped = activity => captured.Add(activity), }; ActivitySource.AddActivityListener(listener); return (listener, captured); } [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(); } } [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(() => 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(); } } [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(); } } [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 { public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) => Task.FromResult>( fullReferences.Select((r, i) => new SubscribeResult { TagAddress = r, ItemHandle = i + 1, WasSuccessful = true, }).ToList()); public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask; public async IAsyncEnumerable StreamEventsAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await Task.CompletedTask; yield break; } } private sealed class ThrowingSubscriber : IGalaxySubscriber { public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) => throw new InvalidOperationException("gw down"); public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask; public async IAsyncEnumerable StreamEventsAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await Task.CompletedTask; yield break; } } private sealed class RecordingWriter : IGalaxyDataWriter { public Task> WriteAsync( IReadOnlyList writes, Func securityResolver, CancellationToken cancellationToken) => Task.FromResult>( writes.Select(_ => new WriteResult(0u)).ToList()); } private sealed class FakeHierarchy : IGalaxyHierarchySource { public Task> GetHierarchyAsync( CancellationToken cancellationToken) => Task.FromResult>( [new(), new()]); } }