test: retry OPC UA host start on a port bind race (TOCTOU in AllocateFreePort)

The tests probe a free port (bind 0, release) and start the SDK server later; under a
highly parallel run another test can take the port in between and the SDK fails with
'Failed to establish tcp listener sockets for Ipv4 and IPv6.'. OpcUaHostStart.StartAsync
builds a fresh host + server on a fresh port per attempt (up to 5) when the failure is a
bind failure; all 45 start sites in OpcUaServer.Tests, OpcUaServer.IntegrationTests and
Runtime.Tests use it. Per-attempt GUID ApplicationUris are hoisted out of the host factory
because a changed URI against a reused PKI store fails with 'Cannot access private key'.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 22:30:55 -04:00
parent d83e8f21aa
commit f02067d295
41 changed files with 948 additions and 1078 deletions
@@ -998,23 +998,26 @@ public sealed class AclEventSubscriptionEnforcementTests
public static async Task<Rig> StartAsync(string name, CancellationToken ct)
{
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort();
rig._server = new OtOpcUaSdkServer();
rig._host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
port =>
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
OpcUaPort = rig._port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true,
rig._port = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance,
new KeyPresentingAuthenticator());
},
NullLogger<OpcUaApplicationHost>.Instance,
new KeyPresentingAuthenticator());
await rig._host.StartAsync(rig._server, ct);
() => new OtOpcUaSdkServer(), ct);
rig._sink = new SdkAddressSpaceSink(rig.Nm);
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>
/// Admits the fixed <c>apikey</c> username as <see cref="KeyPrincipal"/> carrying
/// <see cref="KeyId"/>; anonymous falls through to the interface default (the
@@ -154,23 +154,26 @@ public sealed class AclKeyExpiryEnforcementTests
public static async Task<Rig> StartAsync(string name, CancellationToken ct)
{
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort();
rig._server = new OtOpcUaSdkServer();
rig._host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
port =>
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
OpcUaPort = rig._port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true,
rig._port = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance,
new KeyPresentingAuthenticator());
},
NullLogger<OpcUaApplicationHost>.Instance,
new KeyPresentingAuthenticator());
await rig._host.StartAsync(rig._server, ct);
() => new OtOpcUaSdkServer(), ct);
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>
/// Admits the fixed <c>apikey</c> username as <see cref="Principal"/>, carrying
/// <see cref="KeyId"/>.
@@ -303,146 +303,141 @@ public sealed class AclSubscriptionEnforcementTests
public static async Task<Rig> StartAsync(string name, CancellationToken ct)
{
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort();
rig._server = new OtOpcUaSdkServer();
rig._host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
port =>
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
OpcUaPort = rig._port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
await rig._host.StartAsync(rig._server, ct);
return rig;
}
/// <summary>Switches enforcement on with one grant, returning the state so a test can redeploy.</summary>
public DataPlaneAuthorityState Enforce(
NodeAcl grant, IReadOnlyDictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>? scopes = null)
{
var state = new DataPlaneAuthorityState();
state.Apply(Cluster, "rev-1", [], [grant], scopes ?? NoScopes);
// BOTH must be set: Authorizer decides, and DataPlaneAuthority is what carries the Changed
// signal that reaches live subscriptions. Wiring only the first passes every test that makes a
// request and none that revokes.
Nm.DataPlaneAuthority = state;
Nm.Authorizer = new DataPlaneAuthorizer(
state, new TriePermissionEvaluator(state.Tries), enabled: true, NullLogger.Instance);
return state;
}
public void Materialise()
{
Nm.EnsureFolder("Plant", null, "Plant", AddressSpaceRealm.Raw);
Nm.EnsureFolder("Plant/Modbus", "Plant", "Modbus", AddressSpaceRealm.Raw);
Nm.EnsureFolder(Device, "Plant/Modbus", "dev1", AddressSpaceRealm.Raw);
Nm.EnsureVariable(Tag, Device, "Speed", "Float", writable: false, AddressSpaceRealm.Raw);
}
public void MaterialiseTwoDevices()
{
Materialise();
Nm.EnsureFolder("Plant/Modbus/dev9", "Plant/Modbus", "dev9", AddressSpaceRealm.Raw);
Nm.EnsureVariable(
"Plant/Modbus/dev9/Torque", "Plant/Modbus/dev9", "Torque", "Float",
writable: false, AddressSpaceRealm.Raw);
}
public void Write(float value) =>
Nm.WriteValue(Tag, value, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
public ushort RawNamespace(ISession session)
{
var ns = (ushort)session.NamespaceUris.GetIndex(V3NodeIds.RawNamespaceUri);
ns.ShouldBeGreaterThan((ushort)0);
return ns;
}
public NodeId TagNodeId(ISession session) => new(Tag, RawNamespace(session));
public async Task<(Subscription Subscription, List<DataValue> Received)> SubscribeAsync(
ISession session, CancellationToken ct, bool expectFailure = false)
{
var received = new List<DataValue>();
var gate = new object();
var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100 };
session.AddSubscription(subscription);
await subscription.CreateAsync(ct);
var item = new MonitoredItem(subscription.DefaultItem)
rig._port = port;
return new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
return rig;
}
/// <summary>Switches enforcement on with one grant, returning the state so a test can redeploy.</summary>
public DataPlaneAuthorityState Enforce(
NodeAcl grant, IReadOnlyDictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>? scopes = null)
{
StartNodeId = TagNodeId(session),
AttributeId = Attributes.Value,
SamplingInterval = 50,
QueueSize = 10,
};
item.Notification += (mi, _) =>
var state = new DataPlaneAuthorityState();
state.Apply(Cluster, "rev-1", [], [grant], scopes ?? NoScopes);
// BOTH must be set: Authorizer decides, and DataPlaneAuthority is what carries the Changed
// signal that reaches live subscriptions. Wiring only the first passes every test that makes a
// request and none that revokes.
Nm.DataPlaneAuthority = state;
Nm.Authorizer = new DataPlaneAuthorizer(
state, new TriePermissionEvaluator(state.Tries), enabled: true, NullLogger.Instance);
return state;
}
public void Materialise()
{
foreach (var v in mi.DequeueValues()) lock (gate) received.Add(v);
};
subscription.AddItem(item);
await subscription.ApplyChangesAsync(ct);
if (!expectFailure) item.Status.Error.ShouldBeNull();
// The list is only ever appended under `gate` and read from the test thread between awaits;
// a snapshot-on-read wrapper would hide ordering rather than help.
return (subscription, received);
}
public async Task<ISession> ConnectAsync(CancellationToken ct)
{
var appConfig = new ApplicationConfiguration
Nm.EnsureFolder("Plant", null, "Plant", AddressSpaceRealm.Raw);
Nm.EnsureFolder("Plant/Modbus", "Plant", "Modbus", AddressSpaceRealm.Raw);
Nm.EnsureFolder(Device, "Plant/Modbus", "dev1", AddressSpaceRealm.Raw);
Nm.EnsureVariable(Tag, Device, "Speed", "Float", writable: false, AddressSpaceRealm.Raw);
}
public void MaterialiseTwoDevices()
{
ApplicationName = "OtOpcUa.AclClient",
ApplicationUri = $"urn:OtOpcUa.AclClient.{Guid.NewGuid():N}",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = TestClientSecurity.Build(TestClientSecurity.AllocatePkiRoot()),
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
};
await appConfig.ValidateAsync(ApplicationType.Client, ct);
appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
var endpoint = await CoreClientUtils.SelectEndpointAsync(
appConfig, $"opc.tcp://127.0.0.1:{_port}/OtOpcUa", false, DefaultTelemetry.Create(_ => { }), ct);
return await new DefaultSessionFactory(DefaultTelemetry.Create(_ => { })).CreateAsync(
appConfig,
new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(appConfig)),
updateBeforeConnect: false,
checkDomain: false,
sessionName: "AclSubscriptionEnforcementTests",
sessionTimeout: 60_000,
identity: new UserIdentity(new AnonymousIdentityToken()),
preferredLocales: null,
ct: ct);
}
public async ValueTask DisposeAsync()
{
await _host.DisposeAsync();
if (Directory.Exists(_pkiRoot))
Materialise();
Nm.EnsureFolder("Plant/Modbus/dev9", "Plant/Modbus", "dev9", AddressSpaceRealm.Raw);
Nm.EnsureVariable(
"Plant/Modbus/dev9/Torque", "Plant/Modbus/dev9", "Torque", "Float",
writable: false, AddressSpaceRealm.Raw);
}
public void Write(float value) =>
Nm.WriteValue(Tag, value, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
public ushort RawNamespace(ISession session)
{
try { Directory.Delete(_pkiRoot, recursive: true); }
catch (IOException) { /* best effort */ }
var ns = (ushort)session.NamespaceUris.GetIndex(V3NodeIds.RawNamespaceUri);
ns.ShouldBeGreaterThan((ushort)0);
return ns;
}
public NodeId TagNodeId(ISession session) => new(Tag, RawNamespace(session));
public async Task<(Subscription Subscription, List<DataValue> Received)> SubscribeAsync(
ISession session, CancellationToken ct, bool expectFailure = false)
{
var received = new List<DataValue>();
var gate = new object();
var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100 };
session.AddSubscription(subscription);
await subscription.CreateAsync(ct);
var item = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = TagNodeId(session),
AttributeId = Attributes.Value,
SamplingInterval = 50,
QueueSize = 10,
};
item.Notification += (mi, _) =>
{
foreach (var v in mi.DequeueValues()) lock (gate) received.Add(v);
};
subscription.AddItem(item);
await subscription.ApplyChangesAsync(ct);
if (!expectFailure) item.Status.Error.ShouldBeNull();
// The list is only ever appended under `gate` and read from the test thread between awaits;
// a snapshot-on-read wrapper would hide ordering rather than help.
return (subscription, received);
}
public async Task<ISession> ConnectAsync(CancellationToken ct)
{
var appConfig = new ApplicationConfiguration
{
ApplicationName = "OtOpcUa.AclClient",
ApplicationUri = $"urn:OtOpcUa.AclClient.{Guid.NewGuid():N}",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = TestClientSecurity.Build(TestClientSecurity.AllocatePkiRoot()),
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
};
await appConfig.ValidateAsync(ApplicationType.Client, ct);
appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
var endpoint = await CoreClientUtils.SelectEndpointAsync(
appConfig, $"opc.tcp://127.0.0.1:{_port}/OtOpcUa", false, DefaultTelemetry.Create(_ => { }), ct);
return await new DefaultSessionFactory(DefaultTelemetry.Create(_ => { })).CreateAsync(
appConfig,
new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(appConfig)),
updateBeforeConnect: false,
checkDomain: false,
sessionName: "AclSubscriptionEnforcementTests",
sessionTimeout: 60_000,
identity: new UserIdentity(new AnonymousIdentityToken()),
preferredLocales: null,
ct: ct);
}
public async ValueTask DisposeAsync()
{
await _host.DisposeAsync();
if (Directory.Exists(_pkiRoot))
{
try { Directory.Delete(_pkiRoot, recursive: true); }
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 pkiRootB = Path.Combine(Path.GetTempPath(), $"otopcua-pki-b-{Guid.NewGuid():N}");
var portA = AllocateFreePort();
var portB = AllocateFreePort();
try
{
await using var nodeA = await StartNodeAsync(NodeAUri, portA, pkiRootA, peers: new[] { NodeBUri });
await using var nodeB = await StartNodeAsync(NodeBUri, portB, pkiRootB, peers: new[] { NodeAUri });
var (nodeAHost, portA) = await StartNodeAsync(NodeAUri, pkiRootA, peers: new[] { NodeBUri });
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");
serverArray.ShouldContain(NodeAUri);
@@ -47,24 +47,31 @@ public sealed class DualEndpointTests
}
}
private static async Task<OpcUaApplicationHost> StartNodeAsync(
string applicationUri, int port, string pkiRoot, string[] peers)
private static async Task<(OpcUaApplicationHost Host, int Port)> StartNodeAsync(
string applicationUri, string pkiRoot, string[] peers)
{
var options = new OpcUaApplicationHostOptions
{
ApplicationName = applicationUri,
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
PeerApplicationUris = peers,
};
var server = new StandardServer();
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, CancellationToken.None);
return host;
// 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,
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
PeerApplicationUris = peers,
},
NullLogger<OpcUaApplicationHost>.Instance);
},
() => new StandardServer(), CancellationToken.None);
return (started.Host, boundPort);
}
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);
}
}
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-browse-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-fanout-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-hist-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -178,11 +175,10 @@ public sealed class DualNamespaceAddressSpaceTests
public async Task WriteOperate_gate_is_symmetric_across_both_nodeids()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-write-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -263,23 +259,30 @@ public sealed class DualNamespaceAddressSpaceTests
return response.Results[0];
}
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
int port, string pkiRoot, string appUri, CancellationToken ct)
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
string pkiRoot, string appUri, CancellationToken ct)
{
var options = new OpcUaApplicationHostOptions
{
ApplicationName = appUri,
ApplicationUri = appUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
};
var server = new OtOpcUaSdkServer();
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, ct);
return (server, host);
// 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,
ApplicationUri = appUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
return (started.Server, started.Host, boundPort);
}
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);
}
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)
{
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-identity-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-condclass-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-quality-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -333,23 +330,30 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
}
}
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
int port, string pkiRoot, string appUri, CancellationToken ct)
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
string pkiRoot, string appUri, CancellationToken ct)
{
var options = new OpcUaApplicationHostOptions
{
ApplicationName = appUri,
ApplicationUri = appUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
};
var server = new OtOpcUaSdkServer();
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, ct);
return (server, host);
// 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,
ApplicationUri = appUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
return (started.Server, started.Host, boundPort);
}
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");
}
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)
{
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-evt-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-ovl-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
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;
var sink = new SdkAddressSpaceSink(server.NodeManager!);
@@ -208,23 +206,30 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
}
}
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
int port, string pkiRoot, string appUri, CancellationToken ct)
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
string pkiRoot, string appUri, CancellationToken ct)
{
var options = new OpcUaApplicationHostOptions
{
ApplicationName = appUri,
ApplicationUri = appUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
};
var server = new OtOpcUaSdkServer();
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, ct);
return (server, host);
// 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,
ApplicationUri = appUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
},
() => new OtOpcUaSdkServer(), ct);
return (started.Server, started.Host, boundPort);
}
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");
}
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)
{
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)
{
var rig = new ServerEventRig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
rig._port = AllocateFreePort();
rig._hostOptions = new OpcUaApplicationHostOptions
{
ApplicationName = $"OtOpcUa.{name}",
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
OpcUaPort = rig._port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = rig._pkiRoot,
EnabledSecurityProfiles = encrypted
@@ -64,19 +62,29 @@ internal sealed class ServerEventRig : IAsyncDisposable
: [OpcUaSecurityProfile.None],
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;
}
private async Task BootAsync(CancellationToken ct)
{
_server = new OtOpcUaSdkServer();
_host = new OpcUaApplicationHost(
_hostOptions, NullLogger<OpcUaApplicationHost>.Instance, new KeyPresentingAuthenticator());
_host = NewHost();
await _host.StartAsync(_server, ct);
Sink = new SdkAddressSpaceSink(Nm);
}
private OpcUaApplicationHost NewHost() =>
new(_hostOptions, NullLogger<OpcUaApplicationHost>.Instance, new KeyPresentingAuthenticator());
/// <summary>
/// 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
@@ -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>
/// 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.
@@ -38,7 +38,6 @@ public sealed class SubscriptionSurvivalTests
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 port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
try
@@ -47,15 +46,21 @@ public sealed class SubscriptionSurvivalTests
{
ApplicationName = ServerUri,
ApplicationUri = ServerUri,
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
};
var server = new OtOpcUaSdkServer();
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, ct);
var started = await OpcUaHostStart.StartAsync(
port =>
{
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 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()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-rm-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
try
@@ -154,15 +158,21 @@ public sealed class SubscriptionSurvivalTests
{
ApplicationName = ServerUri,
ApplicationUri = ServerUri + ".Remove",
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
};
var server = new OtOpcUaSdkServer();
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, ct);
var started = await OpcUaHostStart.StartAsync(
port =>
{
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 sink = new SdkAddressSpaceSink(nm);
@@ -234,7 +244,6 @@ public sealed class SubscriptionSurvivalTests
public async Task Subscription_survives_mixed_add_remove_deploy()
{
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-mix-{Guid.NewGuid():N}");
var port = AllocateFreePort();
var ct = TestContext.Current.CancellationToken;
try
@@ -243,15 +252,21 @@ public sealed class SubscriptionSurvivalTests
{
ApplicationName = ServerUri,
ApplicationUri = ServerUri + ".Mixed",
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
AutoAcceptUntrustedClientCertificates = true,
};
var server = new OtOpcUaSdkServer();
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, ct);
var started = await OpcUaHostStart.StartAsync(
port =>
{
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 sink = new SdkAddressSpaceSink(nm);
@@ -372,13 +387,4 @@ public sealed class SubscriptionSurvivalTests
}
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"/>
</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>
@@ -70,19 +70,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact]
public async Task MaterialiseHierarchy_against_real_SDK_node_manager_creates_folder_nodes()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Hierarchy",
ApplicationUri = $"urn:OtOpcUa.Hierarchy:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var sdkServer = new OtOpcUaSdkServer();
await host.StartAsync(sdkServer, Ct);
var applicationUri = $"urn:OtOpcUa.Hierarchy:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Hierarchy",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
await using var host = started.Host;
var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
@@ -116,19 +118,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact]
public async Task MaterialiseEquipmentTags_against_real_SDK_node_manager_is_idempotent()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.EquipmentTags",
ApplicationUri = $"urn:OtOpcUa.EquipmentTags:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var sdkServer = new OtOpcUaSdkServer();
await host.StartAsync(sdkServer, Ct);
var applicationUri = $"urn:OtOpcUa.EquipmentTags:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.EquipmentTags",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
await using var host = started.Host;
var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
@@ -172,19 +176,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact]
public async Task Raw_and_uns_variables_materialise_end_to_end_against_real_SDK()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.EquipmentE2e",
ApplicationUri = $"urn:OtOpcUa.EquipmentE2e:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var sdkServer = new OtOpcUaSdkServer();
await host.StartAsync(sdkServer, Ct);
var applicationUri = $"urn:OtOpcUa.EquipmentE2e:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.EquipmentE2e",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
await using var host = started.Host;
var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull();
// The v3 raw chain: RawFolder "Plant" → Driver "Modbus" → Device "dev1" → Tag "Speed"
@@ -238,19 +244,21 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
[Fact]
public async Task Area_and_line_rename_updates_existing_folder_display_name_in_place_against_real_SDK()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.FolderRename",
ApplicationUri = $"urn:OtOpcUa.FolderRename:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var sdkServer = new OtOpcUaSdkServer();
await host.StartAsync(sdkServer, Ct);
var applicationUri = $"urn:OtOpcUa.FolderRename:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.FolderRename",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
await using var host = started.Host;
var sdkServer = started.Server;
sdkServer.NodeManager.ShouldNotBeNull();
var nm = sdkServer.NodeManager!;
@@ -293,15 +301,6 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
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>
public void Dispose()
{
@@ -659,31 +659,24 @@ public sealed class AlarmCommandRouterTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AlarmCmdTest",
ApplicationUri = $"urn:OtOpcUa.AlarmCmdTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.AlarmCmdTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AlarmCmdTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -361,31 +361,24 @@ public sealed class NodeManagerAclEnforcementTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AclTest",
ApplicationUri = $"urn:OtOpcUa.AclTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.AclTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AclTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -96,31 +96,24 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AlarmIdempotentTest",
ApplicationUri = $"urn:OtOpcUa.AlarmIdempotentTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.AlarmIdempotentTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AlarmIdempotentTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -320,29 +320,24 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable
private async Task<BootedServer> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AlarmSourceFieldsTest",
ApplicationUri = $"urn:OtOpcUa.AlarmSourceFieldsTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.AlarmSourceFieldsTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.AlarmSourceFieldsTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.Server;
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()
{
if (Directory.Exists(_pkiRoot))
@@ -147,31 +147,24 @@ public sealed class NodeManagerArrayTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.ArrayTest",
ApplicationUri = $"urn:OtOpcUa.ArrayTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.ArrayTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.ArrayTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -227,31 +227,24 @@ public sealed class NodeManagerBatchWriteTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.BatchWriteTest",
ApplicationUri = $"urn:OtOpcUa.BatchWriteTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.BatchWriteTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.BatchWriteTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -123,31 +123,24 @@ public sealed class NodeManagerHistorizeTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistorizeTest",
ApplicationUri = $"urn:OtOpcUa.HistorizeTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.HistorizeTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistorizeTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -430,31 +430,24 @@ public sealed class NodeManagerHistoryReadConcurrencyTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryReadConcTest",
ApplicationUri = $"urn:OtOpcUa.HistoryReadConcTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.HistoryReadConcTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryReadConcTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -429,31 +429,24 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryReadEventsTest",
ApplicationUri = $"urn:OtOpcUa.HistoryReadEventsTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.HistoryReadEventsTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryReadEventsTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -509,31 +509,24 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryPagingTest",
ApplicationUri = $"urn:OtOpcUa.HistoryPagingTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.HistoryPagingTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryPagingTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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()
{
if (Directory.Exists(_pkiRoot))
@@ -572,31 +572,24 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryReadTest",
ApplicationUri = $"urn:OtOpcUa.HistoryReadTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.HistoryReadTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.HistoryReadTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -260,31 +260,24 @@ public sealed class NodeManagerKeyUsabilityGateTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.KeyGateTest",
ApplicationUri = $"urn:OtOpcUa.KeyGateTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.KeyGateTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.KeyGateTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -88,31 +88,24 @@ public sealed class NodeManagerModelChangeFilterTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.ModelChangeFilterTest",
ApplicationUri = $"urn:OtOpcUa.ModelChangeFilterTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.ModelChangeFilterTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.ModelChangeFilterTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -108,31 +108,24 @@ public sealed class NodeManagerModelChangeOnAddTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.ModelChangeOnAddTest",
ApplicationUri = $"urn:OtOpcUa.ModelChangeOnAddTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.ModelChangeOnAddTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.ModelChangeOnAddTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -201,31 +201,24 @@ public sealed class NodeManagerMultiNotifierAlarmTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.MultiNotifierTest",
ApplicationUri = $"urn:OtOpcUa.MultiNotifierTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.MultiNotifierTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.MultiNotifierTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -247,31 +247,24 @@ public sealed class NodeManagerNativeAlarmRebuildCarryOverTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager Nm)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Gitea641Test",
ApplicationUri = $"urn:OtOpcUa.Gitea641Test:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.Gitea641Test:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Gitea641Test",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.Server;
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>
public void Dispose()
{
@@ -123,19 +123,21 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
/// caller disposes it after exercising the guard.</summary>
private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager NodeManager)> BuildPreStartNodeManagerAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.PreStartGuardTest",
ApplicationUri = $"urn:OtOpcUa.PreStartGuardTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.PreStartGuardTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.PreStartGuardTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.Server;
var live = server.NodeManager!;
// 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);
}
private static int AllocateFreePort()
{
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
listener.Start();
var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
public void Dispose()
{
if (Directory.Exists(_pkiRoot))
@@ -229,7 +229,6 @@ public sealed class NodeManagerStage6EnforcementTests : IDisposable
finally { await host.DisposeAsync(); }
}
/// <summary>
/// Event subscriptions are gated, and the gate resolves its source through the SDK's own
/// <c>IsHandleInNamespace</c> rather than a cast.
@@ -916,31 +915,24 @@ public sealed class NodeManagerStage6EnforcementTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Acl6Test",
ApplicationUri = $"urn:OtOpcUa.Acl6Test:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.Acl6Test:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Acl6Test",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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 DateTimeOffset _now = new(utcNow, TimeSpan.Zero);
@@ -282,31 +282,24 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SurgicalRemoveTest",
ApplicationUri = $"urn:OtOpcUa.SurgicalRemoveTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.SurgicalRemoveTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SurgicalRemoveTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -233,31 +233,24 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SurgicalShapeTest",
ApplicationUri = $"urn:OtOpcUa.SurgicalShapeTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.SurgicalShapeTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SurgicalShapeTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
Microsoft.Extensions.Logging.Abstractions.NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -254,31 +254,24 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.WriteRevertTest",
ApplicationUri = $"urn:OtOpcUa.WriteRevertTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.WriteRevertTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.WriteRevertTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -108,25 +108,27 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
[Fact]
public async Task StartAsync_populates_ServerConfiguration_with_all_enabled_profiles()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SecAll",
ApplicationUri = $"urn:OtOpcUa.SecAll:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles =
var applicationUri = $"urn:OtOpcUa.SecAll:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
OpcUaSecurityProfile.None,
OpcUaSecurityProfile.Basic256Sha256Sign,
OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
ApplicationName = "OtOpcUa.SecAll",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles =
{
OpcUaSecurityProfile.None,
OpcUaSecurityProfile.Basic256Sha256Sign,
OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
},
AutoAcceptUntrustedClientCertificates = true,
},
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(new StandardServer(), Ct);
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var config = host.ApplicationInstance!.ApplicationConfiguration;
config.ServerConfiguration.SecurityPolicies.Count.ShouldBe(3);
@@ -146,20 +148,22 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
[Fact]
public async Task StartAsync_with_only_signandencrypt_omits_None_endpoint()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SecHardened",
ApplicationUri = $"urn:OtOpcUa.SecHardened:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt },
AutoAcceptUntrustedClientCertificates = false,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(new StandardServer(), Ct);
var applicationUri = $"urn:OtOpcUa.SecHardened:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SecHardened",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt },
AutoAcceptUntrustedClientCertificates = false,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var policies = host.ApplicationInstance!.ApplicationConfiguration.ServerConfiguration.SecurityPolicies;
policies.Count.ShouldBe(1);
@@ -169,15 +173,6 @@ public sealed class OpcUaApplicationHostSecurityTests : IDisposable
.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>
/// Cleans up temporary PKI files.
/// </summary>
@@ -26,19 +26,21 @@ public sealed class OpcUaApplicationHostServerArrayTests
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}");
try
{
var options = new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.UnitTest",
ApplicationUri = "urn:OtOpcUa.UnitTest.NodeA",
OpcUaPort = AllocateFreePort(),
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
PeerApplicationUris = new[] { "urn:OtOpcUa.UnitTest.NodeB" },
};
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, CancellationToken.None);
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.UnitTest",
ApplicationUri = "urn:OtOpcUa.UnitTest.NodeA",
OpcUaPort = port,
PublicHostname = "127.0.0.1",
PkiStoreRoot = pkiRoot,
PeerApplicationUris = new[] { "urn:OtOpcUa.UnitTest.NodeB" },
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), CancellationToken.None);
await using var host = started.Host;
var server = started.Server;
var serverArray = server.CurrentInstance.ServerObject.ServerArray.Value;
serverArray.ShouldNotBeNull();
@@ -50,13 +52,4 @@ public sealed class OpcUaApplicationHostServerArrayTests
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]
public async Task StartAsync_creates_application_certificate_in_pki_own()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Test",
ApplicationUri = $"urn:OtOpcUa.Test:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(new StandardServer(), Ct);
var applicationUri = $"urn:OtOpcUa.Test:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Test",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var ownCerts = Path.Combine(_pkiRoot, "own", "certs");
Directory.Exists(ownCerts).ShouldBeTrue($"expected SDK to create {ownCerts}");
@@ -44,53 +46,42 @@ public sealed class OpcUaApplicationHostTests : IDisposable
[Fact]
public async Task StartAsync_reuses_existing_certificate_on_second_boot()
{
var port1 = AllocateFreePort();
await using (var first = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Reuse",
ApplicationUri = "urn:OtOpcUa.Reuse",
OpcUaPort = port1,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance))
{
await first.StartAsync(new StandardServer(), Ct);
}
var first = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Reuse",
ApplicationUri = "urn:OtOpcUa.Reuse",
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await first.Host.DisposeAsync();
var ownCerts = Path.Combine(_pkiRoot, "own", "certs");
var firstFiles = Directory.GetFiles(ownCerts).OrderBy(f => f).ToArray();
firstFiles.ShouldNotBeEmpty();
var port2 = AllocateFreePort();
await using (var second = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Reuse",
ApplicationUri = "urn:OtOpcUa.Reuse",
OpcUaPort = port2,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance))
{
await second.StartAsync(new StandardServer(), Ct);
}
var second = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.Reuse",
ApplicationUri = "urn:OtOpcUa.Reuse",
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await second.Host.DisposeAsync();
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");
}
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>
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()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.WriteStatusesTest",
ApplicationUri = $"urn:OtOpcUa.WriteStatusesTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.WriteStatusesTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.WriteStatusesTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -704,31 +704,24 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
{
var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SinkTest",
ApplicationUri = $"urn:OtOpcUa.SinkTest:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
var server = new OtOpcUaSdkServer();
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.SinkTest:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SinkTest",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new OtOpcUaSdkServer(), Ct);
var host = started.Host;
var server = started.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>
public void Dispose()
{
@@ -23,19 +23,21 @@ public sealed class SdkServiceLevelPublisherTests : IDisposable
[Fact]
public async Task Publish_writes_value_to_Server_ServiceLevel_variable()
{
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SvcLevel",
ApplicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SvcLevel",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var publisher = new SdkServiceLevelPublisher(
server.CurrentInstance,
@@ -52,19 +54,21 @@ public sealed class SdkServiceLevelPublisherTests : IDisposable
[Fact]
public async Task Publish_is_idempotent_when_called_multiple_times()
{
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SvcLevel.Idem",
ApplicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, Ct);
var applicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SvcLevel.Idem",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var publisher = new SdkServiceLevelPublisher(
server.CurrentInstance,
NullLogger<SdkServiceLevelPublisher>.Instance);
@@ -76,15 +80,6 @@ public sealed class SdkServiceLevelPublisherTests : IDisposable
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>
public void Dispose()
{
@@ -31,11 +31,13 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
var pkiRoot = AllocatePkiRoot();
try
{
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(
BuildOptions("PrimaryLeader", pkiRoot),
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, Ct);
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
BuildOptions("PrimaryLeader", pkiRoot, port),
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var deferred = new DeferredServiceLevelPublisher();
deferred.SetInner(new SdkServiceLevelPublisher(
@@ -71,11 +73,13 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
var pkiRoot = AllocatePkiRoot();
try
{
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(
BuildOptions("Secondary", pkiRoot),
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, Ct);
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
BuildOptions("Secondary", pkiRoot, port),
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var deferred = new DeferredServiceLevelPublisher();
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()
{
ApplicationName = $"OtOpcUa.E2E.{name}",
ApplicationUri = $"urn:OtOpcUa.E2E.{name}:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
ApplicationUri = $"urn:OtOpcUa.E2E.{name}:{Path.GetFileName(pkiRoot)}", // stable across bind-race retries
OpcUaPort = port,
PublicHostname = "localhost",
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)
{
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"/>
</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>