feat: add JoeAppEngine OPC UA nodes, fix DCL auto-reconnect and quality push
- Add JoeAppEngine folder to OPC UA nodes.json (BTCS, AlarmCntsBySeverity, Scheduler/ScanTime) - Fix DataConnectionActor: capture Self in PreStart for use from non-actor threads, preventing Self.Tell failure in Disconnected event handler - Implement InstanceActor.HandleConnectionQualityChanged to mark attributes Bad on disconnect - Fix LmxFakeProxy TagMapper to serialize arrays as JSON instead of "System.Int32[]" - Allow DataType and DataSourceReference updates in TemplateService.UpdateAttributeAsync - Update test_infra_opcua.md with JoeAppEngine documentation
This commit is contained in:
@@ -17,7 +17,7 @@ public class LmxProxyDataConnectionTests
|
||||
{
|
||||
_mockClient = Substitute.For<ILmxProxyClient>();
|
||||
_mockFactory = Substitute.For<ILmxProxyClientFactory>();
|
||||
_mockFactory.Create(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string?>()).Returns(_mockClient);
|
||||
_mockFactory.Create(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string?>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(_mockClient);
|
||||
_adapter = new LmxProxyDataConnection(_mockFactory, NullLogger<LmxProxyDataConnection>.Instance);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class LmxProxyDataConnectionTests
|
||||
});
|
||||
|
||||
Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
|
||||
_mockFactory.Received(1).Create("myhost", 5001, null);
|
||||
_mockFactory.Received(1).Create("myhost", 5001, null, 0, false);
|
||||
await _mockClient.Received(1).ConnectAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class LmxProxyDataConnectionTests
|
||||
["ApiKey"] = "my-secret-key"
|
||||
});
|
||||
|
||||
_mockFactory.Received(1).Create("server", 50051, "my-secret-key");
|
||||
_mockFactory.Received(1).Create("server", 50051, "my-secret-key", 0, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -67,7 +67,7 @@ public class LmxProxyDataConnectionTests
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
|
||||
_mockFactory.Received(1).Create("localhost", 50051, null);
|
||||
_mockFactory.Received(1).Create("localhost", 50051, null, 0, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -201,7 +201,7 @@ public class LmxProxyDataConnectionTests
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var mockSub = Substitute.For<ILmxSubscription>();
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>())
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<Action?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockSub);
|
||||
|
||||
var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { });
|
||||
@@ -209,7 +209,7 @@ public class LmxProxyDataConnectionTests
|
||||
Assert.NotNull(subId);
|
||||
Assert.NotEmpty(subId);
|
||||
await _mockClient.Received(1).SubscribeAsync(
|
||||
Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>());
|
||||
Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<Action?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -217,7 +217,7 @@ public class LmxProxyDataConnectionTests
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var mockSub = Substitute.For<ILmxSubscription>();
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>())
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<Action?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockSub);
|
||||
|
||||
var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { });
|
||||
@@ -240,7 +240,7 @@ public class LmxProxyDataConnectionTests
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var mockSub = Substitute.For<ILmxSubscription>();
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>())
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<Action?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockSub);
|
||||
await _adapter.SubscribeAsync("Tag1", (_, _) => { });
|
||||
|
||||
@@ -277,4 +277,46 @@ public class LmxProxyDataConnectionTests
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_adapter.SubscribeAsync("tag1", (_, _) => { }));
|
||||
}
|
||||
|
||||
// --- Configuration Parsing ---
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_ParsesSamplingInterval()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["Host"] = "server",
|
||||
["Port"] = "50051",
|
||||
["SamplingIntervalMs"] = "500"
|
||||
});
|
||||
|
||||
_mockFactory.Received(1).Create("server", 50051, null, 500, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_ParsesUseTls()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["Host"] = "server",
|
||||
["Port"] = "50051",
|
||||
["UseTls"] = "true"
|
||||
});
|
||||
|
||||
_mockFactory.Received(1).Create("server", 50051, null, 0, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_DefaultsSamplingAndTls()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
|
||||
_mockFactory.Received(1).Create("localhost", 50051, null, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class OpcUaDataConnectionTests
|
||||
});
|
||||
|
||||
Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
|
||||
await _mockClient.Received(1).ConnectAsync("opc.tcp://localhost:4840", Arg.Any<CancellationToken>());
|
||||
await _mockClient.Received(1).ConnectAsync("opc.tcp://localhost:4840", Arg.Any<OpcUaConnectionOptions?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -149,4 +149,123 @@ public class OpcUaDataConnectionTests
|
||||
|
||||
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
||||
}
|
||||
|
||||
// --- Configuration Parsing ---
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_ParsesAllConfigurationKeys()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["EndpointUrl"] = "opc.tcp://myserver:4840",
|
||||
["SessionTimeoutMs"] = "120000",
|
||||
["OperationTimeoutMs"] = "30000",
|
||||
["PublishingIntervalMs"] = "500",
|
||||
["KeepAliveCount"] = "5",
|
||||
["LifetimeCount"] = "15",
|
||||
["MaxNotificationsPerPublish"] = "200",
|
||||
["SamplingIntervalMs"] = "250",
|
||||
["QueueSize"] = "20",
|
||||
["SecurityMode"] = "SignAndEncrypt",
|
||||
["AutoAcceptUntrustedCerts"] = "false"
|
||||
});
|
||||
|
||||
await _mockClient.Received(1).ConnectAsync(
|
||||
"opc.tcp://myserver:4840",
|
||||
Arg.Is<OpcUaConnectionOptions?>(o =>
|
||||
o != null &&
|
||||
o.SessionTimeoutMs == 120000 &&
|
||||
o.OperationTimeoutMs == 30000 &&
|
||||
o.PublishingIntervalMs == 500 &&
|
||||
o.KeepAliveCount == 5 &&
|
||||
o.LifetimeCount == 15 &&
|
||||
o.MaxNotificationsPerPublish == 200 &&
|
||||
o.SamplingIntervalMs == 250 &&
|
||||
o.QueueSize == 20 &&
|
||||
o.SecurityMode == "SignAndEncrypt" &&
|
||||
o.AutoAcceptUntrustedCerts == false),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_UsesDefaults_WhenKeysNotProvided()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
|
||||
await _mockClient.Received(1).ConnectAsync(
|
||||
"opc.tcp://localhost:4840",
|
||||
Arg.Is<OpcUaConnectionOptions?>(o =>
|
||||
o != null &&
|
||||
o.SessionTimeoutMs == 60000 &&
|
||||
o.OperationTimeoutMs == 15000 &&
|
||||
o.PublishingIntervalMs == 1000 &&
|
||||
o.KeepAliveCount == 10 &&
|
||||
o.LifetimeCount == 30 &&
|
||||
o.MaxNotificationsPerPublish == 100 &&
|
||||
o.SamplingIntervalMs == 1000 &&
|
||||
o.QueueSize == 10 &&
|
||||
o.SecurityMode == "None" &&
|
||||
o.AutoAcceptUntrustedCerts == true),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_IgnoresInvalidNumericValues()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["SessionTimeoutMs"] = "notanumber",
|
||||
["OperationTimeoutMs"] = "",
|
||||
["PublishingIntervalMs"] = "abc",
|
||||
["QueueSize"] = "12.5"
|
||||
});
|
||||
|
||||
await _mockClient.Received(1).ConnectAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<OpcUaConnectionOptions?>(o =>
|
||||
o != null &&
|
||||
o.SessionTimeoutMs == 60000 &&
|
||||
o.OperationTimeoutMs == 15000 &&
|
||||
o.PublishingIntervalMs == 1000 &&
|
||||
o.QueueSize == 10),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_ParsesSecurityMode()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["SecurityMode"] = "Sign"
|
||||
});
|
||||
|
||||
await _mockClient.Received(1).ConnectAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<OpcUaConnectionOptions?>(o => o != null && o.SecurityMode == "Sign"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_ParsesAutoAcceptCerts()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["AutoAcceptUntrustedCerts"] = "false"
|
||||
});
|
||||
|
||||
await _mockClient.Received(1).ConnectAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<OpcUaConnectionOptions?>(o => o != null && o.AutoAcceptUntrustedCerts == false),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user