From f02067d2956ea31f22ce71d7cb67f6422b979d75 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 22:30:55 -0400 Subject: [PATCH] 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 --- .../AclEventSubscriptionEnforcementTests.cs | 42 ++- .../AclKeyExpiryEnforcementTests.cs | 42 ++- .../AclSubscriptionEnforcementTests.cs | 265 +++++++++--------- .../DualEndpointTests.cs | 58 ++-- .../DualNamespaceAddressSpaceTests.cs | 60 ++-- ...iveAlarmEventIdentityFieldDeliveryTests.cs | 57 ++-- ...iveAlarmMultiNotifierEventDeliveryTests.cs | 54 ++-- .../ServerEventRig.cs | 27 +- .../SubscriptionSurvivalTests.cs | 54 ++-- ...tOpcUa.OpcUaServer.IntegrationTests.csproj | 5 + .../AddressSpaceApplierHierarchyTests.cs | 121 ++++---- .../AlarmCommandRouterTests.cs | 37 +-- .../NodeManagerAclEnforcementTests.cs | 37 +-- ...eManagerAlarmIdempotentMaterialiseTests.cs | 37 +-- .../NodeManagerAlarmSourceFieldsTests.cs | 35 +-- .../NodeManagerArrayTests.cs | 37 +-- .../NodeManagerBatchWriteTests.cs | 37 +-- .../NodeManagerHistorizeTests.cs | 37 +-- .../NodeManagerHistoryReadConcurrencyTests.cs | 37 +-- .../NodeManagerHistoryReadEventsTests.cs | 37 +-- .../NodeManagerHistoryReadPagingTests.cs | 37 +-- .../NodeManagerHistoryReadTests.cs | 37 +-- .../NodeManagerKeyUsabilityGateTests.cs | 37 +-- .../NodeManagerModelChangeFilterTests.cs | 37 +-- .../NodeManagerModelChangeOnAddTests.cs | 37 +-- .../NodeManagerMultiNotifierAlarmTests.cs | 37 +-- ...ManagerNativeAlarmRebuildCarryOverTests.cs | 37 +-- .../NodeManagerPreStartGuardTests.cs | 37 +-- .../NodeManagerStage6EnforcementTests.cs | 38 +-- .../NodeManagerSurgicalRemoveTests.cs | 37 +-- .../NodeManagerSurgicalShapeUpdateTests.cs | 37 +-- .../NodeManagerWriteRevertTests.cs | 37 +-- .../OpcUaApplicationHostSecurityTests.cs | 75 +++-- .../OpcUaApplicationHostServerArrayTests.cs | 37 +-- .../OpcUaApplicationHostTests.cs | 89 +++--- .../OpcUaHostStart.cs | 81 ++++++ .../OtOpcUaNodeManagerWriteStatusesTests.cs | 37 +-- .../SdkAddressSpaceSinkTests.cs | 37 +-- .../SdkServiceLevelPublisherTests.cs | 65 ++--- .../OpcUa/ServiceLevelEndToEndTests.cs | 39 ++- .../ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj | 5 + 41 files changed, 948 insertions(+), 1078 deletions(-) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaHostStart.cs diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclEventSubscriptionEnforcementTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclEventSubscriptionEnforcementTests.cs index 0104c75d..b0511ec7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclEventSubscriptionEnforcementTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclEventSubscriptionEnforcementTests.cs @@ -998,23 +998,26 @@ public sealed class AclEventSubscriptionEnforcementTests public static async Task 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.Instance, + new KeyPresentingAuthenticator()); }, - NullLogger.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; - } - /// /// Admits the fixed apikey username as carrying /// ; anonymous falls through to the interface default (the diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclKeyExpiryEnforcementTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclKeyExpiryEnforcementTests.cs index cb97261d..f2dfa888 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclKeyExpiryEnforcementTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclKeyExpiryEnforcementTests.cs @@ -154,23 +154,26 @@ public sealed class AclKeyExpiryEnforcementTests public static async Task 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.Instance, + new KeyPresentingAuthenticator()); }, - NullLogger.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; - } - /// /// Admits the fixed apikey username as , carrying /// . diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclSubscriptionEnforcementTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclSubscriptionEnforcementTests.cs index a161e59b..f2d0ee31 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclSubscriptionEnforcementTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AclSubscriptionEnforcementTests.cs @@ -303,146 +303,141 @@ public sealed class AclSubscriptionEnforcementTests public static async Task 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.Instance); - - await rig._host.StartAsync(rig._server, ct); - return rig; - } - - /// Switches enforcement on with one grant, returning the state so a test can redeploy. - public DataPlaneAuthorityState Enforce( - NodeAcl grant, IReadOnlyDictionary>? 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 Received)> SubscribeAsync( - ISession session, CancellationToken ct, bool expectFailure = false) - { - var received = new List(); - 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.Instance); + }, + () => new OtOpcUaSdkServer(), ct); + return rig; + } + + /// Switches enforcement on with one grant, returning the state so a test can redeploy. + public DataPlaneAuthorityState Enforce( + NodeAcl grant, IReadOnlyDictionary>? 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 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 Received)> SubscribeAsync( + ISession session, CancellationToken ct, bool expectFailure = false) + { + var received = new List(); + 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 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; - } } -} + \ No newline at end of file diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs index 5a27863b..fbf22130 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs @@ -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 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.None }, - AutoAcceptUntrustedClientCertificates = true, - PeerApplicationUris = peers, - }; - var server = new StandardServer(); - var host = new OpcUaApplicationHost(options, NullLogger.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.None }, + AutoAcceptUntrustedClientCertificates = true, + PeerApplicationUris = peers, + }, + NullLogger.Instance); + }, + () => new StandardServer(), CancellationToken.None); + return (started.Host, boundPort); } private static async Task 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; - } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualNamespaceAddressSpaceTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualNamespaceAddressSpaceTests.cs index fc0fbde9..370d41de 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualNamespaceAddressSpaceTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualNamespaceAddressSpaceTests.cs @@ -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.None }, - AutoAcceptUntrustedClientCertificates = true, - }; - var server = new OtOpcUaSdkServer(); - var host = new OpcUaApplicationHost(options, NullLogger.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.None }, + AutoAcceptUntrustedClientCertificates = true, + }, + NullLogger.Instance); + }, + () => new OtOpcUaSdkServer(), ct); + return (started.Server, started.Host, boundPort); } private static async Task 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)) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmEventIdentityFieldDeliveryTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmEventIdentityFieldDeliveryTests.cs index e53d3611..8cfe7115 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmEventIdentityFieldDeliveryTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmEventIdentityFieldDeliveryTests.cs @@ -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.None }, - AutoAcceptUntrustedClientCertificates = true, - }; - var server = new OtOpcUaSdkServer(); - var host = new OpcUaApplicationHost(options, NullLogger.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.None }, + AutoAcceptUntrustedClientCertificates = true, + }, + NullLogger.Instance); + }, + () => new OtOpcUaSdkServer(), ct); + return (started.Server, started.Host, boundPort); } private static async Task 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)) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmMultiNotifierEventDeliveryTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmMultiNotifierEventDeliveryTests.cs index 8674ecf2..d3b3183d 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmMultiNotifierEventDeliveryTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmMultiNotifierEventDeliveryTests.cs @@ -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.None }, - AutoAcceptUntrustedClientCertificates = true, - }; - var server = new OtOpcUaSdkServer(); - var host = new OpcUaApplicationHost(options, NullLogger.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.None }, + AutoAcceptUntrustedClientCertificates = true, + }, + NullLogger.Instance); + }, + () => new OtOpcUaSdkServer(), ct); + return (started.Server, started.Host, boundPort); } private static async Task 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)) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs index d5d657be..18641ee2 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs @@ -51,12 +51,10 @@ internal sealed class ServerEventRig : IAsyncDisposable public static async Task 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.Instance, new KeyPresentingAuthenticator()); + _host = NewHost(); await _host.StartAsync(_server, ct); Sink = new SdkAddressSpaceSink(Nm); } + private OpcUaApplicationHost NewHost() => + new(_hostOptions, NullLogger.Instance, new KeyPresentingAuthenticator()); + /// /// 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; - } - /// /// Admits apikey as carrying the two server-wide roles (used only /// while enforcement is off); anonymous falls through to the anonymous principal. diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/SubscriptionSurvivalTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/SubscriptionSurvivalTests.cs index 01722a5c..b8701f69 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/SubscriptionSurvivalTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/SubscriptionSurvivalTests.cs @@ -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.None }, AutoAcceptUntrustedClientCertificates = true, }; - var server = new OtOpcUaSdkServer(); - await using var host = new OpcUaApplicationHost(options, NullLogger.Instance); - await host.StartAsync(server, ct); + var started = await OpcUaHostStart.StartAsync( + port => + { + options.OpcUaPort = port; + return new OpcUaApplicationHost(options, NullLogger.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.None }, AutoAcceptUntrustedClientCertificates = true, }; - var server = new OtOpcUaSdkServer(); - await using var host = new OpcUaApplicationHost(options, NullLogger.Instance); - await host.StartAsync(server, ct); + var started = await OpcUaHostStart.StartAsync( + port => + { + options.OpcUaPort = port; + return new OpcUaApplicationHost(options, NullLogger.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.None }, AutoAcceptUntrustedClientCertificates = true, }; - var server = new OtOpcUaSdkServer(); - await using var host = new OpcUaApplicationHost(options, NullLogger.Instance); - await host.StartAsync(server, ct); + var started = await OpcUaHostStart.StartAsync( + port => + { + options.OpcUaPort = port; + return new OpcUaApplicationHost(options, NullLogger.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; - } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj index c2b3266f..1a47a002 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj @@ -41,4 +41,9 @@ + + + + + diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs index ca0f2a10..39d26056 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs @@ -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.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.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.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.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.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.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.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.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; - } - /// Disposes of resources allocated by this test class. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs index 9972bd68..2254609e 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAclEnforcementTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAclEnforcementTests.cs index f7d02e1e..8198716a 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAclEnforcementTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAclEnforcementTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs index 6b62c032..a14ac81a 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmSourceFieldsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmSourceFieldsTests.cs index 0d733ec4..687ddc0f 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmSourceFieldsTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmSourceFieldsTests.cs @@ -320,29 +320,24 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable private async Task BootAsync() { - var host = new OpcUaApplicationHost( - new OpcUaApplicationHostOptions - { - ApplicationName = "OtOpcUa.AlarmSourceFieldsTest", - ApplicationUri = $"urn:OtOpcUa.AlarmSourceFieldsTest:{Guid.NewGuid():N}", - OpcUaPort = AllocateFreePort(), - PublicHostname = "localhost", - PkiStoreRoot = _pkiRoot, - }, - NullLogger.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.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)) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs index c98d6593..f5040c2d 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerBatchWriteTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerBatchWriteTests.cs index 6c3e9c19..bb0c8725 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerBatchWriteTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerBatchWriteTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs index 8e236777..4a7be972 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs index a8658918..93153acb 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs index 28bba442..e49759a7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs index 7e9bb01e..afee581a 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs @@ -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.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.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)) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs index ac267f35..a2950a7d 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerKeyUsabilityGateTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerKeyUsabilityGateTests.cs index 64addb48..9bf2741c 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerKeyUsabilityGateTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerKeyUsabilityGateTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeFilterTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeFilterTests.cs index 7c0f235e..65107d66 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeFilterTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeFilterTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs index 085112e4..53719165 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerMultiNotifierAlarmTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerMultiNotifierAlarmTests.cs index a377cca0..db500ed9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerMultiNotifierAlarmTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerMultiNotifierAlarmTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerNativeAlarmRebuildCarryOverTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerNativeAlarmRebuildCarryOverTests.cs index 257b89bb..6a6721ab 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerNativeAlarmRebuildCarryOverTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerNativeAlarmRebuildCarryOverTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs index afcbea51..fa228843 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs @@ -123,19 +123,21 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable /// caller disposes it after exercising the guard. 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.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.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)) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerStage6EnforcementTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerStage6EnforcementTests.cs index e4d0b926..3c6b6605 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerStage6EnforcementTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerStage6EnforcementTests.cs @@ -229,7 +229,6 @@ public sealed class NodeManagerStage6EnforcementTests : IDisposable finally { await host.DisposeAsync(); } } - /// /// Event subscriptions are gated, and the gate resolves its source through the SDK's own /// IsHandleInNamespace 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.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.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); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs index f50a2d57..d3776e94 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs index 2f11adf7..744600d7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs index 16767614..2a44021b 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostSecurityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostSecurityTests.cs index bcb13bee..f8005f8b 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostSecurityTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostSecurityTests.cs @@ -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.Instance); - - await host.StartAsync(new StandardServer(), Ct); + NullLogger.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.Basic256Sha256SignAndEncrypt }, - AutoAcceptUntrustedClientCertificates = false, - }, - NullLogger.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.Basic256Sha256SignAndEncrypt }, + AutoAcceptUntrustedClientCertificates = false, + }, + NullLogger.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; - } - /// /// Cleans up temporary PKI files. /// diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs index 6ca2b7e3..aec153e4 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs @@ -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.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.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; - } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostTests.cs index ec7beceb..3fb71d61 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostTests.cs @@ -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.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.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.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.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.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.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; - } - /// Cleans up the temporary PKI directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaHostStart.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaHostStart.cs new file mode 100644 index 00000000..9dc5180f --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaHostStart.cs @@ -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; + +/// +/// Starts an 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 +/// Failed to establish tcp listener sockets for Ipv4 and IPv6.. A +/// 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). +/// +internal static class OpcUaHostStart +{ + private const int DefaultAttempts = 5; + + /// Probes a currently free loopback TCP port. Racy by nature - pair with . + public static int AllocateFreePort() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + return ((IPEndPoint)listener.LocalEndpoint).Port; + } + + /// + /// Builds a host (via , given the port to bind) and a server (via + /// ) and starts them, retrying with a new port, host and server when + /// the bind fails. Any other failure, or the last bind failure, propagates. + /// + /// The started host and server. The caller owns disposal of the host. + public static async Task<(OpcUaApplicationHost Host, TServer Server)> StartAsync( + Func createHost, + Func 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; + } + } + } + + /// True when the exception chain says the OPC UA listener could not bind its port. + 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; + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OtOpcUaNodeManagerWriteStatusesTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OtOpcUaNodeManagerWriteStatusesTests.cs index 145b88ee..6de7a0c2 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OtOpcUaNodeManagerWriteStatusesTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OtOpcUaNodeManagerWriteStatusesTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs index 2e07da49..932e1f95 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs @@ -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.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.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; - } - /// Cleans up the PKI root directory. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkServiceLevelPublisherTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkServiceLevelPublisherTests.cs index e8cae938..ad1f216a 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkServiceLevelPublisherTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkServiceLevelPublisherTests.cs @@ -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.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.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.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.Instance), + () => new StandardServer(), Ct); + await using var host = started.Host; + var server = started.Server; var publisher = new SdkServiceLevelPublisher( server.CurrentInstance, NullLogger.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; - } - /// Disposes and cleans up the test resources. public void Dispose() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs index 70fef2b3..303400b6 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs @@ -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.Instance); - await host.StartAsync(server, Ct); + var started = await OpcUaHostStart.StartAsync( + port => new OpcUaApplicationHost( + BuildOptions("PrimaryLeader", pkiRoot, port), + NullLogger.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.Instance); - await host.StartAsync(server, Ct); + var started = await OpcUaHostStart.StartAsync( + port => new OpcUaApplicationHost( + BuildOptions("Secondary", pkiRoot, port), + NullLogger.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); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj index d7466aa5..c272a47a 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj @@ -36,4 +36,9 @@ + + + + +