test: retry OPC UA host start on a port bind race (TOCTOU in AllocateFreePort)
The tests probe a free port (bind 0, release) and start the SDK server later; under a highly parallel run another test can take the port in between and the SDK fails with 'Failed to establish tcp listener sockets for Ipv4 and IPv6.'. OpcUaHostStart.StartAsync builds a fresh host + server on a fresh port per attempt (up to 5) when the failure is a bind failure; all 45 start sites in OpcUaServer.Tests, OpcUaServer.IntegrationTests and Runtime.Tests use it. Per-attempt GUID ApplicationUris are hoisted out of the host factory because a changed URI against a reused PKI store fails with 'Cannot access private key'. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+18
-24
@@ -998,23 +998,26 @@ public sealed class AclEventSubscriptionEnforcementTests
|
||||
public static async Task<Rig> StartAsync(string name, CancellationToken ct)
|
||||
{
|
||||
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
|
||||
rig._port = AllocateFreePort();
|
||||
rig._server = new OtOpcUaSdkServer();
|
||||
rig._host = new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
|
||||
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
|
||||
OpcUaPort = rig._port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
rig._port = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = applicationUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance,
|
||||
new KeyPresentingAuthenticator());
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance,
|
||||
new KeyPresentingAuthenticator());
|
||||
|
||||
await rig._host.StartAsync(rig._server, ct);
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
rig._sink = new SdkAddressSpaceSink(rig.Nm);
|
||||
return rig;
|
||||
}
|
||||
@@ -1158,15 +1161,6 @@ public sealed class AclEventSubscriptionEnforcementTests
|
||||
}
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Admits the fixed <c>apikey</c> username as <see cref="KeyPrincipal"/> carrying
|
||||
/// <see cref="KeyId"/>; anonymous falls through to the interface default (the
|
||||
|
||||
+18
-24
@@ -154,23 +154,26 @@ public sealed class AclKeyExpiryEnforcementTests
|
||||
public static async Task<Rig> StartAsync(string name, CancellationToken ct)
|
||||
{
|
||||
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
|
||||
rig._port = AllocateFreePort();
|
||||
rig._server = new OtOpcUaSdkServer();
|
||||
rig._host = new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
|
||||
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
|
||||
OpcUaPort = rig._port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
rig._port = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = applicationUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance,
|
||||
new KeyPresentingAuthenticator());
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance,
|
||||
new KeyPresentingAuthenticator());
|
||||
|
||||
await rig._host.StartAsync(rig._server, ct);
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
return rig;
|
||||
}
|
||||
|
||||
@@ -321,15 +324,6 @@ public sealed class AclKeyExpiryEnforcementTests
|
||||
}
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Admits the fixed <c>apikey</c> username as <see cref="Principal"/>, carrying
|
||||
/// <see cref="KeyId"/>.
|
||||
|
||||
+130
-135
@@ -303,146 +303,141 @@ public sealed class AclSubscriptionEnforcementTests
|
||||
public static async Task<Rig> StartAsync(string name, CancellationToken ct)
|
||||
{
|
||||
var rig = new Rig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
|
||||
rig._port = AllocateFreePort();
|
||||
rig._server = new OtOpcUaSdkServer();
|
||||
rig._host = new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
var applicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}";
|
||||
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
|
||||
OpcUaPort = rig._port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
|
||||
await rig._host.StartAsync(rig._server, ct);
|
||||
return rig;
|
||||
}
|
||||
|
||||
/// <summary>Switches enforcement on with one grant, returning the state so a test can redeploy.</summary>
|
||||
public DataPlaneAuthorityState Enforce(
|
||||
NodeAcl grant, IReadOnlyDictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>? scopes = null)
|
||||
{
|
||||
var state = new DataPlaneAuthorityState();
|
||||
state.Apply(Cluster, "rev-1", [], [grant], scopes ?? NoScopes);
|
||||
|
||||
// BOTH must be set: Authorizer decides, and DataPlaneAuthority is what carries the Changed
|
||||
// signal that reaches live subscriptions. Wiring only the first passes every test that makes a
|
||||
// request and none that revokes.
|
||||
Nm.DataPlaneAuthority = state;
|
||||
Nm.Authorizer = new DataPlaneAuthorizer(
|
||||
state, new TriePermissionEvaluator(state.Tries), enabled: true, NullLogger.Instance);
|
||||
return state;
|
||||
}
|
||||
|
||||
public void Materialise()
|
||||
{
|
||||
Nm.EnsureFolder("Plant", null, "Plant", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureFolder("Plant/Modbus", "Plant", "Modbus", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureFolder(Device, "Plant/Modbus", "dev1", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureVariable(Tag, Device, "Speed", "Float", writable: false, AddressSpaceRealm.Raw);
|
||||
}
|
||||
|
||||
public void MaterialiseTwoDevices()
|
||||
{
|
||||
Materialise();
|
||||
Nm.EnsureFolder("Plant/Modbus/dev9", "Plant/Modbus", "dev9", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureVariable(
|
||||
"Plant/Modbus/dev9/Torque", "Plant/Modbus/dev9", "Torque", "Float",
|
||||
writable: false, AddressSpaceRealm.Raw);
|
||||
}
|
||||
|
||||
public void Write(float value) =>
|
||||
Nm.WriteValue(Tag, value, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
|
||||
public ushort RawNamespace(ISession session)
|
||||
{
|
||||
var ns = (ushort)session.NamespaceUris.GetIndex(V3NodeIds.RawNamespaceUri);
|
||||
ns.ShouldBeGreaterThan((ushort)0);
|
||||
return ns;
|
||||
}
|
||||
|
||||
public NodeId TagNodeId(ISession session) => new(Tag, RawNamespace(session));
|
||||
|
||||
public async Task<(Subscription Subscription, List<DataValue> Received)> SubscribeAsync(
|
||||
ISession session, CancellationToken ct, bool expectFailure = false)
|
||||
{
|
||||
var received = new List<DataValue>();
|
||||
var gate = new object();
|
||||
|
||||
var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100 };
|
||||
session.AddSubscription(subscription);
|
||||
await subscription.CreateAsync(ct);
|
||||
|
||||
var item = new MonitoredItem(subscription.DefaultItem)
|
||||
rig._port = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = applicationUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
return rig;
|
||||
}
|
||||
|
||||
/// <summary>Switches enforcement on with one grant, returning the state so a test can redeploy.</summary>
|
||||
public DataPlaneAuthorityState Enforce(
|
||||
NodeAcl grant, IReadOnlyDictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>? scopes = null)
|
||||
{
|
||||
StartNodeId = TagNodeId(session),
|
||||
AttributeId = Attributes.Value,
|
||||
SamplingInterval = 50,
|
||||
QueueSize = 10,
|
||||
};
|
||||
item.Notification += (mi, _) =>
|
||||
var state = new DataPlaneAuthorityState();
|
||||
state.Apply(Cluster, "rev-1", [], [grant], scopes ?? NoScopes);
|
||||
|
||||
// BOTH must be set: Authorizer decides, and DataPlaneAuthority is what carries the Changed
|
||||
// signal that reaches live subscriptions. Wiring only the first passes every test that makes a
|
||||
// request and none that revokes.
|
||||
Nm.DataPlaneAuthority = state;
|
||||
Nm.Authorizer = new DataPlaneAuthorizer(
|
||||
state, new TriePermissionEvaluator(state.Tries), enabled: true, NullLogger.Instance);
|
||||
return state;
|
||||
}
|
||||
|
||||
public void Materialise()
|
||||
{
|
||||
foreach (var v in mi.DequeueValues()) lock (gate) received.Add(v);
|
||||
};
|
||||
subscription.AddItem(item);
|
||||
await subscription.ApplyChangesAsync(ct);
|
||||
|
||||
if (!expectFailure) item.Status.Error.ShouldBeNull();
|
||||
|
||||
// The list is only ever appended under `gate` and read from the test thread between awaits;
|
||||
// a snapshot-on-read wrapper would hide ordering rather than help.
|
||||
return (subscription, received);
|
||||
}
|
||||
|
||||
public async Task<ISession> ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
var appConfig = new ApplicationConfiguration
|
||||
Nm.EnsureFolder("Plant", null, "Plant", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureFolder("Plant/Modbus", "Plant", "Modbus", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureFolder(Device, "Plant/Modbus", "dev1", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureVariable(Tag, Device, "Speed", "Float", writable: false, AddressSpaceRealm.Raw);
|
||||
}
|
||||
|
||||
public void MaterialiseTwoDevices()
|
||||
{
|
||||
ApplicationName = "OtOpcUa.AclClient",
|
||||
ApplicationUri = $"urn:OtOpcUa.AclClient.{Guid.NewGuid():N}",
|
||||
ApplicationType = ApplicationType.Client,
|
||||
SecurityConfiguration = TestClientSecurity.Build(TestClientSecurity.AllocatePkiRoot()),
|
||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
|
||||
};
|
||||
await appConfig.ValidateAsync(ApplicationType.Client, ct);
|
||||
appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
|
||||
|
||||
var endpoint = await CoreClientUtils.SelectEndpointAsync(
|
||||
appConfig, $"opc.tcp://127.0.0.1:{_port}/OtOpcUa", false, DefaultTelemetry.Create(_ => { }), ct);
|
||||
|
||||
return await new DefaultSessionFactory(DefaultTelemetry.Create(_ => { })).CreateAsync(
|
||||
appConfig,
|
||||
new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(appConfig)),
|
||||
updateBeforeConnect: false,
|
||||
checkDomain: false,
|
||||
sessionName: "AclSubscriptionEnforcementTests",
|
||||
sessionTimeout: 60_000,
|
||||
identity: new UserIdentity(new AnonymousIdentityToken()),
|
||||
preferredLocales: null,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _host.DisposeAsync();
|
||||
if (Directory.Exists(_pkiRoot))
|
||||
Materialise();
|
||||
Nm.EnsureFolder("Plant/Modbus/dev9", "Plant/Modbus", "dev9", AddressSpaceRealm.Raw);
|
||||
Nm.EnsureVariable(
|
||||
"Plant/Modbus/dev9/Torque", "Plant/Modbus/dev9", "Torque", "Float",
|
||||
writable: false, AddressSpaceRealm.Raw);
|
||||
}
|
||||
|
||||
public void Write(float value) =>
|
||||
Nm.WriteValue(Tag, value, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
|
||||
public ushort RawNamespace(ISession session)
|
||||
{
|
||||
try { Directory.Delete(_pkiRoot, recursive: true); }
|
||||
catch (IOException) { /* best effort */ }
|
||||
var ns = (ushort)session.NamespaceUris.GetIndex(V3NodeIds.RawNamespaceUri);
|
||||
ns.ShouldBeGreaterThan((ushort)0);
|
||||
return ns;
|
||||
}
|
||||
|
||||
public NodeId TagNodeId(ISession session) => new(Tag, RawNamespace(session));
|
||||
|
||||
public async Task<(Subscription Subscription, List<DataValue> Received)> SubscribeAsync(
|
||||
ISession session, CancellationToken ct, bool expectFailure = false)
|
||||
{
|
||||
var received = new List<DataValue>();
|
||||
var gate = new object();
|
||||
|
||||
var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100 };
|
||||
session.AddSubscription(subscription);
|
||||
await subscription.CreateAsync(ct);
|
||||
|
||||
var item = new MonitoredItem(subscription.DefaultItem)
|
||||
{
|
||||
StartNodeId = TagNodeId(session),
|
||||
AttributeId = Attributes.Value,
|
||||
SamplingInterval = 50,
|
||||
QueueSize = 10,
|
||||
};
|
||||
item.Notification += (mi, _) =>
|
||||
{
|
||||
foreach (var v in mi.DequeueValues()) lock (gate) received.Add(v);
|
||||
};
|
||||
subscription.AddItem(item);
|
||||
await subscription.ApplyChangesAsync(ct);
|
||||
|
||||
if (!expectFailure) item.Status.Error.ShouldBeNull();
|
||||
|
||||
// The list is only ever appended under `gate` and read from the test thread between awaits;
|
||||
// a snapshot-on-read wrapper would hide ordering rather than help.
|
||||
return (subscription, received);
|
||||
}
|
||||
|
||||
public async Task<ISession> ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
var appConfig = new ApplicationConfiguration
|
||||
{
|
||||
ApplicationName = "OtOpcUa.AclClient",
|
||||
ApplicationUri = $"urn:OtOpcUa.AclClient.{Guid.NewGuid():N}",
|
||||
ApplicationType = ApplicationType.Client,
|
||||
SecurityConfiguration = TestClientSecurity.Build(TestClientSecurity.AllocatePkiRoot()),
|
||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
|
||||
};
|
||||
await appConfig.ValidateAsync(ApplicationType.Client, ct);
|
||||
appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
|
||||
|
||||
var endpoint = await CoreClientUtils.SelectEndpointAsync(
|
||||
appConfig, $"opc.tcp://127.0.0.1:{_port}/OtOpcUa", false, DefaultTelemetry.Create(_ => { }), ct);
|
||||
|
||||
return await new DefaultSessionFactory(DefaultTelemetry.Create(_ => { })).CreateAsync(
|
||||
appConfig,
|
||||
new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(appConfig)),
|
||||
updateBeforeConnect: false,
|
||||
checkDomain: false,
|
||||
sessionName: "AclSubscriptionEnforcementTests",
|
||||
sessionTimeout: 60_000,
|
||||
identity: new UserIdentity(new AnonymousIdentityToken()),
|
||||
preferredLocales: null,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _host.DisposeAsync();
|
||||
if (Directory.Exists(_pkiRoot))
|
||||
{
|
||||
try { Directory.Delete(_pkiRoot, recursive: true); }
|
||||
catch (IOException) { /* best effort */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ public sealed class DualEndpointTests
|
||||
{
|
||||
var pkiRootA = Path.Combine(Path.GetTempPath(), $"otopcua-pki-a-{Guid.NewGuid():N}");
|
||||
var pkiRootB = Path.Combine(Path.GetTempPath(), $"otopcua-pki-b-{Guid.NewGuid():N}");
|
||||
var portA = AllocateFreePort();
|
||||
var portB = AllocateFreePort();
|
||||
|
||||
try
|
||||
{
|
||||
await using var nodeA = await StartNodeAsync(NodeAUri, portA, pkiRootA, peers: new[] { NodeBUri });
|
||||
await using var nodeB = await StartNodeAsync(NodeBUri, portB, pkiRootB, peers: new[] { NodeAUri });
|
||||
var (nodeAHost, portA) = await StartNodeAsync(NodeAUri, pkiRootA, peers: new[] { NodeBUri });
|
||||
await using var nodeA = nodeAHost;
|
||||
var (nodeBHost, _) = await StartNodeAsync(NodeBUri, pkiRootB, peers: new[] { NodeAUri });
|
||||
await using var nodeB = nodeBHost;
|
||||
|
||||
var serverArray = await ReadServerArrayAsync($"opc.tcp://127.0.0.1:{portA}/OtOpcUa");
|
||||
serverArray.ShouldContain(NodeAUri);
|
||||
@@ -47,24 +47,31 @@ public sealed class DualEndpointTests
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<OpcUaApplicationHost> StartNodeAsync(
|
||||
string applicationUri, int port, string pkiRoot, string[] peers)
|
||||
private static async Task<(OpcUaApplicationHost Host, int Port)> StartNodeAsync(
|
||||
string applicationUri, string pkiRoot, string[] peers)
|
||||
{
|
||||
var options = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = applicationUri,
|
||||
ApplicationUri = applicationUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
PeerApplicationUris = peers,
|
||||
};
|
||||
var server = new StandardServer();
|
||||
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, CancellationToken.None);
|
||||
return host;
|
||||
// The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
|
||||
var boundPort = 0;
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
boundPort = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = applicationUri,
|
||||
ApplicationUri = applicationUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
PeerApplicationUris = peers,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new StandardServer(), CancellationToken.None);
|
||||
return (started.Host, boundPort);
|
||||
}
|
||||
|
||||
private static async Task<string[]> ReadServerArrayAsync(string endpointUrl)
|
||||
@@ -111,13 +118,4 @@ public sealed class DualEndpointTests
|
||||
if (Directory.Exists(clientPki)) Directory.Delete(clientPki, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-33
@@ -54,11 +54,10 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
public async Task Both_namespaces_registered_and_both_subtrees_browse_read_and_organize()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-browse-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri, ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -108,11 +107,10 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
public async Task Single_source_fans_to_both_nodeids_with_identical_value_quality_and_timestamp()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-fanout-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Fanout", ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".Fanout", ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -146,11 +144,10 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
public async Task HistoryRead_via_either_nodeid_returns_GoodNoData_under_the_shared_tagname()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-hist-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".History", ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".History", ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -178,11 +175,10 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
public async Task WriteOperate_gate_is_symmetric_across_both_nodeids()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-dualns-write-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Write", ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".Write", ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -263,23 +259,30 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
return response.Results[0];
|
||||
}
|
||||
|
||||
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
|
||||
int port, string pkiRoot, string appUri, CancellationToken ct)
|
||||
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
|
||||
string pkiRoot, string appUri, CancellationToken ct)
|
||||
{
|
||||
var options = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = appUri,
|
||||
ApplicationUri = appUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
var server = new OtOpcUaSdkServer();
|
||||
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, ct);
|
||||
return (server, host);
|
||||
// The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
|
||||
var boundPort = 0;
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
boundPort = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = appUri,
|
||||
ApplicationUri = appUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
return (started.Server, started.Host, boundPort);
|
||||
}
|
||||
|
||||
private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
|
||||
@@ -306,15 +309,6 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
identity: new UserIdentity(new AnonymousIdentityToken()), preferredLocales: null, ct: ct);
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
private static void SafeDelete(string dir)
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
|
||||
+26
-31
@@ -65,11 +65,10 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
public async Task Condition_event_carries_populated_EventType_SourceNode_and_SourceName_on_the_wire()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-identity-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri, ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -134,11 +133,10 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
public async Task Condition_event_carries_populated_ConditionClass_fields_on_the_wire()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-condclass-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ConditionClassServerUri, ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ConditionClassServerUri, ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -187,11 +185,10 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
public async Task Condition_event_Quality_tracks_source_connectivity_on_the_wire()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-alarm-quality-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", QualityServerUri, ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", QualityServerUri, ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -333,23 +330,30 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
|
||||
int port, string pkiRoot, string appUri, CancellationToken ct)
|
||||
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
|
||||
string pkiRoot, string appUri, CancellationToken ct)
|
||||
{
|
||||
var options = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = appUri,
|
||||
ApplicationUri = appUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
var server = new OtOpcUaSdkServer();
|
||||
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, ct);
|
||||
return (server, host);
|
||||
// The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
|
||||
var boundPort = 0;
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
boundPort = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = appUri,
|
||||
ApplicationUri = appUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
return (started.Server, started.Host, boundPort);
|
||||
}
|
||||
|
||||
private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
|
||||
@@ -387,15 +391,6 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
condition().ShouldBeTrue("condition not met within timeout");
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
private static void SafeDelete(string dir)
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
|
||||
+25
-29
@@ -47,11 +47,10 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
|
||||
public async Task Single_transition_delivers_exactly_one_event_at_the_Server_object()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-evt-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri, ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -95,11 +94,10 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
|
||||
public async Task Overlapping_folder_and_server_subscriptions_each_receive_exactly_one_copy()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-ovl-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
try
|
||||
{
|
||||
var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Overlap", ct);
|
||||
var (server, host, port) = await BootServerAsync(pkiRoot + "-srv", ServerUri + ".Overlap", ct);
|
||||
await using var _ = host;
|
||||
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
@@ -208,23 +206,30 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
|
||||
int port, string pkiRoot, string appUri, CancellationToken ct)
|
||||
private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host, int Port)> BootServerAsync(
|
||||
string pkiRoot, string appUri, CancellationToken ct)
|
||||
{
|
||||
var options = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = appUri,
|
||||
ApplicationUri = appUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
var server = new OtOpcUaSdkServer();
|
||||
var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, ct);
|
||||
return (server, host);
|
||||
// The port is probed per attempt (OpcUaHostStart retries on a bind race), so report the one that bound.
|
||||
var boundPort = 0;
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
boundPort = port;
|
||||
return new OpcUaApplicationHost(
|
||||
new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = appUri,
|
||||
ApplicationUri = appUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
},
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
return (started.Server, started.Host, boundPort);
|
||||
}
|
||||
|
||||
private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
|
||||
@@ -262,15 +267,6 @@ public sealed class NativeAlarmMultiNotifierEventDeliveryTests
|
||||
condition().ShouldBeTrue("condition not met within timeout");
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
private static void SafeDelete(string dir)
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
|
||||
@@ -51,12 +51,10 @@ internal sealed class ServerEventRig : IAsyncDisposable
|
||||
public static async Task<ServerEventRig> StartAsync(string name, bool encrypted, CancellationToken ct)
|
||||
{
|
||||
var rig = new ServerEventRig(Path.Combine(Path.GetTempPath(), $"otopcua-{name}-{Guid.NewGuid():N}"));
|
||||
rig._port = AllocateFreePort();
|
||||
rig._hostOptions = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = $"OtOpcUa.{name}",
|
||||
ApplicationUri = $"urn:OtOpcUa.{name}.{Guid.NewGuid():N}",
|
||||
OpcUaPort = rig._port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = rig._pkiRoot,
|
||||
EnabledSecurityProfiles = encrypted
|
||||
@@ -64,19 +62,29 @@ internal sealed class ServerEventRig : IAsyncDisposable
|
||||
: [OpcUaSecurityProfile.None],
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
await rig.BootAsync(ct);
|
||||
(rig._host, rig._server) = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
rig._port = port;
|
||||
rig._hostOptions.OpcUaPort = port;
|
||||
return rig.NewHost();
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
rig.Sink = new SdkAddressSpaceSink(rig.Nm);
|
||||
return rig;
|
||||
}
|
||||
|
||||
private async Task BootAsync(CancellationToken ct)
|
||||
{
|
||||
_server = new OtOpcUaSdkServer();
|
||||
_host = new OpcUaApplicationHost(
|
||||
_hostOptions, NullLogger<OpcUaApplicationHost>.Instance, new KeyPresentingAuthenticator());
|
||||
_host = NewHost();
|
||||
await _host.StartAsync(_server, ct);
|
||||
Sink = new SdkAddressSpaceSink(Nm);
|
||||
}
|
||||
|
||||
private OpcUaApplicationHost NewHost() =>
|
||||
new(_hostOptions, NullLogger<OpcUaApplicationHost>.Instance, new KeyPresentingAuthenticator());
|
||||
|
||||
/// <summary>
|
||||
/// Stops the server and boots a fresh one on the SAME port, application URI and PKI store (a process
|
||||
/// restart as a client sees it). The address space is EMPTY afterwards: the caller re-creates its nodes and
|
||||
@@ -291,15 +299,6 @@ internal sealed class ServerEventRig : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Admits <c>apikey</c> as <see cref="KeyPrincipal"/> carrying the two server-wide roles (used only
|
||||
/// while enforcement is off); anonymous falls through to the <c>anonymous</c> principal.
|
||||
|
||||
+30
-24
@@ -38,7 +38,6 @@ public sealed class SubscriptionSurvivalTests
|
||||
public async Task Subscription_on_existing_tag_survives_pure_add_of_another_tag()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
try
|
||||
@@ -47,15 +46,21 @@ public sealed class SubscriptionSurvivalTests
|
||||
{
|
||||
ApplicationName = ServerUri,
|
||||
ApplicationUri = ServerUri,
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
var server = new OtOpcUaSdkServer();
|
||||
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, ct);
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
options.OpcUaPort = port;
|
||||
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
await using var host = started.Host;
|
||||
var server = started.Server;
|
||||
var port = options.OpcUaPort;
|
||||
|
||||
var nm = server.NodeManager!;
|
||||
var sink = new SdkAddressSpaceSink(nm);
|
||||
@@ -145,7 +150,6 @@ public sealed class SubscriptionSurvivalTests
|
||||
public async Task Subscription_on_surviving_tag_survives_pure_remove_of_another_tag()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-rm-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
try
|
||||
@@ -154,15 +158,21 @@ public sealed class SubscriptionSurvivalTests
|
||||
{
|
||||
ApplicationName = ServerUri,
|
||||
ApplicationUri = ServerUri + ".Remove",
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
var server = new OtOpcUaSdkServer();
|
||||
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, ct);
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
options.OpcUaPort = port;
|
||||
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
await using var host = started.Host;
|
||||
var server = started.Server;
|
||||
var port = options.OpcUaPort;
|
||||
|
||||
var nm = server.NodeManager!;
|
||||
var sink = new SdkAddressSpaceSink(nm);
|
||||
@@ -234,7 +244,6 @@ public sealed class SubscriptionSurvivalTests
|
||||
public async Task Subscription_survives_mixed_add_remove_deploy()
|
||||
{
|
||||
var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-subsurvive-mix-{Guid.NewGuid():N}");
|
||||
var port = AllocateFreePort();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
try
|
||||
@@ -243,15 +252,21 @@ public sealed class SubscriptionSurvivalTests
|
||||
{
|
||||
ApplicationName = ServerUri,
|
||||
ApplicationUri = ServerUri + ".Mixed",
|
||||
OpcUaPort = port,
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = pkiRoot,
|
||||
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
var server = new OtOpcUaSdkServer();
|
||||
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await host.StartAsync(server, ct);
|
||||
var started = await OpcUaHostStart.StartAsync(
|
||||
port =>
|
||||
{
|
||||
options.OpcUaPort = port;
|
||||
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
||||
},
|
||||
() => new OtOpcUaSdkServer(), ct);
|
||||
await using var host = started.Host;
|
||||
var server = started.Server;
|
||||
var port = options.OpcUaPort;
|
||||
|
||||
var nm = server.NodeManager!;
|
||||
var sink = new SdkAddressSpaceSink(nm);
|
||||
@@ -372,13 +387,4 @@ public sealed class SubscriptionSurvivalTests
|
||||
}
|
||||
condition().ShouldBeTrue("condition not met within timeout");
|
||||
}
|
||||
|
||||
private static int AllocateFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -41,4 +41,9 @@
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Shared bind-race retry helper for tests that boot a real SDK server on a probed port. -->
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests\OpcUaHostStart.cs" Link="OpcUaHostStart.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user