docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
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
v2-ci / build (push) Failing after 1m43s
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
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
This commit is contained in:
+24
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class EventPumpBoundedChannelTests
|
||||
{
|
||||
/// <summary>Verifies that the event pump drops newest events when the bounded channel fills and records metrics for dropped events.</summary>
|
||||
[Fact]
|
||||
public async Task Drops_newest_when_channel_fills_and_records_metric()
|
||||
{
|
||||
@@ -66,6 +67,7 @@ public sealed class EventPumpBoundedChannelTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the event pump throws an exception when the channel capacity is invalid.</summary>
|
||||
[Fact]
|
||||
public async Task Throws_when_channelCapacity_is_invalid()
|
||||
{
|
||||
@@ -78,6 +80,7 @@ public sealed class EventPumpBoundedChannelTests
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that event pump metrics are tagged with the client name for tracking multiple driver hosts.</summary>
|
||||
[Fact]
|
||||
public async Task Tags_metrics_with_client_name_for_multi_driver_hosts()
|
||||
{
|
||||
@@ -169,10 +172,15 @@ public sealed class EventPumpBoundedChannelTests
|
||||
{
|
||||
public MeterListener? Listener;
|
||||
internal long _received, _dispatched, _dropped;
|
||||
/// <summary>Gets the count of received events.</summary>
|
||||
public long Received => Interlocked.Read(ref _received);
|
||||
/// <summary>Gets the count of dispatched events.</summary>
|
||||
public long Dispatched => Interlocked.Read(ref _dispatched);
|
||||
/// <summary>Gets the count of dropped events.</summary>
|
||||
public long Dropped => Interlocked.Read(ref _dropped);
|
||||
/// <summary>Gets the count of in-flight events.</summary>
|
||||
public long InFlight => Math.Max(0, Received - Dispatched - Dropped);
|
||||
/// <summary>Disposes the meter listener.</summary>
|
||||
public void Dispose() => Listener?.Dispose();
|
||||
}
|
||||
|
||||
@@ -181,16 +189,32 @@ public sealed class EventPumpBoundedChannelTests
|
||||
private readonly Channel<MxEvent> _stream =
|
||||
Channel.CreateUnbounded<MxEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
/// <summary>Subscribes to a bulk list of tag references.</summary>
|
||||
/// <param name="fullReferences">The list of full references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An empty result list.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
|
||||
|
||||
/// <summary>Unsubscribes from a bulk list of item handles.</summary>
|
||||
/// <param name="itemHandles">The list of item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams events asynchronously.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An async enumerable of MxEvent objects.</returns>
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
/// <summary>Emits a test event with the specified item handle and value.</summary>
|
||||
/// <param name="itemHandle">The item handle.</param>
|
||||
/// <param name="value">The event value.</param>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask EmitAsync(int itemHandle, double value) =>
|
||||
_stream.Writer.WriteAsync(new MxEvent
|
||||
{
|
||||
|
||||
+26
@@ -18,6 +18,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
{
|
||||
private const int WaitMs = 2_000;
|
||||
|
||||
/// <summary>Verifies that stream fault invokes the callback with the exception.</summary>
|
||||
[Fact]
|
||||
public async Task StreamFault_InvokesOnStreamFaultCallback_WithTheCause()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
(await faultObserved.Task).ShouldBeOfType<IOException>();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stream fault drives the reconnect supervisor through reopen and replay.</summary>
|
||||
[Fact]
|
||||
public async Task StreamFault_DrivesReconnectSupervisorReopenReplay()
|
||||
{
|
||||
@@ -76,6 +78,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
supervisor.IsDegraded.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a faulted pump cannot be restarted in place, but a fresh pump resumes dispatch.</summary>
|
||||
[Fact]
|
||||
public async Task FaultedPump_IsNotRestartableInPlace_ButAFreshPumpResumesDispatch()
|
||||
{
|
||||
@@ -118,6 +121,7 @@ public sealed class EventPumpStreamFaultTests
|
||||
staleObserved.ShouldBeFalse("the faulted pump must not dispatch after its stream dropped");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clean shutdown does not invoke the stream fault callback.</summary>
|
||||
[Fact]
|
||||
public async Task CleanShutdown_DoesNotInvokeOnStreamFault()
|
||||
{
|
||||
@@ -146,17 +150,27 @@ public sealed class EventPumpStreamFaultTests
|
||||
private readonly Channel<MxEvent> _stream =
|
||||
Channel.CreateUnbounded<MxEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
/// <summary>Subscribes to multiple tags (test stub).</summary>
|
||||
/// <param name="fullReferences">The tag references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
|
||||
|
||||
/// <summary>Unsubscribes from multiple tags (test stub).</summary>
|
||||
/// <param name="itemHandles">The item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams events asynchronously (test stub).</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
/// <summary>Fault the stream so the pump's <c>await foreach</c> throws.</summary>
|
||||
/// <param name="cause">The exception to complete the stream with.</param>
|
||||
public void FaultStream(Exception cause) => _stream.Writer.TryComplete(cause);
|
||||
}
|
||||
|
||||
@@ -169,16 +183,28 @@ public sealed class EventPumpStreamFaultTests
|
||||
private readonly Channel<MxEvent> _stream =
|
||||
Channel.CreateUnbounded<MxEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
/// <summary>Subscribes to multiple tags (test stub).</summary>
|
||||
/// <param name="fullReferences">The tag references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
|
||||
|
||||
/// <summary>Unsubscribes from multiple tags (test stub).</summary>
|
||||
/// <param name="itemHandles">The item handles to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Streams events asynchronously (test stub).</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
/// <summary>Emits a data change event asynchronously.</summary>
|
||||
/// <param name="itemHandle">The item handle for the data change.</param>
|
||||
/// <param name="value">The numeric value of the change.</param>
|
||||
public ValueTask EmitAsync(int itemHandle, double value) =>
|
||||
_stream.Writer.WriteAsync(new MxEvent
|
||||
{
|
||||
|
||||
@@ -23,7 +23,9 @@ public sealed class GalaxyDriverReadTests
|
||||
|
||||
private sealed class FakeReader : IGalaxyDataReader
|
||||
{
|
||||
/// <summary>Gets the last read request.</summary>
|
||||
public IReadOnlyList<string>? LastRequest { get; private set; }
|
||||
/// <summary>Gets or sets the function that decides the result for a given tag list.</summary>
|
||||
public Func<IReadOnlyList<string>, IReadOnlyList<DataValueSnapshot>> Decide { get; set; } =
|
||||
tags => tags.Select(t => new DataValueSnapshot(
|
||||
Value: t,
|
||||
@@ -31,6 +33,7 @@ public sealed class GalaxyDriverReadTests
|
||||
SourceTimestampUtc: DateTime.UtcNow,
|
||||
ServerTimestampUtc: DateTime.UtcNow)).ToArray();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public sealed class GalaxyDriverReadTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync routes through the injected reader.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_RoutesThroughInjectedReader()
|
||||
{
|
||||
@@ -53,6 +57,7 @@ public sealed class GalaxyDriverReadTests
|
||||
result[0].StatusCode.ShouldBe(StatusCodeMap.Good);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync returns empty without calling the reader for an empty request.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_EmptyRequest_ReturnsEmpty_WithoutCallingReader()
|
||||
{
|
||||
@@ -65,6 +70,7 @@ public sealed class GalaxyDriverReadTests
|
||||
reader.LastRequest.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync throws when seams and production runtime are not built.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_NoSeams_AndNoProductionRuntime_Throws()
|
||||
{
|
||||
@@ -79,6 +85,7 @@ public sealed class GalaxyDriverReadTests
|
||||
ex.Message.ShouldContain("production runtime not built");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync throws after the driver is disposed.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_AfterDispose_Throws()
|
||||
{
|
||||
@@ -88,6 +95,7 @@ public sealed class GalaxyDriverReadTests
|
||||
driver.ReadAsync(["x"], CancellationToken.None));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync resolves from the first OnDataChange event on the subscribe-once path.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_SubscribeOncePath_ResolvesFromFirstOnDataChange()
|
||||
{
|
||||
@@ -111,6 +119,7 @@ public sealed class GalaxyDriverReadTests
|
||||
subscriber.UnsubscribedHandles.ShouldContain(itemHandle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync surfaces rejected tags as bad status on the subscribe-once path.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_SubscribeOncePath_RejectedTagSurfacesAsBadStatus()
|
||||
{
|
||||
@@ -130,6 +139,7 @@ public sealed class GalaxyDriverReadTests
|
||||
result[1].StatusCode.ShouldBe(0x80000000u); // Bad
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ReadAsync preserves reader status codes.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_PreservesReaderStatusCodes()
|
||||
{
|
||||
|
||||
+37
@@ -27,11 +27,24 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
{
|
||||
private int _nextHandle = 1;
|
||||
private readonly Channel<MxEvent> _events = Channel.CreateUnbounded<MxEvent>();
|
||||
|
||||
/// <summary>Gets the mapping of tag references to subscription handles.</summary>
|
||||
public Dictionary<string, int> Map { get; } = new();
|
||||
|
||||
/// <summary>Gets the list of unsubscribed handles.</summary>
|
||||
public List<int> UnsubscribedHandles { get; } = [];
|
||||
|
||||
/// <summary>Gets the list of buffered intervals called.</summary>
|
||||
public List<int> BufferedIntervalsCalled { get; } = [];
|
||||
|
||||
/// <summary>Gets or sets a function to decide whether to accept a subscription.</summary>
|
||||
public Func<string, bool> Decide { get; set; } = _ => true;
|
||||
|
||||
/// <summary>Subscribes to bulk updates for the specified tag references.</summary>
|
||||
/// <param name="fullReferences">The tag references to subscribe to.</param>
|
||||
/// <param name="bufferedUpdateIntervalMs">The buffered update interval in milliseconds.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A list of subscription results.</returns>
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -64,15 +77,27 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from bulk updates for the specified item handles.</summary>
|
||||
/// <param name="itemHandles">The handles to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
{
|
||||
UnsubscribedHandles.AddRange(itemHandles);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Streams events asynchronously.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>An async enumerable of MX events.</returns>
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _events.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
/// <summary>Emits a data change event asynchronously.</summary>
|
||||
/// <param name="itemHandle">The handle of the item that changed.</param>
|
||||
/// <param name="value">The new value.</param>
|
||||
/// <param name="quality">The quality of the value.</param>
|
||||
/// <returns>A value task representing the asynchronous emission.</returns>
|
||||
public ValueTask EmitOnDataChangeAsync(int itemHandle, double value, byte quality = 192) =>
|
||||
_events.Writer.WriteAsync(new MxEvent
|
||||
{
|
||||
@@ -83,9 +108,11 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
SourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
|
||||
});
|
||||
|
||||
/// <summary>Completes the event stream.</summary>
|
||||
public void CompleteEvents() => _events.Writer.Complete();
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription allocates a handle and dispatches value changes.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_AllocatesHandle_AndDispatchesValueChange()
|
||||
{
|
||||
@@ -108,6 +135,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
((double)captured[0].Snapshot.Value!).ShouldBe(42.0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies two subscriptions for the same tag each receive updates.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_TwoSubscriptions_SameTag_FanOutOnePerSubscription()
|
||||
{
|
||||
@@ -151,6 +179,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
allowed.ShouldContain(captured0Id);
|
||||
}
|
||||
|
||||
/// <summary>Verifies failed subscriptions do not dispatch events.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_FailedTag_DoesNotDispatchEvents()
|
||||
{
|
||||
@@ -171,6 +200,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
captured.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies unsubscribe removes registration and calls gateway unsubscribe.</summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_RemovesRegistration_AndCallsGwUnsubscribe()
|
||||
{
|
||||
@@ -193,6 +223,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
captured.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies unsubscribing with an unknown handle is handled.</summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_UnknownHandle_NoOp()
|
||||
{
|
||||
@@ -207,6 +238,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
driver.UnsubscribeAsync(foreignHandle, CancellationToken.None));
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription without a subscriber throws.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_NoSubscriber_Throws()
|
||||
{
|
||||
@@ -216,6 +248,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
ex.Message.ShouldContain("PR 4.W");
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription falls back to configured interval when zero is passed.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_FallsBackToConfiguredInterval_WhenCallerPassesZero()
|
||||
{
|
||||
@@ -235,6 +268,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
subscriber.BufferedIntervalsCalled.ShouldHaveSingleItem().ShouldBe(750);
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription respects caller's interval when non-zero.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_RespectsCallerInterval_WhenNonZero()
|
||||
{
|
||||
@@ -254,6 +288,7 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
subscriber.BufferedIntervalsCalled.ShouldHaveSingleItem().ShouldBe(250);
|
||||
}
|
||||
|
||||
/// <summary>Verifies subscription with empty tag list returns handle without calling gateway.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_EmptyTagList_ReturnsHandleWithoutCallingGw()
|
||||
{
|
||||
@@ -266,8 +301,10 @@ public sealed class GalaxyDriverSubscribeTests
|
||||
subscriber.Map.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>A subscription handle from a foreign source.</summary>
|
||||
private sealed class ForeignHandle : ISubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this handle.</summary>
|
||||
public string DiagnosticId => "foreign-x";
|
||||
}
|
||||
|
||||
|
||||
+36
-1
@@ -26,34 +26,61 @@ public sealed class GalaxyDriverWriteTests
|
||||
|
||||
private sealed class FakeHierarchySource(IReadOnlyList<GalaxyObject> objects) : IGalaxyHierarchySource
|
||||
{
|
||||
/// <summary>Returns the fake Galaxy object hierarchy.</summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(objects);
|
||||
}
|
||||
|
||||
private sealed class FakeBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
/// <summary>Gets the list of variables added to this builder.</summary>
|
||||
public List<DriverAttributeInfo> Variables { get; } = [];
|
||||
|
||||
/// <summary>Adds a folder and returns this builder for chaining.</summary>
|
||||
/// <param name="browseName">The browse name of the folder.</param>
|
||||
/// <param name="displayName">The display name of the folder.</param>
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
||||
/// <summary>Adds a variable to the variables list and returns a handle.</summary>
|
||||
/// <param name="browseName">The browse name of the variable.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="attributeInfo">The attribute information for the variable.</param>
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
{
|
||||
Variables.Add(attributeInfo);
|
||||
return new FakeHandle(attributeInfo.FullName);
|
||||
}
|
||||
/// <summary>No-op property adding operation for test compatibility.</summary>
|
||||
/// <param name="browseName">The browse name of the property.</param>
|
||||
/// <param name="dataType">The data type of the property.</param>
|
||||
/// <param name="value">The value of the property.</param>
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
|
||||
|
||||
private sealed class FakeHandle(string fullRef) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference for this variable handle.</summary>
|
||||
public string FullReference { get; } = fullRef;
|
||||
/// <summary>Marks this variable as an alarm condition and returns a noop sink.</summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NoopSink();
|
||||
private sealed class NoopSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } }
|
||||
/// <summary>No-op alarm transition handler.</summary>
|
||||
private sealed class NoopSink : IAlarmConditionSink {
|
||||
/// <summary>Handles alarm state transition events.</summary>
|
||||
/// <param name="args">The alarm event arguments.</param>
|
||||
public void OnTransition(AlarmEventArgs args) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeWriter : IGalaxyDataWriter
|
||||
{
|
||||
/// <summary>Gets the list of write calls received by this writer.</summary>
|
||||
public List<(string FullRef, object? Value, SecurityClassification Resolved)> Calls { get; } = [];
|
||||
|
||||
/// <summary>Records write requests with their resolved security classifications.</summary>
|
||||
/// <param name="writes">The list of write requests to process.</param>
|
||||
/// <param name="securityResolver">Function to resolve security classification for each request.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
public Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
Func<string, SecurityClassification> securityResolver,
|
||||
@@ -79,6 +106,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
return o;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync routes through the injected writer and propagates values correctly.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_RoutesThroughInjectedWriter_AndPropagatesValues()
|
||||
{
|
||||
@@ -102,6 +130,9 @@ public sealed class GalaxyDriverWriteTests
|
||||
writer.Calls[1].Resolved.ShouldBe(SecurityClassification.Operate);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync resolves every security classification from discovery data.</summary>
|
||||
/// <param name="mxSec">The raw MXAccess security integer from the discovery attribute.</param>
|
||||
/// <param name="expected">The expected resolved security classification.</param>
|
||||
[Theory]
|
||||
[InlineData(0, SecurityClassification.FreeAccess)]
|
||||
[InlineData(1, SecurityClassification.Operate)]
|
||||
@@ -125,6 +156,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
writer.Calls[0].Resolved.ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown tags resolve to FreeAccess classification and writes proceed.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_UnknownTag_ResolvesToFreeAccess_DefaultsToWrite()
|
||||
{
|
||||
@@ -139,6 +171,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
writer.Calls[0].Resolved.ShouldBe(SecurityClassification.FreeAccess);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty write request returns empty without calling the writer.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_EmptyRequest_ReturnsEmpty_WithoutCallingWriter()
|
||||
{
|
||||
@@ -152,6 +185,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
writer.Calls.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync throws when no writer is configured, referencing PR 4.4.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_NoWriter_Throws_PointingAtPR44()
|
||||
{
|
||||
@@ -162,6 +196,7 @@ public sealed class GalaxyDriverWriteTests
|
||||
ex.Message.ShouldContain("PR 4.4");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that WriteAsync throws ObjectDisposedException after the driver is disposed.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_AfterDispose_Throws()
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ public sealed class GalaxyTelemetryTests
|
||||
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()
|
||||
{
|
||||
@@ -50,6 +51,7 @@ public sealed class GalaxyTelemetryTests
|
||||
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()
|
||||
{
|
||||
@@ -67,6 +69,7 @@ public sealed class GalaxyTelemetryTests
|
||||
finally { listener.Dispose(); }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TracedGalaxyDataWriter tags the secured write count.</summary>
|
||||
[Fact]
|
||||
public async Task TracedGalaxyDataWriter_tags_secured_write_count()
|
||||
{
|
||||
@@ -102,6 +105,7 @@ public sealed class GalaxyTelemetryTests
|
||||
finally { listener.Dispose(); }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TracedGalaxyHierarchySource tags the object count.</summary>
|
||||
[Fact]
|
||||
public async Task TracedGalaxyHierarchySource_tags_object_count()
|
||||
{
|
||||
@@ -121,6 +125,7 @@ public sealed class GalaxyTelemetryTests
|
||||
|
||||
private sealed class FakeSubscriber : IGalaxySubscriber
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<SubscribeResult>>(
|
||||
@@ -131,9 +136,11 @@ public sealed class GalaxyTelemetryTests
|
||||
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)
|
||||
{
|
||||
@@ -144,13 +151,16 @@ public sealed class GalaxyTelemetryTests
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -161,6 +171,7 @@ public sealed class GalaxyTelemetryTests
|
||||
|
||||
private sealed class RecordingWriter : IGalaxyDataWriter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
Func<string, SecurityClassification> securityResolver,
|
||||
@@ -171,6 +182,7 @@ public sealed class GalaxyTelemetryTests
|
||||
|
||||
private sealed class FakeHierarchy : IGalaxyHierarchySource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<MxGateway.Contracts.Proto.Galaxy.GalaxyObject>> GetHierarchyAsync(
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<MxGateway.Contracts.Proto.Galaxy.GalaxyObject>>(
|
||||
|
||||
+3
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class GatewayGalaxyAlarmFeedTests
|
||||
{
|
||||
/// <summary>Verifies that the feed decodes active alarm snapshots and live transitions.</summary>
|
||||
[Fact]
|
||||
public async Task Decodes_active_alarm_snapshot_then_live_transition()
|
||||
{
|
||||
@@ -68,6 +69,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
|
||||
observed[2].OriginalRaiseTimestampUtc.ShouldBe(raise);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the feed drops transitions with unspecified kind and empty messages.</summary>
|
||||
[Fact]
|
||||
public async Task Drops_transition_with_unspecified_kind_and_empty_message()
|
||||
{
|
||||
@@ -104,6 +106,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
|
||||
observed[0].SeverityBucket.ShouldBe(AlarmSeverity.High);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the feed reopens the stream after a transport fault.</summary>
|
||||
[Fact]
|
||||
public async Task Reopens_stream_after_a_transport_fault()
|
||||
{
|
||||
|
||||
+5
@@ -13,6 +13,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class MxAccessSeverityMapperTests
|
||||
{
|
||||
/// <summary>Verifies that Map assigns the expected severity bucket and OPC UA severity value.</summary>
|
||||
/// <param name="rawMxAccessSeverity">The raw MxAccess severity integer to map.</param>
|
||||
/// <param name="expectedBucket">The expected AlarmSeverity bucket.</param>
|
||||
/// <param name="expectedOpcUaSeverity">The expected OPC UA numeric severity value.</param>
|
||||
[Theory]
|
||||
[InlineData(0, AlarmSeverity.Low, MxAccessSeverityMapper.OpcUaSeverityLow)]
|
||||
[InlineData(1, AlarmSeverity.Low, MxAccessSeverityMapper.OpcUaSeverityLow)]
|
||||
@@ -32,6 +36,7 @@ public sealed class MxAccessSeverityMapperTests
|
||||
opcUa.ShouldBe(expectedOpcUaSeverity);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Map clamps negative severities into the low bucket.</summary>
|
||||
[Fact]
|
||||
public void Map_clamps_negative_severities_into_low_bucket()
|
||||
{
|
||||
|
||||
@@ -14,12 +14,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class MxValueDecoderTests
|
||||
{
|
||||
/// <summary>Verifies that decoding null returns null.</summary>
|
||||
[Fact]
|
||||
public void Decode_Null_ReturnsNull()
|
||||
{
|
||||
MxValueDecoder.Decode(null).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that decoding IsNull flag returns null.</summary>
|
||||
[Fact]
|
||||
public void Decode_IsNullFlag_ReturnsNull()
|
||||
{
|
||||
@@ -27,24 +29,31 @@ public sealed class MxValueDecoderTests
|
||||
MxValueDecoder.Decode(v).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bool values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_Bool() => MxValueDecoder.Decode(new MxValue { BoolValue = true }).ShouldBe(true);
|
||||
|
||||
/// <summary>Verifies that Int32 values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_Int32() => MxValueDecoder.Decode(new MxValue { Int32Value = -42 }).ShouldBe(-42);
|
||||
|
||||
/// <summary>Verifies that Int64 values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_Int64() => MxValueDecoder.Decode(new MxValue { Int64Value = 123456789012L }).ShouldBe(123456789012L);
|
||||
|
||||
/// <summary>Verifies that float values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_Float() => MxValueDecoder.Decode(new MxValue { FloatValue = 3.14f }).ShouldBe(3.14f);
|
||||
|
||||
/// <summary>Verifies that double values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_Double() => MxValueDecoder.Decode(new MxValue { DoubleValue = 2.71828 }).ShouldBe(2.71828);
|
||||
|
||||
/// <summary>Verifies that string values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_String() => MxValueDecoder.Decode(new MxValue { StringValue = "hello" }).ShouldBe("hello");
|
||||
|
||||
/// <summary>Verifies that timestamp values decode to UTC datetime.</summary>
|
||||
[Fact]
|
||||
public void Decode_Timestamp_ReturnsUtcDateTime()
|
||||
{
|
||||
@@ -53,6 +62,7 @@ public sealed class MxValueDecoderTests
|
||||
MxValueDecoder.Decode(v).ShouldBe(when);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bool arrays decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_BoolArray()
|
||||
{
|
||||
@@ -69,6 +79,7 @@ public sealed class MxValueDecoderTests
|
||||
decoded.ShouldBe(new[] { true, false, true });
|
||||
}
|
||||
|
||||
/// <summary>Verifies that double arrays decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_DoubleArray()
|
||||
{
|
||||
@@ -82,6 +93,7 @@ public sealed class MxValueDecoderTests
|
||||
decoded.ShouldBe(new[] { 1.0, 2.0, 3.5 });
|
||||
}
|
||||
|
||||
/// <summary>Verifies that string arrays decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_StringArray()
|
||||
{
|
||||
@@ -95,6 +107,7 @@ public sealed class MxValueDecoderTests
|
||||
decoded.ShouldBe(new[] { "a", "b" });
|
||||
}
|
||||
|
||||
/// <summary>Verifies that raw byte values decode correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_RawValue_ReturnsBytes()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class MxValueEncoderTests
|
||||
{
|
||||
/// <summary>Verifies that encoding a null value sets the IsNull flag.</summary>
|
||||
[Fact]
|
||||
public void Encode_Null_SetsIsNullFlag()
|
||||
{
|
||||
@@ -19,9 +20,13 @@ public sealed class MxValueEncoderTests
|
||||
v.IsNull.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that encoding a boolean value encodes correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_Bool() => MxValueEncoder.Encode(true).BoolValue.ShouldBe(true);
|
||||
|
||||
/// <summary>Verifies that narrow signed and unsigned types fit into Int32 encoding.</summary>
|
||||
/// <param name="input">The input value to encode.</param>
|
||||
/// <param name="expected">The expected encoded integer value.</param>
|
||||
[Theory]
|
||||
[InlineData((sbyte)-5, -5)]
|
||||
[InlineData((short)-1000, -1000)]
|
||||
@@ -34,9 +39,11 @@ public sealed class MxValueEncoderTests
|
||||
v.Int32Value.ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Int32 values round-trip through encoding.</summary>
|
||||
[Fact]
|
||||
public void Encode_Int32_RoundTrip() => MxValueEncoder.Encode(int.MinValue).Int32Value.ShouldBe(int.MinValue);
|
||||
|
||||
/// <summary>Verifies that Int64 values round-trip through encoding.</summary>
|
||||
[Fact]
|
||||
public void Encode_Int64_RoundTrip()
|
||||
{
|
||||
@@ -45,18 +52,23 @@ public sealed class MxValueEncoderTests
|
||||
v.Int64Value.ShouldBe(long.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that UInt32 values fit in Int32 encoding.</summary>
|
||||
[Fact]
|
||||
public void Encode_UInt32_FitsInInt32() => MxValueEncoder.Encode((uint)int.MaxValue).Int32Value.ShouldBe(int.MaxValue);
|
||||
|
||||
/// <summary>Verifies that float values encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_Float() => MxValueEncoder.Encode(3.14f).FloatValue.ShouldBe(3.14f);
|
||||
|
||||
/// <summary>Verifies that double values encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_Double() => MxValueEncoder.Encode(2.71828).DoubleValue.ShouldBe(2.71828);
|
||||
|
||||
/// <summary>Verifies that string values encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_String() => MxValueEncoder.Encode("hello").StringValue.ShouldBe("hello");
|
||||
|
||||
/// <summary>Verifies that UTC DateTime values encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_DateTimeUtc()
|
||||
{
|
||||
@@ -66,6 +78,7 @@ public sealed class MxValueEncoderTests
|
||||
v.TimestampValue.ToDateTime().ShouldBe(when);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that local DateTime values are converted to UTC during encoding.</summary>
|
||||
[Fact]
|
||||
public void Encode_DateTimeLocal_ConvertsToUtc()
|
||||
{
|
||||
@@ -74,6 +87,7 @@ public sealed class MxValueEncoderTests
|
||||
v.TimestampValue.ToDateTime().ShouldBe(local.ToUniversalTime());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that boolean arrays encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_BoolArray()
|
||||
{
|
||||
@@ -82,6 +96,7 @@ public sealed class MxValueEncoderTests
|
||||
v.ArrayValue.Dimensions[0].ShouldBe(3u);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that double arrays encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_DoubleArray()
|
||||
{
|
||||
@@ -89,6 +104,7 @@ public sealed class MxValueEncoderTests
|
||||
v.ArrayValue.DoubleValues.Values.ToArray().ShouldBe(new[] { 1.0, 2.0, 3.5 });
|
||||
}
|
||||
|
||||
/// <summary>Verifies that string arrays encode correctly.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringArray()
|
||||
{
|
||||
@@ -96,6 +112,7 @@ public sealed class MxValueEncoderTests
|
||||
v.ArrayValue.StringValues.Values.ToArray().ShouldBe(new[] { "a", "b" });
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all DateTime values in arrays are converted to UTC during encoding.</summary>
|
||||
[Fact]
|
||||
public void Encode_DateTimeArray_ConvertsAllToUtc()
|
||||
{
|
||||
@@ -104,12 +121,14 @@ public sealed class MxValueEncoderTests
|
||||
v.ArrayValue.TimestampValues.Values[0].ToDateTime().ShouldBe(inputs[0]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that encoding an unsupported type throws an exception.</summary>
|
||||
[Fact]
|
||||
public void Encode_UnsupportedType_Throws()
|
||||
{
|
||||
Should.Throw<ArgumentException>(() => MxValueEncoder.Encode(new { Foo = 1 }));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all scalar types round-trip correctly through encode and decode.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AllScalarTypes_DecodeMatchesOriginal()
|
||||
{
|
||||
|
||||
+9
@@ -17,6 +17,7 @@ public sealed class ReconnectSupervisorTests
|
||||
new(InitialBackoffOverride: TimeSpan.FromMilliseconds(5),
|
||||
MaxBackoffOverride: TimeSpan.FromMilliseconds(20));
|
||||
|
||||
/// <summary>Verifies that the supervisor starts in a healthy state with no errors.</summary>
|
||||
[Fact]
|
||||
public void InitialState_IsHealthy()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public sealed class ReconnectSupervisorTests
|
||||
sup.LastError.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reporting a transport failure drives the supervisor through reopen and replay cycles back to healthy.</summary>
|
||||
[Fact]
|
||||
public async Task ReportTransportFailure_DrivesThroughReopenReplay_BackToHealthy()
|
||||
{
|
||||
@@ -54,6 +56,7 @@ public sealed class ReconnectSupervisorTests
|
||||
sup.IsDegraded.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reopen failures trigger retries and the supervisor stays in reopening state between attempts.</summary>
|
||||
[Fact]
|
||||
public async Task ReopenFailure_RetriesUntilSuccess_StaysInReopeningBetweenAttempts()
|
||||
{
|
||||
@@ -71,6 +74,7 @@ public sealed class ReconnectSupervisorTests
|
||||
sup.LastError.ShouldBeNull(); // cleared on Healthy transition
|
||||
}
|
||||
|
||||
/// <summary>Verifies that replay failures trigger a retry of the entire reopen-replay cycle.</summary>
|
||||
[Fact]
|
||||
public async Task ReplayFailure_RetriesEntireCycle()
|
||||
{
|
||||
@@ -90,6 +94,7 @@ public sealed class ReconnectSupervisorTests
|
||||
sup.CurrentState.ShouldBe(ReconnectSupervisor.State.Healthy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated failure reports during recovery do not spawn parallel recovery loops.</summary>
|
||||
[Fact]
|
||||
public async Task RepeatedFailureReports_DuringRecovery_DoNotSpawnParallelLoops()
|
||||
{
|
||||
@@ -116,6 +121,7 @@ public sealed class ReconnectSupervisorTests
|
||||
attempts.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the last error reflects the most recent failure cause from recovery attempts.</summary>
|
||||
[Fact]
|
||||
public async Task LastError_ReflectsMostRecentFailureCause()
|
||||
{
|
||||
@@ -134,6 +140,7 @@ public sealed class ReconnectSupervisorTests
|
||||
sup.LastError.ShouldContain("reopen broke"); // updates from the loop's failed reopen attempts
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the supervisor cancels a running recovery loop cleanly.</summary>
|
||||
[Fact]
|
||||
public async Task Dispose_CancelsRunningRecoveryLoop_Cleanly()
|
||||
{
|
||||
@@ -153,6 +160,7 @@ public sealed class ReconnectSupervisorTests
|
||||
cancelled.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reporting a transport failure after dispose throws ObjectDisposedException.</summary>
|
||||
[Fact]
|
||||
public void ReportTransportFailure_AfterDispose_Throws()
|
||||
{
|
||||
@@ -161,6 +169,7 @@ public sealed class ReconnectSupervisorTests
|
||||
Should.Throw<ObjectDisposedException>(() => sup.ReportTransportFailure(new IOException("x")));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that waiting for healthy state returns immediately when already healthy.</summary>
|
||||
[Fact]
|
||||
public async Task WaitForHealthy_ReturnsImmediately_WhenAlreadyHealthy()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class StatusCodeMapTests
|
||||
{
|
||||
/// <summary>Verifies that known quality bytes map to the expected OPC UA status codes.</summary>
|
||||
/// <param name="input">The Galaxy quality byte to map.</param>
|
||||
/// <param name="expected">The expected OPC UA status code.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)192, 0x00000000u)] // Good
|
||||
[InlineData((byte)216, 0x00D80000u)] // Good_LocalOverride
|
||||
@@ -35,6 +38,8 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromQualityByte(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown quality bytes fall back to category bucket status codes.</summary>
|
||||
/// <param name="input">The unknown Galaxy quality byte to categorize.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)200)] // Unknown Good — falls back to category bucket
|
||||
[InlineData((byte)100)] // Unknown Uncertain
|
||||
@@ -53,12 +58,14 @@ public sealed class StatusCodeMapTests
|
||||
// would invert the mapping when the worker sets success=1 for an error code (the numeric
|
||||
// value is NOT a boolean).
|
||||
|
||||
/// <summary>Verifies that null status maps to Good status code.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_NullStatus_IsGood()
|
||||
{
|
||||
StatusCodeMap.FromMxStatus(null).ShouldBe(StatusCodeMap.Good);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero success with OK category maps to Good.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_SuccessNonZeroAndCategoryOk_IsGood()
|
||||
{
|
||||
@@ -67,6 +74,7 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Good);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero success with non-OK category does not map to Good.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_SuccessNonZeroButCategoryNotOk_IsNotGood()
|
||||
{
|
||||
@@ -76,6 +84,7 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromMxStatus(s).ShouldNotBe(StatusCodeMap.Good);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that known detail codes map to their specific status codes.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_SuccessZeroAndCategoryNotOk_DetailKnown_MapsToSpecificCode()
|
||||
{
|
||||
@@ -83,6 +92,7 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadNotConnected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero DetectedBy maps to communication error when detail is zero.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_SuccessZero_DetailZero_DetectedByNonZero_IsCommunicationError()
|
||||
{
|
||||
@@ -90,6 +100,7 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadCommunicationError);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all-zero status maps to Bad status code.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_SuccessZero_AllZero_IsBad()
|
||||
{
|
||||
@@ -97,6 +108,7 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that top two bits of status codes follow OPC UA convention.</summary>
|
||||
[Fact]
|
||||
public void TopByteCategoryBits_StayWithinOpcUaConvention()
|
||||
{
|
||||
@@ -115,6 +127,9 @@ public sealed class StatusCodeMapTests
|
||||
|
||||
// ===== Driver.Galaxy-004 regression: ToQualityCategoryByte lives next to its inverse =====
|
||||
|
||||
/// <summary>Verifies that status codes extract to their OPC DA category byte equivalent.</summary>
|
||||
/// <param name="statusCode">The OPC UA status code to convert.</param>
|
||||
/// <param name="expected">The expected OPC DA category byte.</param>
|
||||
[Theory]
|
||||
[InlineData(0x00000000u, (byte)192)] // Good
|
||||
[InlineData(0x00D80000u, (byte)192)] // GoodLocalOverride — still Good category
|
||||
@@ -128,6 +143,7 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.ToQualityCategoryByte(statusCode).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ToQualityCategoryByte is the right inverse of FromQualityByte for category bytes.</summary>
|
||||
[Fact]
|
||||
public void ToQualityCategoryByte_IsRightInverseOfFromQualityByte_ForCategoryBytes()
|
||||
{
|
||||
|
||||
+41
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
/// </summary>
|
||||
public sealed class SubscriptionRegistryTests
|
||||
{
|
||||
/// <summary>Verifies NextSubscriptionId() increments monotonically.</summary>
|
||||
[Fact]
|
||||
public void NextSubscriptionId_IsMonotonic()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public sealed class SubscriptionRegistryTests
|
||||
registry.NextSubscriptionId().ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>Verifies Register() and ResolveSubscribers() correctly store and return a single subscription.</summary>
|
||||
[Fact]
|
||||
public void Register_OneSubscription_OneTag_ResolvesSingleSubscriber()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public sealed class SubscriptionRegistryTests
|
||||
subs[0].FullReference.ShouldBe("Tank.Level");
|
||||
}
|
||||
|
||||
/// <summary>Verifies multiple subscriptions to the same tag are both indexed for fan-out.</summary>
|
||||
[Fact]
|
||||
public void Register_TwoSubscriptions_SameTag_FanOutToBoth()
|
||||
{
|
||||
@@ -44,6 +47,7 @@ public sealed class SubscriptionRegistryTests
|
||||
subs.Select(s => s.SubscriptionId).OrderBy(x => x).ShouldBe(new[] { 1L, 2L });
|
||||
}
|
||||
|
||||
/// <summary>Verifies failed item handles (rejected by gateway) are not indexed for fan-out.</summary>
|
||||
[Fact]
|
||||
public void Register_FailedItemHandle_NotIndexedForFanOut()
|
||||
{
|
||||
@@ -57,6 +61,7 @@ public sealed class SubscriptionRegistryTests
|
||||
registry.ResolveSubscribers(0).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies Remove() drops all bindings and returns them.</summary>
|
||||
[Fact]
|
||||
public void Remove_DropsAllBindings_AndReturnsThemForUnsubscribe()
|
||||
{
|
||||
@@ -74,6 +79,7 @@ public sealed class SubscriptionRegistryTests
|
||||
registry.ResolveSubscribers(200).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies removing one subscription of multiple leaves others intact.</summary>
|
||||
[Fact]
|
||||
public void Remove_OneOfTwoSubscriptions_LeavesOtherIntact()
|
||||
{
|
||||
@@ -88,6 +94,7 @@ public sealed class SubscriptionRegistryTests
|
||||
subs[0].SubscriptionId.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies removing an unknown subscription returns null without error.</summary>
|
||||
[Fact]
|
||||
public void Remove_UnknownSubscription_IsNullSentinel()
|
||||
{
|
||||
@@ -95,6 +102,7 @@ public sealed class SubscriptionRegistryTests
|
||||
registry.Remove(999).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies TrackedSubscriptionCount and TrackedItemHandleCount reflect registrations and removals.</summary>
|
||||
[Fact]
|
||||
public void TrackedCounts_ReflectAdditionsAndRemovals()
|
||||
{
|
||||
@@ -117,6 +125,7 @@ public sealed class SubscriptionRegistryTests
|
||||
|
||||
// ===== Driver.Galaxy-008 regression: reconnect replay rebinds with fresh handles =====
|
||||
|
||||
/// <summary>Verifies SnapshotEntries() groups bindings correctly by subscription ID.</summary>
|
||||
[Fact]
|
||||
public void SnapshotEntries_GroupsBindingsBySubscriptionId()
|
||||
{
|
||||
@@ -131,6 +140,7 @@ public sealed class SubscriptionRegistryTests
|
||||
entries.Single(e => e.SubscriptionId == 2).Bindings.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies Rebind() replaces stale handles with fresh post-reconnect handles.</summary>
|
||||
[Fact]
|
||||
public void Rebind_ReplacesStaleItemHandles_WithThePostReconnectHandles()
|
||||
{
|
||||
@@ -148,6 +158,7 @@ public sealed class SubscriptionRegistryTests
|
||||
subs[0].FullReference.ShouldBe("Tank.Level");
|
||||
}
|
||||
|
||||
/// <summary>Verifies Rebind() on one subscription does not affect others on the same old handle.</summary>
|
||||
[Fact]
|
||||
public void Rebind_LeavesOtherSubscriptionsOnTheSameOldHandleIntact()
|
||||
{
|
||||
@@ -162,6 +173,7 @@ public sealed class SubscriptionRegistryTests
|
||||
registry.ResolveSubscribers(555).Select(s => s.SubscriptionId).ShouldBe(new[] { 1L });
|
||||
}
|
||||
|
||||
/// <summary>Verifies Rebind() on an unknown subscription is a no-op.</summary>
|
||||
[Fact]
|
||||
public void Rebind_UnknownSubscription_IsNoOp()
|
||||
{
|
||||
@@ -170,6 +182,7 @@ public sealed class SubscriptionRegistryTests
|
||||
registry.ResolveSubscribers(1).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies Rebind() does not index rejected item handles.</summary>
|
||||
[Fact]
|
||||
public void Rebind_FailedItemHandle_NotIndexedForFanOut()
|
||||
{
|
||||
@@ -185,6 +198,7 @@ public sealed class SubscriptionRegistryTests
|
||||
|
||||
// ===== Driver.Galaxy-012 regression: ResolveSubscribers is O(1) per binding =====
|
||||
|
||||
/// <summary>Verifies ResolveSubscribers() correctly dispatches in a large binding set without linear scan.</summary>
|
||||
[Fact]
|
||||
public void ResolveSubscribers_LargeBindingSet_DispatchesCorrectly()
|
||||
{
|
||||
@@ -215,23 +229,50 @@ public sealed class SubscriptionRegistryTests
|
||||
|
||||
// Internal types are accessed via friend assembly (InternalsVisibleTo); these
|
||||
// wrapper aliases keep the test code readable.
|
||||
/// <summary>Wrapper for accessing SubscriptionRegistry in tests via internal visibility.</summary>
|
||||
private sealed class SubscriptionRegistryAccess
|
||||
{
|
||||
/// <summary>The underlying SubscriptionRegistry instance.</summary>
|
||||
private readonly SubscriptionRegistry _inner = new();
|
||||
|
||||
/// <summary>Gets the count of tracked subscriptions.</summary>
|
||||
public int TrackedSubscriptionCount => _inner.TrackedSubscriptionCount;
|
||||
|
||||
/// <summary>Gets the count of tracked item handles.</summary>
|
||||
public int TrackedItemHandleCount => _inner.TrackedItemHandleCount;
|
||||
|
||||
/// <summary>Gets the next subscription ID.</summary>
|
||||
public long NextSubscriptionId() => _inner.NextSubscriptionId();
|
||||
|
||||
/// <summary>Registers a subscription with the given bindings.</summary>
|
||||
/// <param name="id">The subscription ID.</param>
|
||||
/// <param name="bindings">The tag bindings to register.</param>
|
||||
public void Register(long id, IReadOnlyList<TagBindingAccess> bindings)
|
||||
=> _inner.Register(id, [.. bindings.Select(b => new TagBinding(b.FullReference, b.ItemHandle))]);
|
||||
|
||||
/// <summary>Removes a subscription and returns its bindings.</summary>
|
||||
/// <param name="id">The subscription ID.</param>
|
||||
/// <returns>The bindings that were removed, or null if not found.</returns>
|
||||
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))];
|
||||
}
|
||||
|
||||
/// <summary>Resolves subscribers for the given item handle.</summary>
|
||||
/// <param name="handle">The item handle to look up.</param>
|
||||
/// <returns>The list of subscriptions observing this handle.</returns>
|
||||
public IReadOnlyList<(long SubscriptionId, string FullReference)> ResolveSubscribers(int handle)
|
||||
=> _inner.ResolveSubscribers(handle);
|
||||
|
||||
/// <summary>Rebinds a subscription to new item handles.</summary>
|
||||
/// <param name="id">The subscription ID.</param>
|
||||
/// <param name="bindings">The new tag bindings.</param>
|
||||
public void Rebind(long id, IReadOnlyList<TagBindingAccess> bindings)
|
||||
=> _inner.Rebind(id, [.. bindings.Select(b => new TagBinding(b.FullReference, b.ItemHandle))]);
|
||||
|
||||
/// <summary>Snapshots all subscription entries grouped by ID.</summary>
|
||||
/// <returns>All subscription entries with their bindings.</returns>
|
||||
public IReadOnlyList<(long SubscriptionId, IReadOnlyList<TagBindingAccess> Bindings)> SnapshotEntries()
|
||||
=> [.. _inner.SnapshotEntries().Select(e =>
|
||||
(e.SubscriptionId,
|
||||
|
||||
Reference in New Issue
Block a user