Communication Layer (WP-1–5): - 8 message patterns with correlation IDs, per-pattern timeouts - Central/Site communication actors, transport heartbeat config - Connection failure handling (no central buffering, debug streams killed) Data Connection Layer (WP-6–14, WP-34): - Connection actor with Become/Stash lifecycle (Connecting/Connected/Reconnecting) - OPC UA + LmxProxy adapters behind IDataConnection - Auto-reconnect, bad quality propagation, transparent re-subscribe - Write-back, tag path resolution with retry, health reporting - Protocol extensibility via DataConnectionFactory Site Runtime (WP-15–25, WP-32–33): - ScriptActor/ScriptExecutionActor (triggers, concurrent execution, blocking I/O dispatcher) - AlarmActor/AlarmExecutionActor (ValueMatch/RangeViolation/RateOfChange, in-memory state) - SharedScriptLibrary (inline execution), ScriptRuntimeContext (API) - ScriptCompilationService (Roslyn, forbidden API enforcement, execution timeout) - Recursion limit (default 10), call direction enforcement - SiteStreamManager (per-subscriber bounded buffers, fire-and-forget) - Debug view backend (snapshot + stream), concurrency serialization - Local artifact storage (4 SQLite tables) Health Monitoring (WP-26–28): - SiteHealthCollector (thread-safe counters, connection state) - HealthReportSender (30s interval, monotonic sequence numbers) - CentralHealthAggregator (offline detection 60s, online recovery) Site Event Logging (WP-29–31): - SiteEventLogger (SQLite, 6 event categories, ISO 8601 UTC) - EventLogPurgeService (30-day retention, 1GB cap) - EventLogQueryService (filters, keyword search, keyset pagination) 541 tests pass, zero warnings.
145 lines
5.1 KiB
C#
145 lines
5.1 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using NSubstitute;
|
|
using ScadaLink.Commons.Interfaces.Protocol;
|
|
using ScadaLink.Commons.Messages.DataConnection;
|
|
using ScadaLink.Commons.Types.Enums;
|
|
using ScadaLink.DataConnectionLayer.Actors;
|
|
|
|
namespace ScadaLink.DataConnectionLayer.Tests;
|
|
|
|
/// <summary>
|
|
/// WP-6: Tests for DataConnectionActor Become/Stash state machine.
|
|
/// WP-9: Auto-reconnect and bad quality tests.
|
|
/// WP-10: Transparent re-subscribe tests.
|
|
/// WP-11: Write-back support tests.
|
|
/// WP-12: Tag path resolution with retry tests.
|
|
/// WP-13: Health reporting tests.
|
|
/// WP-14: Subscription lifecycle tests.
|
|
/// </summary>
|
|
public class DataConnectionActorTests : TestKit
|
|
{
|
|
private readonly IDataConnection _mockAdapter;
|
|
private readonly DataConnectionOptions _options;
|
|
|
|
public DataConnectionActorTests()
|
|
: base(@"akka.loglevel = DEBUG")
|
|
{
|
|
_mockAdapter = Substitute.For<IDataConnection>();
|
|
_options = new DataConnectionOptions
|
|
{
|
|
ReconnectInterval = TimeSpan.FromMilliseconds(100),
|
|
TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200),
|
|
WriteTimeout = TimeSpan.FromSeconds(5)
|
|
};
|
|
}
|
|
|
|
private IActorRef CreateConnectionActor(string name = "test-conn")
|
|
{
|
|
return Sys.ActorOf(Props.Create(() =>
|
|
new DataConnectionActor(name, _mockAdapter, _options)), name);
|
|
}
|
|
|
|
[Fact]
|
|
public void WP6_StartsInConnectingState_AttemptsConnect()
|
|
{
|
|
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var actor = CreateConnectionActor();
|
|
|
|
// Give it time to attempt connection
|
|
AwaitCondition(() =>
|
|
_mockAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"),
|
|
TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
[Fact]
|
|
public void WP6_ConnectingState_StashesSubscribeRequests()
|
|
{
|
|
// Make connect hang so we stay in Connecting
|
|
var tcs = new TaskCompletionSource();
|
|
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(tcs.Task);
|
|
|
|
var actor = CreateConnectionActor("stash-test");
|
|
|
|
// Send subscribe while connecting — should be stashed
|
|
actor.Tell(new SubscribeTagsRequest(
|
|
"corr1", "inst1", "stash-test", ["tag1"], DateTimeOffset.UtcNow));
|
|
|
|
// No response yet (stashed)
|
|
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Complete connection — should unstash and process
|
|
_mockAdapter.SubscribeAsync(Arg.Any<string>(), Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>())
|
|
.Returns("sub-001");
|
|
|
|
tcs.SetResult();
|
|
|
|
// Now we should get the response
|
|
ExpectMsg<SubscribeTagsResponse>(TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WP11_ConnectedState_Write_ReturnsResult()
|
|
{
|
|
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_mockAdapter.WriteAsync("tag1", 42, Arg.Any<CancellationToken>())
|
|
.Returns(new WriteResult(true, null));
|
|
|
|
var actor = CreateConnectionActor("write-test");
|
|
|
|
// Wait for connected state
|
|
AwaitCondition(() =>
|
|
_mockAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"),
|
|
TimeSpan.FromSeconds(2));
|
|
|
|
// Small delay for state transition
|
|
await Task.Delay(200);
|
|
|
|
actor.Tell(new WriteTagRequest("corr1", "write-test", "tag1", 42, DateTimeOffset.UtcNow));
|
|
|
|
var response = ExpectMsg<WriteTagResponse>(TimeSpan.FromSeconds(3));
|
|
Assert.True(response.Success);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WP11_Write_Failure_ReturnedSynchronously()
|
|
{
|
|
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_mockAdapter.WriteAsync("tag1", 42, Arg.Any<CancellationToken>())
|
|
.Returns(new WriteResult(false, "Device offline"));
|
|
|
|
var actor = CreateConnectionActor("write-fail-test");
|
|
|
|
await Task.Delay(300);
|
|
|
|
actor.Tell(new WriteTagRequest("corr1", "write-fail-test", "tag1", 42, DateTimeOffset.UtcNow));
|
|
|
|
var response = ExpectMsg<WriteTagResponse>(TimeSpan.FromSeconds(3));
|
|
Assert.False(response.Success);
|
|
Assert.Equal("Device offline", response.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WP13_HealthReport_ReturnsConnectionStatus()
|
|
{
|
|
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_mockAdapter.Status.Returns(ConnectionHealth.Connected);
|
|
|
|
var actor = CreateConnectionActor("health-test");
|
|
|
|
await Task.Delay(300);
|
|
|
|
actor.Tell(new DataConnectionActor.GetHealthReport());
|
|
|
|
var report = ExpectMsg<DataConnectionHealthReport>(TimeSpan.FromSeconds(2));
|
|
Assert.Equal("health-test", report.ConnectionName);
|
|
Assert.Equal(ConnectionHealth.Connected, report.Status);
|
|
}
|
|
}
|