feat(opcua,host): #81 ServiceLevel SDK publisher
SdkServiceLevelPublisher writes Server.ServiceLevel through the SDK's ServerObjectState — the standard OPC UA non-transparent-redundancy signal clients use to pick a primary. Writes are guarded by DiagnosticsLock so concurrent SDK diagnostics scans don't fight with our updates. DeferredServiceLevelPublisher mirrors the DeferredAddressSpaceSink late- binding pattern: Akka actors resolve IServiceLevelPublisher at construction, hosted service swaps the SDK publisher in after StandardServer.Start. Host Program.cs registers DeferredServiceLevelPublisher as the singleton bound to IServiceLevelPublisher; OtOpcUaServerHostedService gets it injected and fills it once IServerInternal is available. Tests boot a real StandardServer on a free port (cross-platform), call Publish, then verify ServerObject.ServiceLevel.Value reflects the write. 5 new tests; OpcUaServer suite now 45/45 green (was 40, +5). Closes #81 residual. Unblocks Task 60 (OPC UA dual-endpoint + ServiceLevel tests).
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Opc.Ua.Server;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #81 residual — verifies <see cref="SdkServiceLevelPublisher"/> locates the standard
|
||||
/// <c>VariableIds.Server_ServiceLevel</c> node through the SDK's DiagnosticsNodeManager and
|
||||
/// writes the byte value. Boots a real <see cref="StandardServer"/> on a free port so the
|
||||
/// SDK populates its predefined diagnostics nodes — that's what production sees.
|
||||
/// </summary>
|
||||
public sealed class SdkServiceLevelPublisherTests : IDisposable
|
||||
{
|
||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
|
||||
private readonly string _pkiRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"otopcua-pki-{Guid.NewGuid():N}");
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_writes_value_to_Server_ServiceLevel_variable()
|
||||
{
|
||||
var server = new StandardServer();
|
||||
await using var host = new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = "OtOpcUa.SvcLevel",
|
||||
ApplicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}",
|
||||
OpcUaPort = AllocateFreePort(),
|
||||
PublicHostname = "localhost",
|
||||
PkiStoreRoot = _pkiRoot,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
|
||||
await host.StartAsync(server, Ct);
|
||||
|
||||
var publisher = new SdkServiceLevelPublisher(
|
||||
server.CurrentInstance,
|
||||
NullLogger<SdkServiceLevelPublisher>.Instance);
|
||||
|
||||
publisher.Publish(200);
|
||||
|
||||
var variable = server.CurrentInstance.ServerObject.ServiceLevel;
|
||||
variable.ShouldNotBeNull("Server.ServiceLevel must be present in the address space");
|
||||
variable.Value.ShouldBe((byte)200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_is_idempotent_when_called_multiple_times()
|
||||
{
|
||||
var server = new StandardServer();
|
||||
await using var host = new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = "OtOpcUa.SvcLevel.Idem",
|
||||
ApplicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}",
|
||||
OpcUaPort = AllocateFreePort(),
|
||||
PublicHostname = "localhost",
|
||||
PkiStoreRoot = _pkiRoot,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
|
||||
await host.StartAsync(server, Ct);
|
||||
var publisher = new SdkServiceLevelPublisher(
|
||||
server.CurrentInstance,
|
||||
NullLogger<SdkServiceLevelPublisher>.Instance);
|
||||
|
||||
publisher.Publish(100);
|
||||
publisher.Publish(150);
|
||||
publisher.Publish(240);
|
||||
|
||||
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)240);
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_pkiRoot))
|
||||
{
|
||||
try { Directory.Delete(_pkiRoot, recursive: true); }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user