Phase 3B: Site I/O & Observability — Communication, DCL, Script/Alarm actors, Health, Event Logging
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.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ScadaLink.DataConnectionLayer.Adapters;
|
||||
|
||||
namespace ScadaLink.DataConnectionLayer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-34: Tests for protocol extensibility via DataConnectionFactory.
|
||||
/// </summary>
|
||||
public class DataConnectionFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_OpcUa_ReturnsOpcUaAdapter()
|
||||
{
|
||||
var factory = new DataConnectionFactory(NullLoggerFactory.Instance);
|
||||
|
||||
var connection = factory.Create("OpcUa", new Dictionary<string, string>());
|
||||
|
||||
Assert.IsType<OpcUaDataConnection>(connection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_LmxProxy_ReturnsLmxProxyAdapter()
|
||||
{
|
||||
var factory = new DataConnectionFactory(NullLoggerFactory.Instance);
|
||||
|
||||
var connection = factory.Create("LmxProxy", new Dictionary<string, string>());
|
||||
|
||||
Assert.IsType<LmxProxyDataConnection>(connection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_CaseInsensitive()
|
||||
{
|
||||
var factory = new DataConnectionFactory(NullLoggerFactory.Instance);
|
||||
|
||||
var connection = factory.Create("opcua", new Dictionary<string, string>());
|
||||
|
||||
Assert.IsType<OpcUaDataConnection>(connection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_UnknownProtocol_Throws()
|
||||
{
|
||||
var factory = new DataConnectionFactory(NullLoggerFactory.Instance);
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
factory.Create("UnknownProtocol", new Dictionary<string, string>()));
|
||||
|
||||
Assert.Contains("Unknown protocol type", ex.Message);
|
||||
Assert.Contains("OpcUa", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAdapter_ExtendsFactory()
|
||||
{
|
||||
var factory = new DataConnectionFactory(NullLoggerFactory.Instance);
|
||||
|
||||
// WP-34: Adding new protocol = register adapter
|
||||
factory.RegisterAdapter("Custom", _ => new OpcUaDataConnection(
|
||||
new DefaultOpcUaClientFactory(), NullLogger<OpcUaDataConnection>.Instance));
|
||||
|
||||
var connection = factory.Create("Custom", new Dictionary<string, string>());
|
||||
Assert.NotNull(connection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using NSubstitute;
|
||||
using ScadaLink.Commons.Interfaces.Protocol;
|
||||
using ScadaLink.Commons.Messages.DataConnection;
|
||||
using ScadaLink.DataConnectionLayer.Actors;
|
||||
|
||||
namespace ScadaLink.DataConnectionLayer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-34: Tests for DataConnectionManagerActor routing and lifecycle.
|
||||
/// </summary>
|
||||
public class DataConnectionManagerActorTests : TestKit
|
||||
{
|
||||
private readonly IDataConnectionFactory _mockFactory;
|
||||
private readonly DataConnectionOptions _options;
|
||||
|
||||
public DataConnectionManagerActorTests()
|
||||
: base(@"akka.loglevel = DEBUG")
|
||||
{
|
||||
_mockFactory = Substitute.For<IDataConnectionFactory>();
|
||||
_options = new DataConnectionOptions
|
||||
{
|
||||
ReconnectInterval = TimeSpan.FromMilliseconds(100),
|
||||
TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200)
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteToUnknownConnection_ReturnsError()
|
||||
{
|
||||
var manager = Sys.ActorOf(Props.Create(() =>
|
||||
new DataConnectionManagerActor(_mockFactory, _options)));
|
||||
|
||||
manager.Tell(new WriteTagRequest(
|
||||
"corr1", "nonexistent", "tag1", 42, DateTimeOffset.UtcNow));
|
||||
|
||||
var response = ExpectMsg<WriteTagResponse>();
|
||||
Assert.False(response.Success);
|
||||
Assert.Contains("Unknown connection", response.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubscribeToUnknownConnection_ReturnsError()
|
||||
{
|
||||
var manager = Sys.ActorOf(Props.Create(() =>
|
||||
new DataConnectionManagerActor(_mockFactory, _options)));
|
||||
|
||||
manager.Tell(new SubscribeTagsRequest(
|
||||
"corr1", "inst1", "nonexistent", ["tag1"], DateTimeOffset.UtcNow));
|
||||
|
||||
var response = ExpectMsg<SubscribeTagsResponse>();
|
||||
Assert.False(response.Success);
|
||||
Assert.Contains("Unknown connection", response.ErrorMessage);
|
||||
}
|
||||
|
||||
[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)));
|
||||
|
||||
manager.Tell(new CreateConnectionCommand(
|
||||
"conn1", "OpcUa", new Dictionary<string, string>()));
|
||||
|
||||
// Factory should have been called
|
||||
AwaitCondition(() =>
|
||||
_mockFactory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
||||
TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ScadaLink.DataConnectionLayer.Adapters;
|
||||
|
||||
namespace ScadaLink.DataConnectionLayer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-8: Tests for LmxProxy adapter.
|
||||
/// </summary>
|
||||
public class LmxProxyDataConnectionTests
|
||||
{
|
||||
private readonly ILmxProxyClient _mockClient;
|
||||
private readonly ILmxProxyClientFactory _mockFactory;
|
||||
private readonly LmxProxyDataConnection _adapter;
|
||||
|
||||
public LmxProxyDataConnectionTests()
|
||||
{
|
||||
_mockClient = Substitute.For<ILmxProxyClient>();
|
||||
_mockFactory = Substitute.For<ILmxProxyClientFactory>();
|
||||
_mockFactory.Create().Returns(_mockClient);
|
||||
_adapter = new LmxProxyDataConnection(_mockFactory, NullLogger<LmxProxyDataConnection>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_OpensSessionWithHostAndPort()
|
||||
{
|
||||
_mockClient.OpenSessionAsync("myhost", 5001, Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["Host"] = "myhost",
|
||||
["Port"] = "5001"
|
||||
});
|
||||
|
||||
Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
|
||||
await _mockClient.Received(1).OpenSessionAsync("myhost", 5001, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disconnect_ClosesSession()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
await _adapter.DisconnectAsync();
|
||||
|
||||
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
||||
await _mockClient.Received(1).CloseSessionAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_Success_ReturnsGoodResult()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.WriteTagAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.WriteAsync("Tag1", 42);
|
||||
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_Failure_ReturnsError()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.WriteTagAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Returns(false);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.WriteAsync("Tag1", 42);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal("LmxProxy write failed", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Good_ReturnsValue()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.ReadTagAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns((42.5, DateTime.UtcNow, true));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(42.5, result.Value!.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Bad_ReturnsFailure()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.ReadTagAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns((null, DateTime.UtcNow, false));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnOperations()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_adapter.ReadAsync("tag1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ScadaLink.Commons.Interfaces.Protocol;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ScadaLink.DataConnectionLayer.Adapters;
|
||||
|
||||
namespace ScadaLink.DataConnectionLayer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: Tests for OPC UA adapter.
|
||||
/// </summary>
|
||||
public class OpcUaDataConnectionTests
|
||||
{
|
||||
private readonly IOpcUaClient _mockClient;
|
||||
private readonly IOpcUaClientFactory _mockFactory;
|
||||
private readonly OpcUaDataConnection _adapter;
|
||||
|
||||
public OpcUaDataConnectionTests()
|
||||
{
|
||||
_mockClient = Substitute.For<IOpcUaClient>();
|
||||
_mockFactory = Substitute.For<IOpcUaClientFactory>();
|
||||
_mockFactory.Create().Returns(_mockClient);
|
||||
_adapter = new OpcUaDataConnection(_mockFactory, NullLogger<OpcUaDataConnection>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_SetsStatusToConnected()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["EndpointUrl"] = "opc.tcp://localhost:4840"
|
||||
});
|
||||
|
||||
Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
|
||||
await _mockClient.Received(1).ConnectAsync("opc.tcp://localhost:4840", Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disconnect_SetsStatusToDisconnected()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
await _adapter.DisconnectAsync();
|
||||
|
||||
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Subscribe_DelegatesAndReturnsId()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.CreateSubscriptionAsync(Arg.Any<string>(), Arg.Any<Action<string, object?, DateTime, uint>>(), Arg.Any<CancellationToken>())
|
||||
.Returns("sub-001");
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var subId = await _adapter.SubscribeAsync("ns=2;s=Tag1", (_, _) => { });
|
||||
|
||||
Assert.Equal("sub-001", subId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_Success_ReturnsGoodResult()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.WriteValueAsync("ns=2;s=Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Returns((uint)0);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.WriteAsync("ns=2;s=Tag1", 42);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_Failure_ReturnsError()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.WriteValueAsync("ns=2;s=Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Returns(0x80000000u);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.WriteAsync("ns=2;s=Tag1", 42);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("0x80000000", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_BadStatus_ReturnsBadResult()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.ReadValueAsync("ns=2;s=Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns((null, DateTime.UtcNow, 0x80000000u));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.ReadAsync("ns=2;s=Tag1");
|
||||
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_GoodStatus_ReturnsValue()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.ReadValueAsync("ns=2;s=Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns((42.5, DateTime.UtcNow, 0u));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.ReadAsync("ns=2;s=Tag1");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Value);
|
||||
Assert.Equal(42.5, result.Value!.Value);
|
||||
Assert.Equal(QualityCode.Good, result.Value.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadBatch_ReadsAllTags()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.ReadValueAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns((1.0, DateTime.UtcNow, 0u));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var results = await _adapter.ReadBatchAsync(["tag1", "tag2", "tag3"]);
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
Assert.All(results.Values, r => Assert.True(r.Success));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnOperations()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_adapter.ReadAsync("tag1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_CleansUp()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
await _adapter.DisposeAsync();
|
||||
|
||||
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@@ -9,8 +9,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akka.TestKit.Xunit2" Version="1.5.62" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
@@ -21,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/ScadaLink.DataConnectionLayer/ScadaLink.DataConnectionLayer.csproj" />
|
||||
<ProjectReference Include="../../src/ScadaLink.Commons/ScadaLink.Commons.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace ScadaLink.DataConnectionLayer.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user