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:
Joseph Doherty
2026-03-19 13:27:54 -04:00
parent ffdda51990
commit 7740a3bcf9
70 changed files with 2684 additions and 541 deletions

View File

@@ -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);
}
}