513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
163 lines
7.7 KiB
C#
163 lines
7.7 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
|
|
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for DataConnectionManagerActor routing and lifecycle.
|
|
/// </summary>
|
|
[Collection("DataConnectionManagerActor")]
|
|
public class DataConnectionManagerActorTests : TestKit
|
|
{
|
|
private readonly IDataConnectionFactory _mockFactory;
|
|
private readonly DataConnectionOptions _options;
|
|
private readonly ISiteHealthCollector _mockHealthCollector;
|
|
|
|
/// <summary>Initializes the test class with mock collaborators and the actor-system configuration.</summary>
|
|
public DataConnectionManagerActorTests()
|
|
: base(@"akka.loglevel = DEBUG")
|
|
{
|
|
_mockFactory = Substitute.For<IDataConnectionFactory>();
|
|
_mockHealthCollector = Substitute.For<ISiteHealthCollector>();
|
|
_options = new DataConnectionOptions
|
|
{
|
|
ReconnectInterval = TimeSpan.FromMilliseconds(100),
|
|
TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200)
|
|
};
|
|
}
|
|
|
|
/// <summary>Verifies that writing to an unknown connection returns an error.</summary>
|
|
[Fact]
|
|
public void WriteToUnknownConnection_ReturnsError()
|
|
{
|
|
var manager = Sys.ActorOf(Props.Create(() =>
|
|
new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector)));
|
|
|
|
manager.Tell(new WriteTagRequest(
|
|
"corr1", "nonexistent", "tag1", 42, DateTimeOffset.UtcNow));
|
|
|
|
var response = ExpectMsg<WriteTagResponse>();
|
|
Assert.False(response.Success);
|
|
Assert.Contains("Unknown connection", response.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>Verifies that subscribing to an unknown connection returns an error.</summary>
|
|
[Fact]
|
|
public void SubscribeToUnknownConnection_ReturnsError()
|
|
{
|
|
var manager = Sys.ActorOf(Props.Create(() =>
|
|
new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector)));
|
|
|
|
manager.Tell(new SubscribeTagsRequest(
|
|
"corr1", "inst1", "nonexistent", ["tag1"], DateTimeOffset.UtcNow));
|
|
|
|
var response = ExpectMsg<SubscribeTagsResponse>();
|
|
Assert.False(response.Success);
|
|
Assert.Contains("Unknown connection", response.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>Verifies that subscribing to alarms on an unknown connection returns an error.</summary>
|
|
[Fact]
|
|
public void SubscribeAlarmsToUnknownConnection_ReturnsError()
|
|
{
|
|
// Regression for the live integration gap: the NativeAlarmActor sends
|
|
// SubscribeAlarmsRequest to the DCL manager, which must route it to the
|
|
// named connection actor (or reply with an error) — not dead-letter it.
|
|
var manager = Sys.ActorOf(Props.Create(() =>
|
|
new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector)));
|
|
|
|
manager.Tell(new SubscribeAlarmsRequest(
|
|
"corr1", "inst1", "nonexistent", "ns=2;s=Tank01", null, DateTimeOffset.UtcNow));
|
|
|
|
var response = ExpectMsg<SubscribeAlarmsResponse>();
|
|
Assert.False(response.Success);
|
|
Assert.Contains("Unknown connection", response.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>Verifies that a connection actor crash preserves the subscription state.</summary>
|
|
[Fact]
|
|
public void DCL002_ConnectionActorCrash_PreservesSubscriptionState()
|
|
{
|
|
// Regression test: the supervisor used
|
|
// Directive.Restart, which discards the connection actor's in-memory
|
|
// subscription registry — breaking the design doc's "transparent
|
|
// re-subscribe" guarantee (subscribers are never re-subscribed and sit at
|
|
// stale quality forever). After the fix the supervisor uses Resume, which
|
|
// keeps the actor instance and its state across a transient exception.
|
|
var mockAdapter = Substitute.For<IDataConnection>();
|
|
mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
mockAdapter.Status.Returns(ConnectionHealth.Connected);
|
|
mockAdapter.SubscribeAsync(Arg.Any<string>(), Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>())
|
|
.Returns("sub-001");
|
|
mockAdapter.ReadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(new ReadResult(false, null, null));
|
|
// A write throws synchronously, escaping the message handler and crashing
|
|
// the connection actor — exercising the supervisor strategy.
|
|
mockAdapter.WriteAsync(Arg.Any<string>(), Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
|
.Returns<Task<WriteResult>>(_ => throw new InvalidOperationException("boom"));
|
|
|
|
_mockFactory.Create("OpcUa", Arg.Any<IDictionary<string, string>>()).Returns(mockAdapter);
|
|
|
|
var manager = Sys.ActorOf(Props.Create(() =>
|
|
new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector)));
|
|
|
|
manager.Tell(new CreateConnectionCommand("conn1", "OpcUa", new Dictionary<string, string>(), null, 3));
|
|
// Wait until the connection actor has called ConnectAsync (i.e. reached Connected)
|
|
// rather than sleeping a fixed 300 ms that can expire before the async connect
|
|
// completes under CPU contention.
|
|
AwaitCondition(
|
|
() => mockAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"),
|
|
TimeSpan.FromSeconds(30));
|
|
|
|
// Register a subscription.
|
|
manager.Tell(new SubscribeTagsRequest("c1", "inst1", "conn1", ["tag1"], DateTimeOffset.UtcNow));
|
|
ExpectMsg<SubscribeTagsResponse>(TimeSpan.FromSeconds(3));
|
|
|
|
// Crash the connection actor via a synchronously-throwing write.
|
|
manager.Tell(new WriteTagRequest("c2", "conn1", "tag1", 42, DateTimeOffset.UtcNow));
|
|
// Wait until WriteAsync was invoked, proving the WriteTagRequest was fully
|
|
// processed by the connection actor (and the supervisor's Resume directive
|
|
// applied) before we query health. A fixed sleep can expire before the
|
|
// actor processes the message under CPU load.
|
|
AwaitCondition(
|
|
() => mockAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "WriteAsync"),
|
|
TimeSpan.FromSeconds(30));
|
|
|
|
// After the crash the subscription state must survive: the health report
|
|
// still shows the subscribed/resolved tag. With Restart it would be 0.
|
|
manager.Tell(new GetAllHealthReports());
|
|
var report = ExpectMsg<DataConnectionHealthReport>(TimeSpan.FromSeconds(3));
|
|
Assert.Equal(1, report.TotalSubscribedTags);
|
|
Assert.Equal(1, report.ResolvedTags);
|
|
}
|
|
|
|
/// <summary>Verifies that creating a connection uses the connection factory.</summary>
|
|
[Fact]
|
|
public void CreateConnection_UsesFactory()
|
|
{
|
|
var mockAdapter = Substitute.For<IDataConnection>();
|
|
mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_mockFactory.Create("OpcUa", Arg.Any<IDictionary<string, string>>())
|
|
.Returns(mockAdapter);
|
|
|
|
var manager = Sys.ActorOf(Props.Create(() =>
|
|
new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector)));
|
|
|
|
manager.Tell(new CreateConnectionCommand(
|
|
"conn1", "OpcUa", new Dictionary<string, string>(), null, 3));
|
|
|
|
// Factory should have been called
|
|
AwaitCondition(() =>
|
|
_mockFactory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
|
TimeSpan.FromSeconds(2));
|
|
}
|
|
}
|