mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
49305d9085
# Description V2 subscription engine feature parity round to make the classic engine eligible for deprecation. Splits the test surface so the classic and V2 engines can evolve independently, ports the gaps the classic-engine integration tests had been exercising, and lands the public V2 API additions needed to close them. - Splits `Tests/Opc.Ua.Subscriptions.Tests` into a classic (`Opc.Ua.Subscriptions.Classic.Tests`) and a V2 (`Opc.Ua.Subscriptions.Tests`) project to shrink CI time and let the two engines diverge cleanly. - Adds the V2 public-surface gaps required by the test ports (`Subscription.ServerId` on the class, `SetSubscriptionDurableAsync`, `SendInitialValuesOnTransfer`, unified `OnSubscriptionStateChangedAsync` handler callback, per-item `ConditionRefreshAsync`, fluent `LoadSubscriptionsAsync(Stream, …)` and `Subscription.Snapshot()` / `RestoreAsync()` for save/load + transfer). - Lands handler-centric design (no `PublishingStopped` property handlers maintain derived state); always-sequential per-subscription delivery documented + tested. - **Adds short and long haul stability tests for the V2 `ManagedSession` + subscription engine. Closes #3744** (see "Issue #3744 Haul tests" below). - **Migrates SubscriptionStateSnapshot / MonitoredItemStateSnapshot to `[DataType]` source-generator codecs**. - `SubscriptionDurableV2Tests.cs` 5 V2 ports of the classic durable test patterns. - `V2FollowUpCoverageTests.cs` handler state-change callback, fluent stream-based `LoadSubscriptionsAsync`, `SendInitialValuesOnTransfer`, snapshot edge cases (empty / `DataChangeFilter` round-trip / concurrent mutation). - `SubscriptionFailoverV2Tests.cs` forced channel-break with and without `WithTransferSubscriptionsOnRecreate`. - `MonitoredItemConditionRefreshLiveV2Tests.cs` — live `ConditionRefresh` against the reference server's event source; asserts `RefreshStart` / `RefreshEnd` events flow through the handler. ## Fixes - #3744 Haul tests (V2 ManagedSession + subscription engine) `Tests/Opc.Ua.Sessions.Tests/ManagedSessionStabilityTest.cs` adds the short/long haul coverage requested by [#3744](https://github.com/OPCFoundation/UA-.NETStandard/issues/3744) plus the bonus fault-injection variant: | Test | Duration | Category | Purpose | |---|---|---|---| | `ShortHaulManagedSessionV2Async` | 2 minutes | `ManagedSessionHaul` (`[Explicit]`) | Quick local sanity check of the V2 monotonicity contract. | | `LongHaulManagedSessionV2Async` | 90 minutes (configurable via `TEST_DURATION_MINUTES`) | `ManagedSessionHaul` (`[Explicit]`) | Long-running V2 stability + monotonicity. | | `LongHaulManagedSessionWithFaultInjectionV2Async` | 90 minutes (configurable) + fault every `FAULT_INJECTION_INTERVAL_SECONDS` (default 60s) | `ManagedSessionHaul` (`[Explicit]`) | **Bonus** — force-closes the subscriber transport channel periodically; verifies V2 manager transfers/recreates the subscription and continues monotonic delivery across reconnects. | **Pattern.** A writer `ManagedSession` increments a `uint32` counter and writes it into `Scalar_Static_Mass_UInt32_UInt32_00` every 250ms. A subscriber `ManagedSession` runs a V2 subscription with a single monitored item on the same node. `MonotonicCounterHandler` asserts each received sample is *strictly greater than* the previous one. V2 `ISubscriptionNotificationHandler` per-subscription ordering guarantee. Skipped values (sampling rate < write rate) are tolerated; duplicates and reorderings are violations.
786 lines
32 KiB
C#
786 lines
32 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using BenchmarkDotNet.Attributes;
|
|
using Microsoft.Extensions.Logging;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Server.TestFramework;
|
|
using Opc.Ua.Tests;
|
|
using Quickstarts.ReferenceServer;
|
|
|
|
namespace Opc.Ua.Client.TestFramework
|
|
{
|
|
/// <summary>
|
|
/// Client test framework, common code to setup connections.
|
|
/// </summary>
|
|
public class ClientTestFramework
|
|
{
|
|
public static readonly object[] FixtureArgs =
|
|
[
|
|
new object[] { Utils.UriSchemeOpcTcp },
|
|
new object[] { Utils.UriSchemeHttps },
|
|
new object[] { Utils.UriSchemeOpcHttps }
|
|
];
|
|
|
|
public const int MaxReferences = 100;
|
|
public const int MaxTimeout = 10000;
|
|
public const int TransportQuotaMaxMessageSize = 4 * 1024 * 1024;
|
|
public const int TransportQuotaMaxStringLength = 1 * 1024 * 1024;
|
|
public TokenValidatorMock TokenValidator { get; set; } = new TokenValidatorMock();
|
|
public bool SingleSession { get; set; } = true;
|
|
public bool AllNodeManagers { get; set; }
|
|
public int MaxChannelCount { get; set; } = 100;
|
|
public bool SupportsExternalServerUrl { get; set; }
|
|
public bool UseSamplingGroupsInReferenceNodeManager { get; set; }
|
|
|
|
/// <summary>
|
|
/// When non-null, the fixture's <see cref="ClientFixture"/> is
|
|
/// configured to use this engine factory for every session it
|
|
/// creates. Defaults to
|
|
/// <see cref="ClassicSubscriptionEngineFactory.Instance"/> so
|
|
/// existing classic-engine tests (which use
|
|
/// <c>TestableSubscription</c> / <c>Session.AddSubscription</c>)
|
|
/// continue to work. V2-only fixtures should set this to
|
|
/// <see cref="DefaultSubscriptionEngineFactory.Instance"/> during
|
|
/// <c>OneTimeSetUpAsync</c>.
|
|
/// </summary>
|
|
public ISubscriptionEngineFactory ClientFixtureSubscriptionEngineFactory { get; set; }
|
|
= ClassicSubscriptionEngineFactory.Instance;
|
|
public ServerFixture<ReferenceServer> ServerFixture { get; set; }
|
|
public ClientFixture ClientFixture { get; set; }
|
|
public ReferenceServer ReferenceServer { get; set; }
|
|
public ArrayOf<EndpointDescription> Endpoints { get; set; }
|
|
public ArrayOf<ReferenceDescription> ReferenceDescriptions { get; set; }
|
|
public ISession Session { get; protected set; }
|
|
public OperationLimits OperationLimits { get; private set; }
|
|
public int SecurityTokenLifetime { get; set; } = 3_600_000;
|
|
public string UriScheme { get; }
|
|
public string PkiRoot { get; set; }
|
|
public Uri ServerUrl { get; private set; }
|
|
public int ServerFixturePort { get; set; }
|
|
public ExpandedNodeId[] TestSetStatic { get; private set; }
|
|
public (Type Type, ExpandedNodeId[] NodeIds)[] TestSetStaticMassNumeric { get; }
|
|
public ExpandedNodeId[] TestSetSimulation { get; private set; }
|
|
public ExpandedNodeId[] TestSetDataSimulation { get; }
|
|
public ExpandedNodeId[] TestSetHistory { get; }
|
|
public ITelemetryContext Telemetry { get; }
|
|
|
|
private readonly ILogger<ClientTestFramework> m_logger;
|
|
|
|
public ClientTestFramework(string uriScheme = Utils.UriSchemeOpcTcp, ITelemetryContext telemetry = null)
|
|
{
|
|
Telemetry = telemetry ?? NUnitTelemetryContext.Create();
|
|
m_logger = Telemetry.CreateLogger<ClientTestFramework>();
|
|
UriScheme = uriScheme;
|
|
TestSetStatic = CommonTestWorkers.NodeIdTestSetStatic;
|
|
TestSetStaticMassNumeric = CommonTestWorkers.NodeIdTestSetStaticMassNumeric;
|
|
TestSetSimulation = CommonTestWorkers.NodeIdTestSetSimulation;
|
|
TestSetDataSimulation = CommonTestWorkers.NodeIdTestSetDataSimulation;
|
|
TestSetHistory = CommonTestWorkers.NodeIdTestDataHistory;
|
|
}
|
|
|
|
public void InitializeSession(ISession session)
|
|
{
|
|
Session = session;
|
|
}
|
|
|
|
[DatapointSource]
|
|
public static readonly string[] Policies =
|
|
[
|
|
.. GetPolicyUrisForTests()
|
|
];
|
|
|
|
protected static IEnumerable<string> GetSupportedEccPolicyUris(
|
|
bool includeCurvePolicies = true)
|
|
{
|
|
IEnumerable<string> displayNames = SecurityPolicies.GetDisplayNames()
|
|
.Where(name => name.StartsWith("ECC_", StringComparison.Ordinal));
|
|
|
|
if (!includeCurvePolicies)
|
|
{
|
|
displayNames = displayNames
|
|
.Where(name => !name.StartsWith("ECC_curve", StringComparison.Ordinal));
|
|
}
|
|
|
|
return displayNames.Select(SecurityPolicies.GetUri);
|
|
}
|
|
|
|
private static IEnumerable<string> GetPolicyUrisForTests()
|
|
{
|
|
IEnumerable<string> displayNames = SecurityPolicies.GetDisplayNames();
|
|
|
|
#if NETFRAMEWORK
|
|
displayNames = displayNames.Where(name =>
|
|
!name.EndsWith("_AesGcm", StringComparison.Ordinal) &&
|
|
!name.EndsWith("_ChaChaPoly", StringComparison.Ordinal));
|
|
#endif
|
|
|
|
return displayNames.Select(SecurityPolicies.GetUri);
|
|
}
|
|
|
|
protected async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri)
|
|
{
|
|
if (Endpoints.IsNull)
|
|
{
|
|
Endpoints = await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false);
|
|
}
|
|
|
|
bool isAdvertised = false;
|
|
foreach (EndpointDescription endpoint in Endpoints)
|
|
{
|
|
if (string.Equals(
|
|
endpoint.SecurityPolicyUri,
|
|
securityPolicyUri,
|
|
StringComparison.Ordinal))
|
|
{
|
|
isAdvertised = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isAdvertised)
|
|
{
|
|
string advertisedPolicies = Endpoints.IsNull
|
|
? "<none>"
|
|
: string.Join(
|
|
", ",
|
|
(Endpoints.ToArray() ?? [])
|
|
.Select(endpoint => endpoint.SecurityPolicyUri)
|
|
.Where(policy => !string.IsNullOrEmpty(policy))
|
|
.Distinct()
|
|
.OrderBy(policy => policy, StringComparer.Ordinal));
|
|
Assert.Ignore(
|
|
$"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " +
|
|
$"Advertised: {advertisedPolicies}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set up a Server and a Client instance.
|
|
/// </summary>
|
|
public virtual Task OneTimeSetUpAsync()
|
|
{
|
|
return OneTimeSetUpCoreAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Setup a server and client fixture.
|
|
/// </summary>
|
|
public virtual async Task OneTimeSetUpCoreAsync(
|
|
bool securityNone = false,
|
|
bool enableClientSideTracing = false,
|
|
bool enableServerSideTracing = false,
|
|
bool disableActivityLogging = false)
|
|
{
|
|
// pki directory root for test runs.
|
|
PkiRoot = Path.GetTempPath() + Path.GetRandomFileName();
|
|
|
|
m_logger.LogInformation("Using the Pki Root {FilePath}", PkiRoot);
|
|
|
|
// The parameters are read from the .runsettings file
|
|
string customUrl = null;
|
|
if (SupportsExternalServerUrl)
|
|
{
|
|
customUrl = TestContext.Parameters["ServerUrl"];
|
|
if (customUrl?.StartsWith(UriScheme, StringComparison.Ordinal) == true)
|
|
{
|
|
m_logger.LogInformation("Using the external Server Url {Url}", customUrl);
|
|
|
|
// load custom test sets
|
|
TestSetStatic = ReadCustomTestSet("TestSetStatic");
|
|
TestSetSimulation = ReadCustomTestSet("TestSetSimulation");
|
|
}
|
|
else
|
|
{
|
|
customUrl = null;
|
|
}
|
|
}
|
|
|
|
if (customUrl == null)
|
|
{
|
|
await CreateReferenceServerFixtureAsync(
|
|
enableServerSideTracing,
|
|
disableActivityLogging,
|
|
securityNone)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
ClientFixture = new ClientFixture(enableClientSideTracing, disableActivityLogging, Telemetry);
|
|
|
|
// If a derived fixture pinned the engine factory, propagate
|
|
// it. Default = null = use Session default (currently the
|
|
// classic engine; see Session.cs).
|
|
if (ClientFixtureSubscriptionEngineFactory != null)
|
|
{
|
|
ClientFixture.UseSubscriptionEngineFactory(
|
|
ClientFixtureSubscriptionEngineFactory);
|
|
}
|
|
|
|
await ClientFixture.LoadClientConfigurationAsync(PkiRoot).ConfigureAwait(false);
|
|
ClientFixture.Config.TransportQuotas.MaxMessageSize = TransportQuotaMaxMessageSize;
|
|
ClientFixture.Config.TransportQuotas.MaxByteStringLength = ClientFixture
|
|
.Config
|
|
.TransportQuotas
|
|
.MaxStringLength = TransportQuotaMaxStringLength;
|
|
ClientFixture.Config.TransportQuotas.SecurityTokenLifetime = SecurityTokenLifetime;
|
|
|
|
if (!string.IsNullOrEmpty(customUrl))
|
|
{
|
|
ServerUrl = new Uri(customUrl);
|
|
}
|
|
else
|
|
{
|
|
string url = UriScheme +
|
|
"://localhost:" +
|
|
ServerFixturePort.ToString(CultureInfo.InvariantCulture);
|
|
|
|
if (UriScheme.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) ||
|
|
Utils.IsUriHttpsScheme(UriScheme))
|
|
{
|
|
url += ConfiguredEndpoint.DiscoverySuffix;
|
|
}
|
|
|
|
ServerUrl = new Uri(url);
|
|
}
|
|
|
|
if (SingleSession)
|
|
{
|
|
try
|
|
{
|
|
Session = await ClientFixture
|
|
.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256)
|
|
.ConfigureAwait(false);
|
|
Assert.That(Session, Is.Not.Null);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Assert.Warn(
|
|
$"OneTimeSetup failed to create session with {ServerUrl}, tests fail. Error: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual async Task CreateReferenceServerFixtureAsync(
|
|
bool enableTracing,
|
|
bool disableActivityLogging,
|
|
bool securityNone)
|
|
{
|
|
// start Ref server
|
|
ServerFixture = new ServerFixture<ReferenceServer>(
|
|
t => new ReferenceServer(t),
|
|
enableTracing,
|
|
disableActivityLogging)
|
|
{
|
|
UriScheme = UriScheme,
|
|
SecurityNone = securityNone,
|
|
AutoAccept = true,
|
|
AllNodeManagers = AllNodeManagers,
|
|
OperationLimits = true,
|
|
UseSamplingGroupsInReferenceNodeManager = UseSamplingGroupsInReferenceNodeManager
|
|
};
|
|
|
|
await ServerFixture.LoadConfigurationAsync(PkiRoot).ConfigureAwait(false);
|
|
ServerFixture.Config.TransportQuotas.MaxMessageSize = TransportQuotaMaxMessageSize;
|
|
ServerFixture.Config.TransportQuotas.MaxByteStringLength = ServerFixture
|
|
.Config
|
|
.TransportQuotas
|
|
.MaxStringLength = TransportQuotaMaxStringLength;
|
|
ServerFixture.Config.TransportQuotas.SecurityTokenLifetime = SecurityTokenLifetime;
|
|
|
|
void AddExplicitUserTokenPolicies(string securityPolicyUri)
|
|
{
|
|
if (SecurityPolicies.GetInfo(securityPolicyUri) == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
|
|
new UserTokenPolicy(UserTokenType.UserName)
|
|
{
|
|
SecurityPolicyUri = securityPolicyUri
|
|
};
|
|
|
|
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
|
|
new UserTokenPolicy(UserTokenType.Certificate)
|
|
{
|
|
SecurityPolicyUri = securityPolicyUri
|
|
};
|
|
|
|
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
|
|
new UserTokenPolicy(UserTokenType.IssuedToken)
|
|
{
|
|
IssuedTokenType = Profiles.JwtUserToken,
|
|
PolicyId = Profiles.JwtUserToken,
|
|
SecurityPolicyUri = securityPolicyUri
|
|
};
|
|
}
|
|
|
|
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
|
|
new UserTokenPolicy(UserTokenType.UserName);
|
|
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
|
|
new UserTokenPolicy(UserTokenType.Certificate);
|
|
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
|
|
new UserTokenPolicy(UserTokenType.IssuedToken)
|
|
{
|
|
IssuedTokenType = Profiles.JwtUserToken
|
|
};
|
|
|
|
// Reconnect tests need an explicit legacy RSA token policy in addition to the
|
|
// generic channel-bound policies to cover alternate-token activation.
|
|
AddExplicitUserTokenPolicies(SecurityPolicies.Basic128Rsa15);
|
|
|
|
foreach (string securityPolicyUri in GetSupportedEccPolicyUris())
|
|
{
|
|
AddExplicitUserTokenPolicies(securityPolicyUri);
|
|
}
|
|
|
|
ServerFixture.Config.ServerConfiguration.MaxChannelCount = MaxChannelCount;
|
|
ServerFixture.Config.ServerConfiguration.MaxSubscriptionCount = 1000;
|
|
ServerFixture.Config.ServerConfiguration.MaxQueuedRequestCount = 100000;
|
|
ReferenceServer = await ServerFixture.StartAsync()
|
|
.ConfigureAwait(false);
|
|
ReferenceServer.TokenValidator = TokenValidator;
|
|
ServerFixturePort = ServerFixture.Port;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tear down the Server and the Client.
|
|
/// </summary>
|
|
public virtual async Task OneTimeTearDownAsync()
|
|
{
|
|
if (Session != null)
|
|
{
|
|
try
|
|
{
|
|
await Session.CloseAsync(5000, true).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(e, "Error closing session during teardown.");
|
|
}
|
|
Session.Dispose();
|
|
Session = null;
|
|
}
|
|
if (ServerFixture != null)
|
|
{
|
|
try
|
|
{
|
|
await ServerFixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(e, "Error stopping server during teardown.");
|
|
}
|
|
await Task.Delay(100).ConfigureAwait(false);
|
|
}
|
|
ClientFixture?.Dispose();
|
|
|
|
// Clean up pki
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(PkiRoot) && Directory.Exists(PkiRoot))
|
|
{
|
|
Directory.Delete(PkiRoot, true);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test setup.
|
|
/// </summary>
|
|
public virtual async Task SetUpAsync()
|
|
{
|
|
if (!SingleSession)
|
|
{
|
|
try
|
|
{
|
|
Session = await ClientFixture
|
|
.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Assert.Ignore(
|
|
$"OneTimeSetup failed to create session, tests skipped. Error: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test Teardown.
|
|
/// </summary>
|
|
public virtual async Task TearDownAsync()
|
|
{
|
|
if (!SingleSession && Session != null)
|
|
{
|
|
await Session.CloseAsync().ConfigureAwait(false);
|
|
Session.Dispose();
|
|
Session = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return a test set of nodes with static character.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namesapce table used in the session.</param>
|
|
/// <returns>The list of static test nodes.</returns>
|
|
public IList<NodeId> GetTestSetStatic(NamespaceTable namespaceUris)
|
|
{
|
|
return [.. TestSetStatic.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
|
|
.Where(n => !n.IsNull)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return a test set of nodes with static character.
|
|
/// 100 of each numeric data type.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namespace table used in the session.</param>
|
|
/// <returns>The list of static test nodes.</returns>
|
|
public IDictionary<NodeId, Type> GetTestSetStaticMassNumeric(NamespaceTable namespaceUris)
|
|
{
|
|
var result = new Dictionary<NodeId, Type>();
|
|
foreach ((Type Type, ExpandedNodeId[] NodeIds) entry in TestSetStaticMassNumeric)
|
|
{
|
|
Type type = entry.Type;
|
|
foreach (ExpandedNodeId expandedNodeId in entry.NodeIds)
|
|
{
|
|
var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, namespaceUris);
|
|
if (!nodeId.IsNull)
|
|
{
|
|
result[nodeId] = type;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return a test set of nodes with simulated values.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namesapce table used in the session.</param>
|
|
/// <returns>The list of simulated test nodes.</returns>
|
|
public IList<NodeId> GetTestSetSimulation(NamespaceTable namespaceUris)
|
|
{
|
|
return [.. TestSetSimulation.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
|
|
.Where(n => !n.IsNull)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return a test set of nodes all nodes with simulated values.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namesapce table used in the session.</param>
|
|
/// <returns>The list of simulated test nodes.</returns>
|
|
public IList<NodeId> GetTestSetFullSimulation(NamespaceTable namespaceUris)
|
|
{
|
|
var simulation = TestSetSimulation
|
|
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
|
|
.Where(n => !n.IsNull)
|
|
.ToList();
|
|
simulation.AddRange(
|
|
TestSetDataSimulation.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
|
|
.Where(n => !n.IsNull));
|
|
return simulation;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return a test data set of nodes with simulated values.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namesapce table used in the session.</param>
|
|
/// <returns>The list of simulated test nodes.</returns>
|
|
public IList<NodeId> GetTestSetDataSimulation(NamespaceTable namespaceUris)
|
|
{
|
|
return
|
|
[
|
|
.. TestSetDataSimulation.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
|
|
.Where(n => !n.IsNull)
|
|
];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return a test set of nodes with history values.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namesapce table used in the session.</param>
|
|
/// <returns>The list of test nodes.</returns>
|
|
public IList<NodeId> GetTestSetHistory(NamespaceTable namespaceUris)
|
|
{
|
|
return [.. TestSetHistory.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
|
|
.Where(n => !n.IsNull)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerator for security policies.
|
|
/// </summary>
|
|
public IEnumerable<string> BenchPolicies()
|
|
{
|
|
return Policies;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper variable for benchmark.
|
|
/// </summary>
|
|
[ParamsSource(nameof(BenchPolicies))]
|
|
public string SecurityPolicy = SecurityPolicies.None;
|
|
|
|
/// <summary>
|
|
/// Global Setup for benchmarks.
|
|
/// </summary>
|
|
public virtual void GlobalSetup()
|
|
{
|
|
m_logger.LogInformation("GlobalSetup: Start Server");
|
|
OneTimeSetUpCoreAsync().GetAwaiter().GetResult();
|
|
m_logger.LogInformation("GlobalSetup: Connecting");
|
|
Session = ClientFixture.ConnectAsync(ServerUrl, SecurityPolicy).GetAwaiter()
|
|
.GetResult();
|
|
m_logger.LogInformation("GlobalSetup: Ready");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Global cleanup for benchmarks.
|
|
/// </summary>
|
|
public virtual void GlobalCleanup()
|
|
{
|
|
m_logger.LogInformation("GlobalCleanup: Disconnect and Stop Server");
|
|
OneTimeTearDownAsync().GetAwaiter().GetResult();
|
|
m_logger.LogInformation("GlobalCleanup: Done");
|
|
}
|
|
|
|
public async Task GetOperationLimitsAsync()
|
|
{
|
|
OperationLimits = new OperationLimits
|
|
{
|
|
MaxNodesPerRead = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerHistoryReadData = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadData
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerHistoryReadEvents = await GetOperationLimitValueAsync(
|
|
VariableIds
|
|
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadEvents
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerWrite = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerWrite
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerHistoryUpdateData = await GetOperationLimitValueAsync(
|
|
VariableIds
|
|
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateData
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerHistoryUpdateEvents = await GetOperationLimitValueAsync(
|
|
VariableIds
|
|
.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateEvents
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerBrowse = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse
|
|
).ConfigureAwait(false),
|
|
MaxMonitoredItemsPerCall = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxMonitoredItemsPerCall
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerNodeManagement = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerNodeManagement
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerRegisterNodes = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRegisterNodes
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerTranslateBrowsePathsToNodeIds = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerTranslateBrowsePathsToNodeIds
|
|
).ConfigureAwait(false),
|
|
MaxNodesPerMethodCall = await GetOperationLimitValueAsync(
|
|
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerMethodCall
|
|
).ConfigureAwait(false)
|
|
};
|
|
}
|
|
|
|
public async Task<uint> GetOperationLimitValueAsync(NodeId nodeId)
|
|
{
|
|
try
|
|
{
|
|
return await Session.ReadValueAsync<uint>(nodeId).ConfigureAwait(false);
|
|
}
|
|
catch (ServiceResultException sre)
|
|
{
|
|
if (sre.StatusCode == StatusCodes.BadNodeIdUnknown)
|
|
{
|
|
return 0;
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void OutputSubscriptionInfo(TextWriter writer, Subscription subscription)
|
|
{
|
|
writer.WriteLine("Subscription : {0}", subscription.DisplayName);
|
|
writer.WriteLine("CurrentKeepAliveCount : {0}", subscription.CurrentKeepAliveCount);
|
|
writer.WriteLine(
|
|
"CurrentPublishingEnabled: {0}",
|
|
subscription.CurrentPublishingEnabled);
|
|
writer.WriteLine("CurrentPriority : {0}", subscription.CurrentPriority);
|
|
writer.WriteLine("PublishTime : {0}", subscription.PublishTime);
|
|
writer.WriteLine("LastNotificationTime : {0}", subscription.LastNotificationTime);
|
|
writer.WriteLine("SequenceNumber : {0}", subscription.SequenceNumber);
|
|
writer.WriteLine("NotificationCount : {0}", subscription.NotificationCount);
|
|
writer.WriteLine("LastNotification : {0}", subscription.LastNotification);
|
|
writer.WriteLine("Notifications : {0}", subscription.Notifications.Count());
|
|
writer.WriteLine(
|
|
"OutstandingMessageWorker: {0}",
|
|
subscription.OutstandingMessageWorkers);
|
|
}
|
|
|
|
private static ExpandedNodeId[] ReadCustomTestSet(string param)
|
|
{
|
|
// load custom test sets
|
|
string testSetParameter = TestContext.Parameters[param];
|
|
string[] testSetParameters = testSetParameter.Split('#');
|
|
if (testSetParameters != null)
|
|
{
|
|
// parse the custom content
|
|
var testSet = new List<ExpandedNodeId>();
|
|
foreach (string parameter in testSetParameters)
|
|
{
|
|
testSet.Add(ExpandedNodeId.Parse(parameter));
|
|
}
|
|
return [.. testSet];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
protected void SessionClosing(object sender, EventArgs e)
|
|
{
|
|
if (sender is ISession session)
|
|
{
|
|
TestContext.Out.WriteLine("Session_Closing: {0}", session.SessionId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a set of subscriptions with monitored items on the given session.
|
|
/// </summary>
|
|
protected async Task CreateSubscriptionsAsync(
|
|
ISession session,
|
|
Subscription template,
|
|
SubscriptionCollection originSubscriptions,
|
|
int[] notificationCounters,
|
|
int[] fastDataCounters,
|
|
int subscriptionCount,
|
|
uint queueSize)
|
|
{
|
|
for (int ii = 0; ii < subscriptionCount; ii++)
|
|
{
|
|
// create subscription with static monitored items
|
|
var subscription = new TestableSubscription(template)
|
|
{
|
|
PublishingEnabled = true,
|
|
Handle = ii,
|
|
FastDataChangeCallback = (s, n, _) =>
|
|
{
|
|
TestContext.Out.WriteLine(
|
|
$"FastDataChangeHandlerOrigin: {s.Id}-{n.SequenceNumber}-{n.MonitoredItems.Count}");
|
|
fastDataCounters[(int)s.Handle]++;
|
|
}
|
|
};
|
|
|
|
subscription.StateChanged += (s, e) =>
|
|
TestContext.Out
|
|
.WriteLine($"StateChanged: {s.Session.SessionId}-{s.Id}-{e.Status}");
|
|
|
|
subscription.PublishStatusChanged += (s, e) =>
|
|
TestContext.Out.WriteLine(
|
|
$"PublishStatusChanged: {s.Session.SessionId}-{s.Id}-{e.Status}");
|
|
|
|
originSubscriptions.Add(subscription);
|
|
session.AddSubscription(subscription);
|
|
await subscription.CreateAsync().ConfigureAwait(false);
|
|
|
|
// set defaults
|
|
subscription.DefaultItem.DiscardOldest = true;
|
|
subscription.DefaultItem.QueueSize = ii == 0 ? 0U : queueSize;
|
|
subscription.DefaultItem.MonitoringMode = MonitoringMode.Reporting;
|
|
|
|
// create test set using the passed session's namespace URIs
|
|
NamespaceTable namespaceUris = session.NamespaceUris;
|
|
var testSet = new List<NodeId>();
|
|
if (ii == 0)
|
|
{
|
|
testSet.AddRange(GetTestSetStatic(namespaceUris));
|
|
}
|
|
else
|
|
{
|
|
testSet.AddRange(GetTestSetSimulation(namespaceUris));
|
|
}
|
|
|
|
var list = CreateMonitoredItemTestSet(subscription, testSet).ToList();
|
|
list.ForEach(i =>
|
|
i.Notification += (item, _) =>
|
|
{
|
|
notificationCounters[(int)subscription.Handle]++;
|
|
foreach (DataValue value in item.DequeueValues())
|
|
{
|
|
TestContext.Out.WriteLine(
|
|
"Org:{0}: {1:20}, {2}, {3}, {4}",
|
|
subscription.Id,
|
|
item.DisplayName,
|
|
value.WrappedValue,
|
|
value.SourceTimestamp,
|
|
value.StatusCode);
|
|
}
|
|
});
|
|
subscription.AddItems(list);
|
|
await subscription.ApplyChangesAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create monitored items from a list of node ids on a subscription.
|
|
/// </summary>
|
|
protected static List<MonitoredItem> CreateMonitoredItemTestSet(
|
|
Subscription subscription,
|
|
IList<NodeId> nodeIds)
|
|
{
|
|
var list = new List<MonitoredItem>();
|
|
foreach (NodeId nodeId in nodeIds)
|
|
{
|
|
var item = new TestableMonitoredItem(subscription.DefaultItem)
|
|
{
|
|
StartNodeId = nodeId
|
|
};
|
|
list.Add(item);
|
|
}
|
|
return list;
|
|
}
|
|
}
|
|
}
|