Resolve all high-severity architecture review findings
This commit is contained in:
@@ -15,8 +15,9 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
||||
/// <c>infra/docker-compose.yml</c>). Marked <c>[SkippableFact]</c> so it reports
|
||||
/// Skipped — not failed — when the endpoint is unreachable OR does not expose
|
||||
/// Alarms & Conditions (no ConditionRefresh snapshot arrives). The OPC PLC
|
||||
/// simulator does not reliably expose A&C; see
|
||||
/// <c>docs/test_infra/test_infra.md</c> for the alarm-capable-server requirement.
|
||||
/// fixture exposes two Boiler event sources used by the multi-source regression.
|
||||
/// That regression skips only when the endpoint is unavailable; missing fixture
|
||||
/// nodes or incomplete refresh frames fail once the server is reachable.
|
||||
///
|
||||
/// The pure field→transition mapping is covered without a server by
|
||||
/// <see cref="Adapters.OpcUaAlarmMapperTests"/>; this test proves the live
|
||||
@@ -78,4 +79,59 @@ public class OpcUaAlarmLiveSmokeTests
|
||||
await adapter.DisconnectAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task TwoBoilerSources_KeepDistinctRefreshScopesAndEpochs()
|
||||
{
|
||||
await using var client = new RealOpcUaClient();
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(EndpointUrl,
|
||||
new OpcUaConnectionOptions(OperationTimeoutMs: 5000), deadline.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Skip.If(true, $"OPC UA test server unavailable: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Distinct event notifier objects exposed by the local OPC PLC fixture.
|
||||
// Adding B requests a subscription-wide ConditionRefresh, so A receives
|
||||
// another refresh as well. Each monitored item must keep its own frame.
|
||||
const string sourceA = "nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5017";
|
||||
const string sourceB = "nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=15070";
|
||||
var transitions = new System.Collections.Concurrent.ConcurrentQueue<NativeAlarmTransition>();
|
||||
var bothComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
void Capture(NativeAlarmTransition transition)
|
||||
{
|
||||
transitions.Enqueue(transition);
|
||||
if (transitions.Any(t => t.Kind == AlarmTransitionKind.SnapshotComplete && t.SourceObjectReference == sourceA)
|
||||
&& transitions.Any(t => t.Kind == AlarmTransitionKind.SnapshotComplete && t.SourceObjectReference == sourceB))
|
||||
bothComplete.TrySetResult();
|
||||
}
|
||||
|
||||
await client.CreateAlarmSubscriptionAsync(sourceA, null, Capture, deadline.Token);
|
||||
await client.CreateAlarmSubscriptionAsync(sourceB, null, Capture, deadline.Token);
|
||||
await bothComplete.Task.WaitAsync(deadline.Token);
|
||||
|
||||
var boundaries = transitions.Where(t => t.Kind is AlarmTransitionKind.SnapshotStart
|
||||
or AlarmTransitionKind.SnapshotComplete).ToArray();
|
||||
foreach (var source in new[] { sourceA, sourceB })
|
||||
{
|
||||
var frames = boundaries.Where(t => t.SourceObjectReference == source).ToArray();
|
||||
Assert.Contains(frames, t => t.Kind == AlarmTransitionKind.SnapshotStart);
|
||||
Assert.Contains(frames, t => t.Kind == AlarmTransitionKind.SnapshotComplete);
|
||||
Assert.All(frames, t =>
|
||||
{
|
||||
Assert.Equal(source, t.SnapshotSourceReference);
|
||||
Assert.NotNull(t.SnapshotId);
|
||||
});
|
||||
foreach (var end in frames.Where(t => t.Kind == AlarmTransitionKind.SnapshotComplete))
|
||||
Assert.Single(frames, t => t.Kind == AlarmTransitionKind.SnapshotStart && t.SnapshotId == end.SnapshotId);
|
||||
}
|
||||
Assert.Empty(boundaries.Where(t => t.SourceObjectReference == sourceA).Select(t => t.SnapshotId)
|
||||
.Intersect(boundaries.Where(t => t.SourceObjectReference == sourceB).Select(t => t.SnapshotId)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user