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

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