Merge hw2-ports: OPC UA test hosts retry a free-port bind race (#606)
v2-ci / build (push) Successful in 4m52s
v2-ci / unit-tests (push) Failing after 20m17s

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 22:31:28 -04:00
41 changed files with 948 additions and 1078 deletions
@@ -998,14 +998,17 @@ public sealed class AclEventSubscriptionEnforcementTests
public static async Task<Rig> StartAsync(string name, CancellationToken ct) public static async Task<Rig> StartAsync(string name, CancellationToken ct)
{ {
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}")); var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort(); var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
rig._server = new OtOpcUaSdkServer(); (rig._host, rig._server) = await OpcUaHostStart.StartAsync(
rig._host = new OpcUaApplicationHost( port =>
{
rig._port = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = $"OtOpcUa.{name}", ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = rig._port, OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot, PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None], EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
@@ -1013,8 +1016,8 @@ public sealed class AclEventSubscriptionEnforcementTests
}, },
NullLogger<OpcUaApplicationHost>.Instance, NullLogger<OpcUaApplicationHost>.Instance,
new KeyPresentingAuthenticator()); new KeyPresentingAuthenticator());
},
await rig._host.StartAsync(rig._server, ct); () => new OtOpcUaSdkServer(), ct);
rig._sink = new SdkAddressSpaceSink(rig.Nm); rig._sink = new SdkAddressSpaceSink(rig.Nm);
return rig; return rig;
} }
@@ -1158,15 +1161,6 @@ public sealed class AclEventSubscriptionEnforcementTests
} }
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
/// <summary> /// <summary>
/// Admits the fixed <c>apikey</c> username as <see cref="KeyPrincipal"/> carrying /// Admits the fixed <c>apikey</c> username as <see cref="KeyPrincipal"/> carrying
/// <see cref="KeyId"/>; anonymous falls through to the interface default (the /// <see cref="KeyId"/>; anonymous falls through to the interface default (the
@@ -154,14 +154,17 @@ public sealed class AclKeyExpiryEnforcementTests
public static async Task<Rig> StartAsync(string name, CancellationToken ct) public static async Task<Rig> StartAsync(string name, CancellationToken ct)
{ {
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}")); var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort(); var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
rig._server = new OtOpcUaSdkServer(); (rig._host, rig._server) = await OpcUaHostStart.StartAsync(
rig._host = new OpcUaApplicationHost( port =>
{
rig._port = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = $"OtOpcUa.{name}", ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = rig._port, OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot, PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None], EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
@@ -169,8 +172,8 @@ public sealed class AclKeyExpiryEnforcementTests
}, },
NullLogger<OpcUaApplicationHost>.Instance, NullLogger<OpcUaApplicationHost>.Instance,
new KeyPresentingAuthenticator()); new KeyPresentingAuthenticator());
},
await rig._host.StartAsync(rig._server, ct); () => new OtOpcUaSdkServer(), ct);
return rig; return rig;
} }
@@ -321,15 +324,6 @@ public sealed class AclKeyExpiryEnforcementTests
} }
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
/// <summary> /// <summary>
/// Admits the fixed <c>apikey</c> username as <see cref="Principal"/>, carrying /// Admits the fixed <c>apikey</c> username as <see cref="Principal"/>, carrying
/// <see cref="KeyId"/>. /// <see cref="KeyId"/>.
@@ -303,22 +303,25 @@ public sealed class AclSubscriptionEnforcementTests
public static async Task<Rig> StartAsync(string name, CancellationToken ct) public static async Task<Rig> StartAsync(string name, CancellationToken ct)
{ {
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}")); var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort(); var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
rig._server = new OtOpcUaSdkServer(); (rig._host, rig._server) = await OpcUaHostStart.StartAsync(
rig._host = new OpcUaApplicationHost( port =>
{
rig._port = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = $"OtOpcUa.{name}", ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = rig._port, OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot, PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None], EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance);
},
await rig._host.StartAsync(rig._server, ct); () => new OtOpcUaSdkServer(), ct);
return rig; return rig;
} }
@@ -435,14 +438,6 @@ public sealed class AclSubscriptionEnforcementTests
catch (IOException) { /* best effort */ } catch (IOException) { /* best effort */ }
} }
} }
}
}
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}
@@ -28,13 +28,13 @@ public sealed class DualEndpointTests
{ {
var pkiRootA = Path.Combine(Path.GetTempPath(), $"otopcua-pki-a-{Guid.NewGuid():N}"); var pkiRootA = Path.Combine(Path.GetTempPath(), $"otopcua-pki-a-{Guid.NewGuid():N}");
var pkiRootB = Path.Combine(Path.GetTempPath(), $"otopcua-pki-b-{Guid.NewGuid():N}"); var pkiRootB = Path.Combine(Path.GetTempPath(), $"otopcua-pki-b-{Guid.NewGuid():N}");
var portA = AllocateFreePort();
var portB = AllocateFreePort();
try try
{ {
await using var nodeA = await StartNodeAsync(NodeAUri, portA, pkiRootA, peers: new[] { NodeBUri }); var (nodeAHost, portA) = await StartNodeAsync(NodeAUri, pkiRootA, peers: new[] { NodeBUri });
await using var nodeB = await StartNodeAsync(NodeBUri, portB, pkiRootB, peers: new[] { NodeAUri }); await using var nodeA = nodeAHost;
var (nodeBHost, _) = await StartNodeAsync(NodeBUri, pkiRootB, peers: new[] { NodeAUri });
await using var nodeB = nodeBHost;
var serverArray = await ReadServerArrayAsync($"opc.tcp://127.0.0.1:{portA}/OtOpcUa"); var serverArray = await ReadServerArrayAsync($"opc.tcp://127.0.0.1:{portA}/OtOpcUa");
serverArray.ShouldContain(NodeAUri); serverArray.ShouldContain(NodeAUri);
@@ -47,10 +47,17 @@ public sealed class DualEndpointTests
} }
} }
private static async Task<OpcUaApplicationHost> StartNodeAsync( private static async Task<(OpcUaApplicationHost Host, int Port)> StartNodeAsync(
string applicationUri, int port, string pkiRoot, string[] peers) string applicationUri, string pkiRoot, string[] peers)
{ {
var options = new OpcUaApplicationHostOptions // The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
var boundPort = 0;
var started = await OpcUaHostStart.StartAsync(
port =>
{
boundPort = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{ {
ApplicationName = applicationUri, ApplicationName = applicationUri,
ApplicationUri = applicationUri, ApplicationUri = applicationUri,
@@ -60,11 +67,11 @@ public sealed class DualEndpointTests
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
PeerApplicationUris = peers, PeerApplicationUris = peers,
}; },
var server = new StandardServer(); NullLogger<OpcUaApplicationHost>.Instance);
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); },
await host.StartAsync(server, CancellationToken.None); () => new StandardServer(), CancellationToken.None);
return host; return (started.Host, boundPort);
} }
private static async Task<string[]> ReadServerArrayAsync(string endpointUrl) private static async Task<string[]> ReadServerArrayAsync(string endpointUrl)
@@ -111,13 +118,4 @@ public sealed class DualEndpointTests
if (Directory.Exists(clientPki)) Directory.Delete(clientPki, recursive: true); if (Directory.Exists(clientPki)) Directory.Delete(clientPki, recursive: true);
} }
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
} }
@@ -54,11 +54,10 @@ public sealed class DualNamespaceAddressSpaceTests
public async Task Both_namespaces_registered_and_both_subtrees_browse_read_and_organize() public async Task Both_namespaces_registered_and_both_subtrees_browse_read_and_organize()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-browse-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-browse-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri, ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -108,11 +107,10 @@ public sealed class DualNamespaceAddressSpaceTests
public async Task Single_source_fans_to_both_nodeids_with_identical_value_quality_and_timestamp() public async Task Single_source_fans_to_both_nodeids_with_identical_value_quality_and_timestamp()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-fanout-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-fanout-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Fanout", ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".Fanout", ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -146,11 +144,10 @@ public sealed class DualNamespaceAddressSpaceTests
public async Task HistoryRead_via_either_nodeid_returns_GoodNoData_under_the_shared_tagname() public async Task HistoryRead_via_either_nodeid_returns_GoodNoData_under_the_shared_tagname()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-hist-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-hist-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".History", ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".History", ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -178,11 +175,10 @@ public sealed class DualNamespaceAddressSpaceTests
public async Task WriteOperate_gate_is_symmetric_across_both_nodeids() public async Task WriteOperate_gate_is_symmetric_across_both_nodeids()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-write-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-write-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Write", ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".Write", ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -263,10 +259,17 @@ public sealed class DualNamespaceAddressSpaceTests
return response.Results[0]; return response.Results[0];
} }
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync( private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
int port, string pkiRoot, string appUri, CancellationToken ct) string pkiRoot, string appUri, CancellationToken ct)
{ {
var options = new OpcUaApplicationHostOptions // The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
var boundPort = 0;
var started = await OpcUaHostStart.StartAsync(
port =>
{
boundPort = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{ {
ApplicationName = appUri, ApplicationName = appUri,
ApplicationUri = appUri, ApplicationUri = appUri,
@@ -275,11 +278,11 @@ public sealed class DualNamespaceAddressSpaceTests
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; },
var server = new OtOpcUaSdkServer(); NullLogger<OpcUaApplicationHost>.Instance);
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); },
await host.StartAsync(server, ct); () => new OtOpcUaSdkServer(), ct);
return (server, host); return (started.Server, started.Host, boundPort);
} }
private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct) private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
@@ -306,15 +309,6 @@ public sealed class DualNamespaceAddressSpaceTests
identity: new UserIdentity(new AnonymousIdentityToken()), preferredLocales: null, ct: ct); identity: new UserIdentity(new AnonymousIdentityToken()), preferredLocales: null, ct: ct);
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static void SafeDelete(string dir) private static void SafeDelete(string dir)
{ {
if (Directory.Exists(dir)) if (Directory.Exists(dir))
@@ -65,11 +65,10 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
public async Task Condition_event_carries_populated_EventType_SourceNode_and_SourceName_on_the_wire() public async Task Condition_event_carries_populated_EventType_SourceNode_and_SourceName_on_the_wire()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-identity-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-identity-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri, ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -134,11 +133,10 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
public async Task Condition_event_carries_populated_ConditionClass_fields_on_the_wire() public async Task Condition_event_carries_populated_ConditionClass_fields_on_the_wire()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-condclass-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-condclass-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ConditionClassServerUri, ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ConditionClassServerUri, ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -187,11 +185,10 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
public async Task Condition_event_Quality_tracks_source_connectivity_on_the_wire() public async Task Condition_event_Quality_tracks_source_connectivity_on_the_wire()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-quality-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-quality-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", QualityServerUri, ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", QualityServerUri, ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -333,10 +330,17 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
} }
} }
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync( private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
int port, string pkiRoot, string appUri, CancellationToken ct) string pkiRoot, string appUri, CancellationToken ct)
{ {
var options = new OpcUaApplicationHostOptions // The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
var boundPort = 0;
var started = await OpcUaHostStart.StartAsync(
port =>
{
boundPort = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{ {
ApplicationName = appUri, ApplicationName = appUri,
ApplicationUri = appUri, ApplicationUri = appUri,
@@ -345,11 +349,11 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; },
var server = new OtOpcUaSdkServer(); NullLogger<OpcUaApplicationHost>.Instance);
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); },
await host.StartAsync(server, ct); () => new OtOpcUaSdkServer(), ct);
return (server, host); return (started.Server, started.Host, boundPort);
} }
private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct) private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
@@ -387,15 +391,6 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
condition().ShouldBeTrue("condition not met within timeout"); condition().ShouldBeTrue("condition not met within timeout");
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static void SafeDelete(string dir) private static void SafeDelete(string dir)
{ {
if (Directory.Exists(dir)) if (Directory.Exists(dir))
@@ -47,11 +47,10 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
public async Task Single_transition_delivers_exactly_one_event_at_the_Server_object() public async Task Single_transition_delivers_exactly_one_event_at_the_Server_object()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-evt-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-evt-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri, ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -95,11 +94,10 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
public async Task Overlapping_folder_and_server_subscriptions_each_receive_exactly_one_copy() public async Task Overlapping_folder_and_server_subscriptions_each_receive_exactly_one_copy()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-ovl-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-ovl-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
{ {
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Overlap", ct); var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".Overlap", ct);
await using var _ = host; await using var _ = host;
var sink = new SdkAddressSpaceSink(server.NodeManager!); var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -208,10 +206,17 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
} }
} }
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync( private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
int port, string pkiRoot, string appUri, CancellationToken ct) string pkiRoot, string appUri, CancellationToken ct)
{ {
var options = new OpcUaApplicationHostOptions // The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
var boundPort = 0;
var started = await OpcUaHostStart.StartAsync(
port =>
{
boundPort = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{ {
ApplicationName = appUri, ApplicationName = appUri,
ApplicationUri = appUri, ApplicationUri = appUri,
@@ -220,11 +225,11 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; },
var server = new OtOpcUaSdkServer(); NullLogger<OpcUaApplicationHost>.Instance);
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); },
await host.StartAsync(server, ct); () => new OtOpcUaSdkServer(), ct);
return (server, host); return (started.Server, started.Host, boundPort);
} }
private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct) private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
@@ -262,15 +267,6 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
condition().ShouldBeTrue("condition not met within timeout"); condition().ShouldBeTrue("condition not met within timeout");
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static void SafeDelete(string dir) private static void SafeDelete(string dir)
{ {
if (Directory.Exists(dir)) if (Directory.Exists(dir))
@@ -51,12 +51,10 @@ internal sealed class ServerEventRig : IAsyncDisposable
public static async Task<ServerEventRig> StartAsync(string name, bool encrypted, CancellationToken ct) public static async Task<ServerEventRig> StartAsync(string name, bool encrypted, CancellationToken ct)
{ {
var rig = new ServerEventRig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}")); var rig = new ServerEventRig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort();
rig._hostOptions = new OpcUaApplicationHostOptions rig._hostOptions = new OpcUaApplicationHostOptions
{ {
ApplicationName = $"OtOpcUa.{name}", ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}", ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
OpcUaPort = rig._port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot, PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = encrypted EnabledSecurityProfiles = encrypted
@@ -64,19 +62,29 @@ internal sealed class ServerEventRig : IAsyncDisposable
: [OpcUaSecurityProfile.None], : [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; };
await rig.BootAsync(ct); (rig._host, rig._server) = await OpcUaHostStart.StartAsync(
port =>
{
rig._port = port;
rig._hostOptions.OpcUaPort = port;
return rig.NewHost();
},
() => new OtOpcUaSdkServer(), ct);
rig.Sink = new SdkAddressSpaceSink(rig.Nm);
return rig; return rig;
} }
private async Task BootAsync(CancellationToken ct) private async Task BootAsync(CancellationToken ct)
{ {
_server = new OtOpcUaSdkServer(); _server = new OtOpcUaSdkServer();
_host = new OpcUaApplicationHost( _host = NewHost();
_hostOptions, NullLogger<OpcUaApplicationHost>.Instance, new KeyPresentingAuthenticator());
await _host.StartAsync(_server, ct); await _host.StartAsync(_server, ct);
Sink = new SdkAddressSpaceSink(Nm); Sink = new SdkAddressSpaceSink(Nm);
} }
private OpcUaApplicationHost NewHost() =>
new(_hostOptions, NullLogger<OpcUaApplicationHost>.Instance, new KeyPresentingAuthenticator());
/// <summary> /// <summary>
/// Stops the server and boots a fresh one on the SAME port, application URI and PKI store (a process /// Stops the server and boots a fresh one on the SAME port, application URI and PKI store (a process
/// restart as a client sees it). The address space is EMPTY afterwards: the caller re-creates its nodes and /// restart as a client sees it). The address space is EMPTY afterwards: the caller re-creates its nodes and
@@ -291,15 +299,6 @@ internal sealed class ServerEventRig : IAsyncDisposable
} }
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
/// <summary> /// <summary>
/// Admits <c>apikey</c> as <see cref="KeyPrincipal"/> carrying the two server-wide roles (used only /// Admits <c>apikey</c> as <see cref="KeyPrincipal"/> carrying the two server-wide roles (used only
/// while enforcement is off); anonymous falls through to the <c>anonymous</c> principal. /// while enforcement is off); anonymous falls through to the <c>anonymous</c> principal.
@@ -38,7 +38,6 @@ public sealed class SubscriptionSurvivalTests
public async Task Subscription_on_existing_tag_survives_pure_add_of_another_tag() public async Task Subscription_on_existing_tag_survives_pure_add_of_another_tag()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
@@ -47,15 +46,21 @@ public sealed class SubscriptionSurvivalTests
{ {
ApplicationName = ServerUri, ApplicationName = ServerUri,
ApplicationUri = ServerUri, ApplicationUri = ServerUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; };
var server = new OtOpcUaSdkServer(); var started = await OpcUaHostStart.StartAsync(
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); port =>
await host.StartAsync(server, ct); {
options.OpcUaPort = port;
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
await using var host = started.Host;
var server = started.Server;
var port = options.OpcUaPort;
var nm = server.NodeManager!; var nm = server.NodeManager!;
var sink = new SdkAddressSpaceSink(nm); var sink = new SdkAddressSpaceSink(nm);
@@ -145,7 +150,6 @@ public sealed class SubscriptionSurvivalTests
public async Task Subscription_on_surviving_tag_survives_pure_remove_of_another_tag() public async Task Subscription_on_surviving_tag_survives_pure_remove_of_another_tag()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-rm-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-rm-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
@@ -154,15 +158,21 @@ public sealed class SubscriptionSurvivalTests
{ {
ApplicationName = ServerUri, ApplicationName = ServerUri,
ApplicationUri = ServerUri + ".Remove", ApplicationUri = ServerUri + ".Remove",
OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; };
var server = new OtOpcUaSdkServer(); var started = await OpcUaHostStart.StartAsync(
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); port =>
await host.StartAsync(server, ct); {
options.OpcUaPort = port;
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
await using var host = started.Host;
var server = started.Server;
var port = options.OpcUaPort;
var nm = server.NodeManager!; var nm = server.NodeManager!;
var sink = new SdkAddressSpaceSink(nm); var sink = new SdkAddressSpaceSink(nm);
@@ -234,7 +244,6 @@ public sealed class SubscriptionSurvivalTests
public async Task Subscription_survives_mixed_add_remove_deploy() public async Task Subscription_survives_mixed_add_remove_deploy()
{ {
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-mix-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-mix-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
try try
@@ -243,15 +252,21 @@ public sealed class SubscriptionSurvivalTests
{ {
ApplicationName = ServerUri, ApplicationName = ServerUri,
ApplicationUri = ServerUri + ".Mixed", ApplicationUri = ServerUri + ".Mixed",
OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}; };
var server = new OtOpcUaSdkServer(); var started = await OpcUaHostStart.StartAsync(
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); port =>
await host.StartAsync(server, ct); {
options.OpcUaPort = port;
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
await using var host = started.Host;
var server = started.Server;
var port = options.OpcUaPort;
var nm = server.NodeManager!; var nm = server.NodeManager!;
var sink = new SdkAddressSpaceSink(nm); var sink = new SdkAddressSpaceSink(nm);
@@ -372,13 +387,4 @@ public sealed class SubscriptionSurvivalTests
} }
condition().ShouldBeTrue("condition not met within timeout"); condition().ShouldBeTrue("condition not met within timeout");
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
} }
@@ -41,4 +41,9 @@
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj"/> <ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Shared bind-race retry helper for tests that boot a real SDK server on a probed port. -->
<Compile Include="..\ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests\OpcUaHostStart.cs" Link="OpcUaHostStart.cs"/>
</ItemGroup>
</Project> </Project>
@@ -70,19 +70,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact] [Fact]
public async Task MaterialiseHierarchy_against_real_SDK_node_manager_creates_folder_nodes() public async Task MaterialiseHierarchy_against_real_SDK_node_manager_creates_folder_nodes()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.Hierarchy:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.Hierarchy", ApplicationName = "OtOpcUa.Hierarchy",
ApplicationUri = $"urn:OtOpcUa.Hierarchy:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var sdkServer = new OtOpcUaSdkServer(); await using var host = started.Host;
await host.StartAsync(sdkServer, Ct); var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull(); sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!); var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
@@ -116,19 +118,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact] [Fact]
public async Task MaterialiseEquipmentTags_against_real_SDK_node_manager_is_idempotent() public async Task MaterialiseEquipmentTags_against_real_SDK_node_manager_is_idempotent()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.EquipmentTags:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.EquipmentTags", ApplicationName = "OtOpcUa.EquipmentTags",
ApplicationUri = $"urn:OtOpcUa.EquipmentTags:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var sdkServer = new OtOpcUaSdkServer(); await using var host = started.Host;
await host.StartAsync(sdkServer, Ct); var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull(); sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!); var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
@@ -172,19 +176,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact] [Fact]
public async Task Raw_and_uns_variables_materialise_end_to_end_against_real_SDK() public async Task Raw_and_uns_variables_materialise_end_to_end_against_real_SDK()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.EquipmentE2e:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.EquipmentE2e", ApplicationName = "OtOpcUa.EquipmentE2e",
ApplicationUri = $"urn:OtOpcUa.EquipmentE2e:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var sdkServer = new OtOpcUaSdkServer(); await using var host = started.Host;
await host.StartAsync(sdkServer, Ct); var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull(); sdkServer.NodeManager.ShouldNotBeNull();
// The v3 raw chain: RawFolder "Plant" → Driver "Modbus" → Device "dev1" → Tag "Speed" // The v3 raw chain: RawFolder "Plant" → Driver "Modbus" → Device "dev1" → Tag "Speed"
@@ -238,19 +244,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact] [Fact]
public async Task Area_and_line_rename_updates_existing_folder_display_name_in_place_against_real_SDK() public async Task Area_and_line_rename_updates_existing_folder_display_name_in_place_against_real_SDK()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.FolderRename:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.FolderRename", ApplicationName = "OtOpcUa.FolderRename",
ApplicationUri = $"urn:OtOpcUa.FolderRename:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var sdkServer = new OtOpcUaSdkServer(); await using var host = started.Host;
await host.StartAsync(sdkServer, Ct); var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull(); sdkServer.NodeManager.ShouldNotBeNull();
var nm = sdkServer.NodeManager!; var nm = sdkServer.NodeManager!;
@@ -293,15 +301,6 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
nm.TryGetFolder("eq-1")!.DisplayName.Text.ShouldBe("Pump-1"); nm.TryGetFolder("eq-1")!.DisplayName.Text.ShouldBe("Pump-1");
} }
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;
}
/// <summary>Disposes of resources allocated by this test class.</summary> /// <summary>Disposes of resources allocated by this test class.</summary>
public void Dispose() public void Dispose()
{ {
@@ -659,31 +659,24 @@ public sealed class AlarmCommandRouterTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.AlarmCmdTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.AlarmCmdTest", ApplicationName = "OtOpcUa.AlarmCmdTest",
ApplicationUri = $"urn:OtOpcUa.AlarmCmdTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -361,31 +361,24 @@ public sealed class NodeManagerAclEnforcementTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.AclTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.AclTest", ApplicationName = "OtOpcUa.AclTest",
ApplicationUri = $"urn:OtOpcUa.AclTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -96,31 +96,24 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.AlarmIdempotentTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.AlarmIdempotentTest", ApplicationName = "OtOpcUa.AlarmIdempotentTest",
ApplicationUri = $"urn:OtOpcUa.AlarmIdempotentTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance); Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -320,29 +320,24 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable
private async Task<BootedServer> BootAsync() private async Task<BootedServer> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.AlarmSourceFieldsTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.AlarmSourceFieldsTest", ApplicationName = "OtOpcUa.AlarmSourceFieldsTest",
ApplicationUri = $"urn:OtOpcUa.AlarmSourceFieldsTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return new BootedServer(host, server.NodeManager!); return new BootedServer(host, server.NodeManager!);
} }
private static int AllocateFreePort()
{
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
listener.Start();
return ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
}
public void Dispose() public void Dispose()
{ {
if (Directory.Exists(_pkiRoot)) if (Directory.Exists(_pkiRoot))
@@ -147,31 +147,24 @@ public sealed class NodeManagerArrayTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.ArrayTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.ArrayTest", ApplicationName = "OtOpcUa.ArrayTest",
ApplicationUri = $"urn:OtOpcUa.ArrayTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -227,31 +227,24 @@ public sealed class NodeManagerBatchWriteTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.BatchWriteTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.BatchWriteTest", ApplicationName = "OtOpcUa.BatchWriteTest",
ApplicationUri = $"urn:OtOpcUa.BatchWriteTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -123,31 +123,24 @@ public sealed class NodeManagerHistorizeTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.HistorizeTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.HistorizeTest", ApplicationName = "OtOpcUa.HistorizeTest",
ApplicationUri = $"urn:OtOpcUa.HistorizeTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -430,31 +430,24 @@ public sealed class NodeManagerHistoryReadConcurrencyTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.HistoryReadConcTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.HistoryReadConcTest", ApplicationName = "OtOpcUa.HistoryReadConcTest",
ApplicationUri = $"urn:OtOpcUa.HistoryReadConcTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -429,31 +429,24 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.HistoryReadEventsTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.HistoryReadEventsTest", ApplicationName = "OtOpcUa.HistoryReadEventsTest",
ApplicationUri = $"urn:OtOpcUa.HistoryReadEventsTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -509,31 +509,24 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.HistoryPagingTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.HistoryPagingTest", ApplicationName = "OtOpcUa.HistoryPagingTest",
ApplicationUri = $"urn:OtOpcUa.HistoryPagingTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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() public void Dispose()
{ {
if (Directory.Exists(_pkiRoot)) if (Directory.Exists(_pkiRoot))
@@ -572,31 +572,24 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.HistoryReadTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.HistoryReadTest", ApplicationName = "OtOpcUa.HistoryReadTest",
ApplicationUri = $"urn:OtOpcUa.HistoryReadTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -260,31 +260,24 @@ public sealed class NodeManagerKeyUsabilityGateTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.KeyGateTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.KeyGateTest", ApplicationName = "OtOpcUa.KeyGateTest",
ApplicationUri = $"urn:OtOpcUa.KeyGateTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -88,31 +88,24 @@ public sealed class NodeManagerModelChangeFilterTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.ModelChangeFilterTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.ModelChangeFilterTest", ApplicationName = "OtOpcUa.ModelChangeFilterTest",
ApplicationUri = $"urn:OtOpcUa.ModelChangeFilterTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -108,31 +108,24 @@ public sealed class NodeManagerModelChangeOnAddTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.ModelChangeOnAddTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.ModelChangeOnAddTest", ApplicationName = "OtOpcUa.ModelChangeOnAddTest",
ApplicationUri = $"urn:OtOpcUa.ModelChangeOnAddTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance); Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -201,31 +201,24 @@ public sealed class NodeManagerMultiNotifierAlarmTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.MultiNotifierTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.MultiNotifierTest", ApplicationName = "OtOpcUa.MultiNotifierTest",
ApplicationUri = $"urn:OtOpcUa.MultiNotifierTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance); Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -247,31 +247,24 @@ public sealed class NodeManagerNativeAlarmRebuildCarryOverTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager Nm)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager Nm)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.Gitea641Test:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.Gitea641Test", ApplicationName = "OtOpcUa.Gitea641Test",
ApplicationUri = $"urn:OtOpcUa.Gitea641Test:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance); Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server.NodeManager!); return (host, server.NodeManager!);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -123,19 +123,21 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
/// caller disposes it after exercising the guard.</summary> /// caller disposes it after exercising the guard.</summary>
private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager NodeManager)> BuildPreStartNodeManagerAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager NodeManager)> BuildPreStartNodeManagerAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.PreStartGuardTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.PreStartGuardTest", ApplicationName = "OtOpcUa.PreStartGuardTest",
ApplicationUri = $"urn:OtOpcUa.PreStartGuardTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
var live = server.NodeManager!; var live = server.NodeManager!;
// Borrow the SDK's real IServerInternal from the live manager and build a brand-new node manager — // Borrow the SDK's real IServerInternal from the live manager and build a brand-new node manager —
@@ -145,15 +147,6 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
return (host, fresh); return (host, fresh);
} }
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() public void Dispose()
{ {
if (Directory.Exists(_pkiRoot)) if (Directory.Exists(_pkiRoot))
@@ -229,7 +229,6 @@ public sealed class NodeManagerStage6EnforcementTests : IDisposable
finally { await host.DisposeAsync(); } finally { await host.DisposeAsync(); }
} }
/// <summary> /// <summary>
/// Event subscriptions are gated, and the gate resolves its source through the SDK's own /// Event subscriptions are gated, and the gate resolves its source through the SDK's own
/// <c>IsHandleInNamespace</c> rather than a cast. /// <c>IsHandleInNamespace</c> rather than a cast.
@@ -916,31 +915,24 @@ public sealed class NodeManagerStage6EnforcementTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.Acl6Test:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.Acl6Test", ApplicationName = "OtOpcUa.Acl6Test",
ApplicationUri = $"urn:OtOpcUa.Acl6Test:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
private sealed class FakeTimeProvider(DateTime utcNow) : TimeProvider private sealed class FakeTimeProvider(DateTime utcNow) : TimeProvider
{ {
private DateTimeOffset _now = new(utcNow, TimeSpan.Zero); private DateTimeOffset _now = new(utcNow, TimeSpan.Zero);
@@ -282,31 +282,24 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.SurgicalRemoveTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SurgicalRemoveTest", ApplicationName = "OtOpcUa.SurgicalRemoveTest",
ApplicationUri = $"urn:OtOpcUa.SurgicalRemoveTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance); Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -233,31 +233,24 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.SurgicalShapeTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SurgicalShapeTest", ApplicationName = "OtOpcUa.SurgicalShapeTest",
ApplicationUri = $"urn:OtOpcUa.SurgicalShapeTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance); Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -254,31 +254,24 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.WriteRevertTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.WriteRevertTest", ApplicationName = "OtOpcUa.WriteRevertTest",
ApplicationUri = $"urn:OtOpcUa.WriteRevertTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -108,12 +108,14 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
[Fact] [Fact]
public async Task StartAsync_populates_ServerConfiguration_with_all_enabled_profiles() public async Task StartAsync_populates_ServerConfiguration_with_all_enabled_profiles()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.SecAll:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SecAll", ApplicationName = "OtOpcUa.SecAll",
ApplicationUri = $"urn:OtOpcUa.SecAll:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles = EnabledSecurityProfiles =
@@ -124,9 +126,9 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
}, },
AutoAcceptUntrustedClientCertificates = true, AutoAcceptUntrustedClientCertificates = true,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await host.StartAsync(new StandardServer(), Ct); await using var host = started.Host;
var config = host.ApplicationInstance!.ApplicationConfiguration; var config = host.ApplicationInstance!.ApplicationConfiguration;
config.ServerConfiguration.SecurityPolicies.Count.ShouldBe(3); config.ServerConfiguration.SecurityPolicies.Count.ShouldBe(3);
@@ -146,20 +148,22 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
[Fact] [Fact]
public async Task StartAsync_with_only_signandencrypt_omits_None_endpoint() public async Task StartAsync_with_only_signandencrypt_omits_None_endpoint()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.SecHardened:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SecHardened", ApplicationName = "OtOpcUa.SecHardened",
ApplicationUri = $"urn:OtOpcUa.SecHardened:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt }, EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt },
AutoAcceptUntrustedClientCertificates = false, AutoAcceptUntrustedClientCertificates = false,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await host.StartAsync(new StandardServer(), Ct); await using var host = started.Host;
var policies = host.ApplicationInstance!.ApplicationConfiguration.ServerConfiguration.SecurityPolicies; var policies = host.ApplicationInstance!.ApplicationConfiguration.ServerConfiguration.SecurityPolicies;
policies.Count.ShouldBe(1); policies.Count.ShouldBe(1);
@@ -169,15 +173,6 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
.AutoAcceptUntrustedCertificates.ShouldBeFalse(); .AutoAcceptUntrustedCertificates.ShouldBeFalse();
} }
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;
}
/// <summary> /// <summary>
/// Cleans up temporary PKI files. /// Cleans up temporary PKI files.
/// </summary> /// </summary>
@@ -26,19 +26,21 @@ public sealed class OpcUaApplicationHostServerArrayTests
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}"); var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}");
try try
{ {
var options = new OpcUaApplicationHostOptions var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.UnitTest", ApplicationName = "OtOpcUa.UnitTest",
ApplicationUri = "urn:OtOpcUa.UnitTest.NodeA", ApplicationUri = "urn:OtOpcUa.UnitTest.NodeA",
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "127.0.0.1", PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
PeerApplicationUris = new[] { "urn:OtOpcUa.UnitTest.NodeB" }, PeerApplicationUris = new[] { "urn:OtOpcUa.UnitTest.NodeB" },
}; },
NullLogger<OpcUaApplicationHost>.Instance),
var server = new StandardServer(); () => new StandardServer(), CancellationToken.None);
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance); await using var host = started.Host;
await host.StartAsync(server, CancellationToken.None); var server = started.Server;
var serverArray = server.CurrentInstance.ServerObject.ServerArray.Value; var serverArray = server.CurrentInstance.ServerObject.ServerArray.Value;
serverArray.ShouldNotBeNull(); serverArray.ShouldNotBeNull();
@@ -50,13 +52,4 @@ public sealed class OpcUaApplicationHostServerArrayTests
if (Directory.Exists(pkiRoot)) Directory.Delete(pkiRoot, recursive: true); if (Directory.Exists(pkiRoot)) Directory.Delete(pkiRoot, recursive: true);
} }
} }
private static int AllocateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
} }
@@ -22,18 +22,20 @@ public sealed class OpcUaApplicationHostTests : IDisposable
[Fact] [Fact]
public async Task StartAsync_creates_application_certificate_in_pki_own() public async Task StartAsync_creates_application_certificate_in_pki_own()
{ {
await using var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.Test:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.Test", ApplicationName = "OtOpcUa.Test",
ApplicationUri = $"urn:OtOpcUa.Test:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await host.StartAsync(new StandardServer(), Ct); await using var host = started.Host;
var ownCerts = Path.Combine(_pkiRoot, "own", "certs"); var ownCerts = Path.Combine(_pkiRoot, "own", "certs");
Directory.Exists(ownCerts).ShouldBeTrue($"expected SDK to create {ownCerts}"); Directory.Exists(ownCerts).ShouldBeTrue($"expected SDK to create {ownCerts}");
@@ -44,53 +46,42 @@ public sealed class OpcUaApplicationHostTests : IDisposable
[Fact] [Fact]
public async Task StartAsync_reuses_existing_certificate_on_second_boot() public async Task StartAsync_reuses_existing_certificate_on_second_boot()
{ {
var port1 = AllocateFreePort(); var first = await OpcUaHostStart.StartAsync(
await using (var first = new OpcUaApplicationHost( port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.Reuse", ApplicationName = "OtOpcUa.Reuse",
ApplicationUri = "urn:OtOpcUa.Reuse", ApplicationUri = "urn:OtOpcUa.Reuse",
OpcUaPort = port1, OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance)) NullLogger<OpcUaApplicationHost>.Instance),
{ () => new StandardServer(), Ct);
await first.StartAsync(new StandardServer(), Ct); await first.Host.DisposeAsync();
}
var ownCerts = Path.Combine(_pkiRoot, "own", "certs"); var ownCerts = Path.Combine(_pkiRoot, "own", "certs");
var firstFiles = Directory.GetFiles(ownCerts).OrderBy(f => f).ToArray(); var firstFiles = Directory.GetFiles(ownCerts).OrderBy(f => f).ToArray();
firstFiles.ShouldNotBeEmpty(); firstFiles.ShouldNotBeEmpty();
var port2 = AllocateFreePort(); var second = await OpcUaHostStart.StartAsync(
await using (var second = new OpcUaApplicationHost( port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.Reuse", ApplicationName = "OtOpcUa.Reuse",
ApplicationUri = "urn:OtOpcUa.Reuse", ApplicationUri = "urn:OtOpcUa.Reuse",
OpcUaPort = port2, OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance)) NullLogger<OpcUaApplicationHost>.Instance),
{ () => new StandardServer(), Ct);
await second.StartAsync(new StandardServer(), Ct); await second.Host.DisposeAsync();
}
var secondFiles = Directory.GetFiles(ownCerts).OrderBy(f => f).ToArray(); var secondFiles = Directory.GetFiles(ownCerts).OrderBy(f => f).ToArray();
secondFiles.ShouldBe(firstFiles, "expected the second boot to reuse the cert from the first, not create a new one"); secondFiles.ShouldBe(firstFiles, "expected the second boot to reuse the cert from the first, not create a new one");
} }
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;
}
/// <summary>Cleans up the temporary PKI directory.</summary> /// <summary>Cleans up the temporary PKI directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -0,0 +1,81 @@
using System.Net;
using System.Net.Sockets;
using Opc.Ua;
using Opc.Ua.Server;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
namespace ZB.MOM.WW.OtOpcUa;
/// <summary>
/// Starts an <see cref="OpcUaApplicationHost"/> on a freshly probed port, retrying on a bind race.
/// Probing a free port (bind 0, read, release) and handing it to the SDK later is a TOCTOU: under a
/// highly parallel test run another test can take the port in between, and the SDK then fails with
/// <c>Failed to establish tcp listener sockets for Ipv4 and IPv6.</c>. A <see cref="StandardServer"/>
/// that failed to start is not safe to start again, so every attempt builds a NEW host and server on
/// a NEW port and the caller reads the port off the options it builds, never from a cached value.
/// This file is compiled into each test project that boots a real SDK server (linked, not a library).
/// </summary>
internal static class OpcUaHostStart
{
private const int DefaultAttempts = 5;
/// <summary>Probes a currently free loopback TCP port. Racy by nature - pair with <see cref="StartAsync{TServer}"/>.</summary>
public static int AllocateFreePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
/// <summary>
/// Builds a host (via <paramref name="createHost"/>, given the port to bind) and a server (via
/// <paramref name="createServer"/>) and starts them, retrying with a new port, host and server when
/// the bind fails. Any other failure, or the last bind failure, propagates.
/// </summary>
/// <returns>The started host and server. The caller owns disposal of the host.</returns>
public static async Task<(OpcUaApplicationHost Host, TServer Server)> StartAsync<TServer>(
Func<int, OpcUaApplicationHost> createHost,
Func<TServer> createServer,
CancellationToken ct,
int maxAttempts = DefaultAttempts)
where TServer : StandardServer
{
for (var attempt = 1; ; attempt++)
{
var host = createHost(AllocateFreePort());
var server = createServer();
try
{
await host.StartAsync(server, ct);
return (host, server);
}
catch (Exception ex) when (attempt < maxAttempts && IsBindFailure(ex))
{
try { await host.DisposeAsync(); }
catch { /* the half-started host is being discarded */ }
}
catch
{
try { await host.DisposeAsync(); }
catch { /* surface the original failure */ }
throw;
}
}
}
/// <summary>True when the exception chain says the OPC UA listener could not bind its port.</summary>
public static bool IsBindFailure(Exception? ex)
{
for (; ex is not null; ex = ex.InnerException)
{
if (ex is SocketException { SocketErrorCode: SocketError.AddressAlreadyInUse }) return true;
if (ex is ServiceResultException sre
&& (sre.StatusCode == StatusCodes.BadNoCommunication
|| sre.Message.Contains("Failed to establish tcp listener", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
}
@@ -189,31 +189,24 @@ public sealed class OtOpcUaNodeManagerWriteStatusesTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.WriteStatusesTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.WriteStatusesTest", ApplicationName = "OtOpcUa.WriteStatusesTest",
ApplicationUri = $"urn:OtOpcUa.WriteStatusesTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -704,31 +704,24 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{ {
var host = new OpcUaApplicationHost( var applicationUri = $"urn:OtOpcUa.SinkTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SinkTest", ApplicationName = "OtOpcUa.SinkTest",
ApplicationUri = $"urn:OtOpcUa.SinkTest:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var server = new OtOpcUaSdkServer(); var host = started.Host;
await host.StartAsync(server, Ct); var server = started.Server;
return (host, server); return (host, server);
} }
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;
}
/// <summary>Cleans up the PKI root directory.</summary> /// <summary>Cleans up the PKI root directory.</summary>
public void Dispose() public void Dispose()
{ {
@@ -23,19 +23,21 @@ public sealed class SdkServiceLevelPublisherTests : IDisposable
[Fact] [Fact]
public async Task Publish_writes_value_to_Server_ServiceLevel_variable() public async Task Publish_writes_value_to_Server_ServiceLevel_variable()
{ {
var server = new StandardServer(); var applicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}";
await using var host = new OpcUaApplicationHost( var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SvcLevel", ApplicationName = "OtOpcUa.SvcLevel",
ApplicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await host.StartAsync(server, Ct); await using var host = started.Host;
var server = started.Server;
var publisher = new SdkServiceLevelPublisher( var publisher = new SdkServiceLevelPublisher(
server.CurrentInstance, server.CurrentInstance,
@@ -52,19 +54,21 @@ public sealed class SdkServiceLevelPublisherTests : IDisposable
[Fact] [Fact]
public async Task Publish_is_idempotent_when_called_multiple_times() public async Task Publish_is_idempotent_when_called_multiple_times()
{ {
var server = new StandardServer(); var applicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}";
await using var host = new OpcUaApplicationHost( var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions new OpcUaApplicationHostOptions
{ {
ApplicationName = "OtOpcUa.SvcLevel.Idem", ApplicationName = "OtOpcUa.SvcLevel.Idem",
ApplicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}", ApplicationUri = applicationUri,
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot, PkiStoreRoot = _pkiRoot,
}, },
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await host.StartAsync(server, Ct); await using var host = started.Host;
var server = started.Server;
var publisher = new SdkServiceLevelPublisher( var publisher = new SdkServiceLevelPublisher(
server.CurrentInstance, server.CurrentInstance,
NullLogger<SdkServiceLevelPublisher>.Instance); NullLogger<SdkServiceLevelPublisher>.Instance);
@@ -76,15 +80,6 @@ public sealed class SdkServiceLevelPublisherTests : IDisposable
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)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;
}
/// <summary>Disposes and cleans up the test resources.</summary> /// <summary>Disposes and cleans up the test resources.</summary>
public void Dispose() public void Dispose()
{ {
@@ -31,11 +31,13 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
var pkiRoot = AllocatePkiRoot(); var pkiRoot = AllocatePkiRoot();
try try
{ {
var server = new StandardServer(); var started = await OpcUaHostStart.StartAsync(
await using var host = new OpcUaApplicationHost( port => new OpcUaApplicationHost(
BuildOptions("PrimaryLeader", pkiRoot), BuildOptions("PrimaryLeader", pkiRoot, port),
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
await host.StartAsync(server, Ct); () => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var deferred = new DeferredServiceLevelPublisher(); var deferred = new DeferredServiceLevelPublisher();
deferred.SetInner(new SdkServiceLevelPublisher( deferred.SetInner(new SdkServiceLevelPublisher(
@@ -71,11 +73,13 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
var pkiRoot = AllocatePkiRoot(); var pkiRoot = AllocatePkiRoot();
try try
{ {
var server = new StandardServer(); var started = await OpcUaHostStart.StartAsync(
await using var host = new OpcUaApplicationHost( port => new OpcUaApplicationHost(
BuildOptions("Secondary", pkiRoot), BuildOptions("Secondary", pkiRoot, port),
NullLogger<OpcUaApplicationHost>.Instance); NullLogger<OpcUaApplicationHost>.Instance),
await host.StartAsync(server, Ct); () => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var deferred = new DeferredServiceLevelPublisher(); var deferred = new DeferredServiceLevelPublisher();
deferred.SetInner(new SdkServiceLevelPublisher( deferred.SetInner(new SdkServiceLevelPublisher(
@@ -104,12 +108,12 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
} }
} }
private static OpcUaApplicationHostOptions BuildOptions(string name, string pkiRoot) => private static OpcUaApplicationHostOptions BuildOptions(string name, string pkiRoot, int port) =>
new() new()
{ {
ApplicationName = $"OtOpcUa.E2E.{name}", ApplicationName = $"OtOpcUa.E2E.{name}",
ApplicationUri = $"urn:OtOpcUa.E2E.{name}:{Guid.NewGuid():N}", ApplicationUri = $"urn:OtOpcUa.E2E.{name}:{Path.GetFileName(pkiRoot)}", // stable across bind-race retries
OpcUaPort = AllocateFreePort(), OpcUaPort = port,
PublicHostname = "localhost", PublicHostname = "localhost",
PkiStoreRoot = pkiRoot, PkiStoreRoot = pkiRoot,
}; };
@@ -126,15 +130,6 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
} }
} }
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;
}
private void AwaitAssertion(Action assertion) private void AwaitAssertion(Action assertion)
{ {
var deadline = DateTime.UtcNow.AddSeconds(3); var deadline = DateTime.UtcNow.AddSeconds(3);
@@ -36,4 +36,9 @@
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.FOCAS\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj"/> <ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.FOCAS\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Shared bind-race retry helper for tests that boot a real SDK server on a probed port. -->
<Compile Include="..\ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests\OpcUaHostStart.cs" Link="OpcUaHostStart.cs"/>
</ItemGroup>
</Project> </Project>