Files
UA-.NETStandard/tests/Opc.Ua.Subscriptions.Durable.Tests/DurableSubscriptionTest.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

888 lines
36 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.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client;
using Opc.Ua.Client.TestFramework;
using Opc.Ua.Server.TestFramework;
using Quickstarts.ReferenceServer;
namespace Opc.Ua.Subscriptions.Durable.Tests
{
/// <summary>
/// Test Client Services.
/// </summary>
[TestFixture]
[Category("Client")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
public class DurableSubscriptionTest : ClientTestFramework
{
public readonly uint MillisecondsPerHour = 3600 * 1000;
/// <summary>
/// Set up a Server and a Client instance.
/// </summary>
[OneTimeSetUp]
public override Task OneTimeSetUpAsync()
{
// the tests can be run against server specified in .runsettings
SupportsExternalServerUrl = true;
// create a new session for every test
SingleSession = false;
MaxChannelCount = 1000;
return OneTimeSetUpCoreAsync(securityNone: true);
}
public override 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 = true,
OperationLimits = true,
DurableSubscriptionsEnabled = true
};
}
await ServerFixture.LoadConfigurationAsync(PkiRoot).ConfigureAwait(false);
ServerFixture.Config.TransportQuotas!.MaxMessageSize = TransportQuotaMaxMessageSize;
ServerFixture.Config.TransportQuotas.MaxByteStringLength = ServerFixture
.Config
.TransportQuotas
.MaxStringLength = TransportQuotaMaxStringLength;
ServerFixture.Config.ServerConfiguration!.MinSessionTimeout = 1000;
ServerFixture.Config.ServerConfiguration.MinSubscriptionLifetime = 1500;
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
};
ReferenceServer = await ServerFixture.StartAsync()
.ConfigureAwait(false);
ReferenceServer.TokenValidator = TokenValidator;
ServerFixturePort = ServerFixture.Port;
}
/// <summary>
/// Tear down the Server and the Client.
/// </summary>
[OneTimeTearDown]
public override Task OneTimeTearDownAsync()
{
return base.OneTimeTearDownAsync();
}
/// <summary>
/// Test setup.
/// </summary>
[SetUp]
public override Task SetUpAsync()
{
return MySetUpAsync();
}
private async Task MySetUpAsync()
{
if (!SingleSession)
{
try
{
ClientFixture.SessionTimeout = 10000;
var userIdentity = new UserIdentity("sysadmin", "demo"u8);
Session = await ClientFixture
.ConnectAsync(
ServerUrl,
SecurityPolicies.Basic256Sha256,
default,
userIdentity)
.ConfigureAwait(false);
Session.DeleteSubscriptionsOnClose = false;
}
catch (Exception e)
{
Assert.Ignore(
$"OneTimeSetup failed to create session, tests skipped. Error: {e.Message}");
}
}
}
/// <summary>
/// Test teardown.
/// </summary>
[TearDown]
public override Task TearDownAsync()
{
return base.TearDownAsync();
}
[Test]
[Order(100)]
[TestCase(900, 100u, 100u, 10000u, 3600u, 14_400_000u, TestName = "Test Lifetime Over Maximum")]
[TestCase(900, 100u, 100u, 0u, 3600u, 14_400_000u, TestName = "Test Lifetime Zero")]
[TestCase(1200, 100u, 100u, 1u, 1u, 3000u, TestName = "Test Lifetime One")]
[TestCase(
60000,
183u,
61u,
1u,
1u,
60u,
TestName = "Test Lifetime Reduce Count",
Description = "Expected MaxLifetimeCount matches what the demo server does"
)]
public async Task TestLifetimeAsync(
int publishingInterval,
uint keepAliveCount,
uint lifetimeCount,
uint requestedHours,
uint expectedHours,
uint expectedLifetime)
{
using var subscription = new TestableSubscription(Session.DefaultSubscription)
{
KeepAliveCount = keepAliveCount,
LifetimeCount = lifetimeCount,
PublishingInterval = publishingInterval
};
Assert.That(Session.AddSubscription(subscription), Is.True);
await subscription.CreateAsync().ConfigureAwait(false);
Dictionary<string, NodeId> desiredNodeIds =
await GetDesiredNodeIdsAsync(subscription.Id).ConfigureAwait(false);
(bool success, uint revisedLifetimeInHours) =
await subscription.SetSubscriptionDurableAsync(requestedHours).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(revisedLifetimeInHours, Is.EqualTo(expectedHours));
Dictionary<string, object> modifiedValues =
await GetValuesAsync(desiredNodeIds).ConfigureAwait(false);
Assert.That(modifiedValues["MaxLifetimeCount"], Is.InstanceOf<DataValue>());
var maxLifetimeCountValue = (DataValue)modifiedValues["MaxLifetimeCount"];
Assert.That(maxLifetimeCountValue.WrappedValue.IsNull, Is.False);
Assert.That(
maxLifetimeCountValue.WrappedValue.ConvertToUInt32(),
Is.EqualTo(expectedLifetime));
Assert.That(await Session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false), Is.True);
}
[Test]
[Order(110)]
[TestCase(0u, 1u, 1u, false, TestName = "QueueSize 0")]
[TestCase(101u, 101u, 102u, false, TestName = "QueueSize over standard subscripion limit")]
[TestCase(9999u, 1000u, 1000u, false, TestName = "QueueSize over durable limit")]
// Part 4 §7.21: event queueSize 0 is the server default and 1 the
// server minimum, so neither is returned literally.
[TestCase(0u, 1000u, 1000u, true, TestName = "QueueSize 0 Event MI")]
[TestCase(
1001u,
1001u,
1002u,
true,
TestName = "QueueSize over standard subscripion limit Event MI")]
[TestCase(
99999u,
10000u,
10000u,
true,
TestName = "QueueSize over durable limit, Event MI")]
public async Task TestRevisedQueueSizeAsync(
uint queueSize,
uint expectedRevisedQueueSize,
uint expectedModifiedQueueSize,
bool useEventMI)
{
using TestableSubscription subscription = await CreateDurableSubscriptionAsync()
.ConfigureAwait(false);
MonitoredItem mi;
if (useEventMI)
{
mi = CreateEventMonitoredItem(queueSize);
}
else
{
mi = new MonitoredItem(Session.MessageContext.Telemetry)
{
AttributeId = Attributes.Value,
StartNodeId = VariableIds.Server_ServerStatus_CurrentTime,
MonitoringMode = MonitoringMode.Reporting,
Handle = 1,
SamplingInterval = 500,
Filter = null,
DiscardOldest = true,
QueueSize = queueSize
};
}
subscription.AddItem(mi);
ArrayOf<MonitoredItem> result = await subscription.CreateItemsAsync().ConfigureAwait(false);
Assert.That(ServiceResult.IsGood(result[0].Status.Error), Is.True);
Assert
.That(result[0].Status.QueueSize, Is.EqualTo(expectedRevisedQueueSize));
mi.QueueSize = queueSize + 1;
ArrayOf<MonitoredItem> resultModify = await subscription.ModifyItemsAsync()
.ConfigureAwait(false);
Assert
.That(ServiceResult.IsGood(resultModify[0].Status.Error), Is.True);
Assert
.That(resultModify[0].Status.QueueSize, Is.EqualTo(expectedModifiedQueueSize));
(bool success, _, _) = await subscription.GetMonitoredItemsAsync().ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(await Session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false), Is.True);
}
[Test]
[Order(160)]
public async Task SetSubscriptionDurableFailsWhenMIExistsAsync()
{
using var subscription = new TestableSubscription(Session.DefaultSubscription)
{
KeepAliveCount = 100u,
LifetimeCount = 100u,
PublishingInterval = 900
};
Assert.That(Session.AddSubscription(subscription), Is.True);
await subscription.CreateAsync().ConfigureAwait(false);
uint id = subscription.Id;
var mi = new MonitoredItem(Session.MessageContext.Telemetry)
{
AttributeId = Attributes.Value,
StartNodeId = VariableIds.Server_ServerStatus_CurrentTime,
MonitoringMode = MonitoringMode.Reporting,
Handle = 1,
SamplingInterval = 500,
Filter = null,
DiscardOldest = true,
QueueSize = 1
};
subscription.AddItem(mi);
ArrayOf<MonitoredItem> result = await subscription.CreateItemsAsync().ConfigureAwait(false);
Assert.That(ServiceResult.IsGood(result[0].Status.Error), Is.True);
Assert.ThrowsAsync<ServiceResultException>(() =>
Session.CallAsync(ObjectIds.Server, MethodIds.Server_SetSubscriptionDurable, default, id, 1));
Assert.That(await Session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false), Is.True);
}
[Test]
[Order(180)]
public async Task SetSubscriptionDurableFailsWhenSubscriptionDoesNotExistAsync()
{
using var subscription = new TestableSubscription(Session.DefaultSubscription)
{
KeepAliveCount = 100u,
LifetimeCount = 100u,
PublishingInterval = 900
};
Assert.That(Session.AddSubscription(subscription), Is.True);
await subscription.CreateAsync().ConfigureAwait(false);
uint id = subscription.Id;
Assert.That(await Session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false), Is.True);
Assert.ThrowsAsync<ServiceResultException>(() =>
Session.CallAsync(ObjectIds.Server, MethodIds.Server_SetSubscriptionDurable, default, id, 1));
}
[Test]
[Order(200)]
[TestCase(false, false, TestName = "Validate Session Close")]
[TestCase(true, false, TestName = "Validate Transfer")]
[TestCase(true, true, TestName = "Restart of Server")]
public async Task TestSessionTransferAsync(bool setSubscriptionDurable, bool restartServer)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Assert.Ignore("Timing on mac OS causes issues");
}
ISession? transferSession = null;
try
{
transferSession = await TestSessionTransferInternalAsync(
setSubscriptionDurable,
restartServer).ConfigureAwait(false);
}
finally
{
if (transferSession != null)
{
try
{
transferSession.DeleteSubscriptionsOnClose = true;
TestContext.Out.WriteLine("------- Transfer session closing --------");
await transferSession.CloseAsync().ConfigureAwait(false);
}
finally
{
transferSession.Dispose();
}
}
}
}
private async Task<ISession> TestSessionTransferInternalAsync(
bool setSubscriptionDurable,
bool restartServer)
{
const int publishingInterval = 100;
const uint keepAliveCount = 5;
const uint lifetimeCount = 15;
const uint requestedHours = 1;
const uint expectedHours = 1;
const uint expectedLifetime = 36000;
using var subscription = new TestableSubscription(Session.DefaultSubscription)
{
KeepAliveCount = keepAliveCount,
LifetimeCount = lifetimeCount,
PublishingInterval = publishingInterval,
MinLifetimeInterval = 1500
};
subscription.StateChanged += (s, e) =>
TestContext.Out.WriteLine($"StateChanged: {s.Session!.SessionId}-{s.Id}-{e.Status}");
Assert.That(Session.AddSubscription(subscription), Is.True);
await subscription.CreateAsync().ConfigureAwait(false);
// Give some time to allow for the true browse of items
await Task.Delay(500).ConfigureAwait(false);
Dictionary<string, NodeId> desiredNodeIds =
await GetDesiredNodeIdsAsync(subscription.Id).ConfigureAwait(false);
Dictionary<string, object> initialValues =
await GetValuesAsync(desiredNodeIds).ConfigureAwait(false);
if (setSubscriptionDurable)
{
(bool success, uint revisedLifetimeInHours) =
await subscription.SetSubscriptionDurableAsync(requestedHours).ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(revisedLifetimeInHours, Is.EqualTo(expectedHours));
await ValidateDataValueAsync(desiredNodeIds, "MaxLifetimeCount", expectedLifetime)
.ConfigureAwait(false);
}
else
{
await ValidateDataValueAsync(desiredNodeIds, "MaxLifetimeCount", lifetimeCount)
.ConfigureAwait(false);
}
var testSet = new List<NodeId>();
testSet.AddRange(GetTestSetFullSimulation(Session.NamespaceUris));
var valueTimeStamps = new Dictionary<NodeId, List<DateTimeUtc>>();
TaskCompletionSource<bool>? restartDataReceived = restartServer
? new(TaskCreationOptions.RunContinuationsAsynchronously)
: null;
bool awaitingRestartData = false;
var monitoredItemsList = new List<MonitoredItem>();
foreach (NodeId nodeId in testSet)
{
if (nodeId.IdType == IdType.String)
{
valueTimeStamps.Add(nodeId, []);
var monitoredItem = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = nodeId,
SamplingInterval = 1000,
QueueSize = 100
};
monitoredItem.Notification += (item, _) =>
{
List<DateTimeUtc> list = valueTimeStamps[nodeId];
foreach (DataValue value in item.DequeueValues())
{
list.Add(value.SourceTimestamp);
if (awaitingRestartData)
{
restartDataReceived?.TrySetResult(true);
}
}
};
monitoredItemsList.Add(monitoredItem);
}
}
//Add Event Monitored Item
monitoredItemsList.Add(CreateEventMonitoredItem(100));
DateTimeUtc startTime = DateTimeUtc.Now;
subscription.AddItems(monitoredItemsList);
await subscription.ApplyChangesAsync().ConfigureAwait(false);
await Task.Delay(2000).ConfigureAwait(false);
Dictionary<string, object> closeValues =
await GetValuesAsync(desiredNodeIds).ConfigureAwait(false);
var subscriptions = new SubscriptionCollection(Session.Subscriptions);
DateTimeUtc closeTime = DateTimeUtc.Now;
TestContext.Out.WriteLine("Session Id {0} Closing at {1}",
Session.SessionId, closeTime);
await Session.CloseAsync(closeChannel: false).ConfigureAwait(false);
TestContext.Out.WriteLine("Session closed. Initiated at {0}", closeTime);
if (restartServer)
{
// if durable subscription the server will restore the subscription
TestContext.Out.WriteLine("------- Server stopping --------");
await ReferenceServer.StopAsync().ConfigureAwait(false);
await ReferenceServer.StartAsync(ServerFixture.Config).ConfigureAwait(false);
TestContext.Out.WriteLine("------- Server restarted --------");
}
else
{
// Subscription should time out with initial lifetime count
await Task.Delay(3000).ConfigureAwait(false);
}
DateTimeUtc restartTime = DateTimeUtc.Now;
#if !DEBUG_CONNECT_FAILED
var transferUserIdentity = new UserIdentity("sysadmin", "demo"u8);
ISession transferSession = await ClientFixture
.ConnectAsync(
ServerUrl,
SecurityPolicies.Basic256Sha256,
default,
transferUserIdentity)
.ConfigureAwait(false);
#else // TODO: Remove once failure is understood.
ISession transferSession;
for (int i = 0; ; i++)
{
try
{
var retryUserIdentity = new UserIdentity("sysadmin", "demo"u8);
transferSession = await ClientFixture
.ConnectAsync(
ServerUrl,
SecurityPolicies.Basic256Sha256,
null,
retryUserIdentity)
.ConfigureAwait(false);
if (i != 0)
{
Debugger.Break();
}
break;
}
catch
{
TestContext.Out.WriteLine("------- Transfer session failed to connect --------");
while (!Debugger.IsAttached)
{
System.Threading.Thread.Sleep(5000);
}
Debugger.Break();
}
}
#endif
awaitingRestartData = restartServer && setSubscriptionDurable;
Task restartData = restartDataReceived?.Task ?? Task.CompletedTask;
bool result = await transferSession.TransferSubscriptionsAsync(subscriptions, true)
.ConfigureAwait(false);
TestContext.Out.WriteLine("------- Dispose original session --------");
Session.Dispose();
Session = null!;
bool expected = setSubscriptionDurable; // Otherwise we close the session above and then transfer fails.
Assert.That(
result,
Is.EqualTo(expected),
$"SetSubscriptionDurable = {setSubscriptionDurable} => Transfer Result: {result} != Expected {expected}");
if (restartServer && setSubscriptionDurable)
{
await restartData.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
Assert.That(
valueTimeStamps.Values.Any(values => values.Count > 0),
Is.True,
"A transferred subscription did not deliver queued data after restart.");
}
if (setSubscriptionDurable && !restartServer)
{
// New Session and Transfer - consume 4 seconds of messages then turn off.
await Task.Delay(4000).ConfigureAwait(false);
await subscription.SetPublishingModeAsync(false).ConfigureAwait(false);
DateTimeUtc completionTime = DateTimeUtc.Now;
await Task.Delay(1000).ConfigureAwait(false); // Let last notifications trickle through
const double tolerance = 2500;
TestContext.Out.WriteLine("Session StartTime at {0}", DateTimeMs(startTime));
TestContext.Out.WriteLine("Session Closed at {0}", DateTimeMs(closeTime));
TestContext.Out.WriteLine("Restart at {0}", DateTimeMs(restartTime));
TestContext.Out.WriteLine("Completion at {0}", DateTimeMs(completionTime));
// Validate
foreach (KeyValuePair<NodeId, List<DateTimeUtc>> pair in valueTimeStamps)
{
DateTimeUtc previous = startTime;
for (int index = 0; index < pair.Value.Count; index++)
{
DateTimeUtc timestamp = pair.Value[index];
TimeSpan timeSpan = timestamp - previous;
TestContext.Out.WriteLine(
$"Node: {pair.Key} Index: {index} Time: {DateTimeMs(timestamp)} " +
$"Previous: {DateTimeMs(previous)} " +
$"Timespan {timeSpan.TotalMilliseconds.ToString("000.", CultureInfo.InvariantCulture)}");
Assert.That(
Math.Abs(timeSpan.TotalMilliseconds),
Is.LessThan(tolerance),
$"Node: {pair.Key} Index: {index} Timespan {timeSpan.TotalMilliseconds} ");
previous = timestamp;
if (index == pair.Value.Count - 1)
{
TimeSpan finalTimeSpan = completionTime - timestamp;
Assert.That(
Math.Abs(finalTimeSpan.TotalMilliseconds),
Is.LessThan(tolerance * 2),
$"Last Value - Node: {pair.Key} Index: {index} Timespan {finalTimeSpan.TotalMilliseconds} ");
}
}
}
}
return transferSession;
}
private async Task<Dictionary<string, object>> ValidateDataValueAsync(
Dictionary<string, NodeId> nodeIds,
string desiredValue,
uint expectedValue)
{
Dictionary<string, object> modifiedValues =
await GetValuesAsync(nodeIds).ConfigureAwait(false);
Assert.That(modifiedValues[desiredValue], Is.InstanceOf<DataValue>());
var dataValue = (DataValue)modifiedValues[desiredValue];
Assert.That(dataValue.WrappedValue.IsNull, Is.False);
Assert.That(
dataValue.WrappedValue.ConvertToUInt32(),
Is.EqualTo(expectedValue));
return modifiedValues;
}
private async Task<TestableSubscription> CreateDurableSubscriptionAsync()
{
var subscription = new TestableSubscription(Session.DefaultSubscription)
{
KeepAliveCount = 100u,
LifetimeCount = 100u,
PublishingInterval = 900
};
Assert.That(Session.AddSubscription(subscription), Is.True);
await subscription.CreateAsync().ConfigureAwait(false);
(bool success, _) = await subscription.SetSubscriptionDurableAsync(1)
.ConfigureAwait(false);
Assert.That(success, Is.True);
return subscription;
}
private async Task<Dictionary<string, NodeId>> GetDesiredNodeIdsAsync(
uint subscriptionId)
{
var desiredNodeIds = new Dictionary<string, NodeId>();
var serverDiags = new NodeId(
Variables.Server_ServerDiagnostics_SubscriptionDiagnosticsArray);
NodeId monitoredItemCountNodeId = default;
NodeId maxLifetimeCountNodeId = default;
NodeId maxKeepAliveCountNodeId = default;
NodeId currentLifetimeCountNodeId = default;
NodeId publishingIntervalNodeId = default;
(_, _, ArrayOf<ReferenceDescription> references) = await Session.BrowseAsync(
null,
null,
serverDiags,
0u,
BrowseDirection.Forward,
ReferenceTypeIds.HierarchicalReferences,
true,
0).ConfigureAwait(false);
Assert.That(references.IsNull, Is.False, "Initial Browse has no references");
Assert.That(references.Count, Is.GreaterThan(0), "Initial Browse has zero references");
TestContext.Out.WriteLine(
"Initial Browse for SubscriptionDiagnosticsArray has {0} references, Desired SubscriptionId {1}",
references.Count,
subscriptionId);
foreach (ReferenceDescription reference in references.ToList())
{
TestContext.Out
.WriteLine("Initial Browse Reference {0}", reference.BrowseName.Name);
if (reference.BrowseName.Name == subscriptionId.ToString(
CultureInfo.InvariantCulture))
{
(
_,
ByteString anotherContinuationPoint,
ArrayOf<ReferenceDescription> desiredReferences
) = await Session.BrowseAsync(
null,
null,
(NodeId)reference.NodeId,
0u,
BrowseDirection.Forward,
ReferenceTypeIds.HierarchicalReferences,
true,
0).ConfigureAwait(false);
Assert.That(desiredReferences.IsNull, Is.False, "Secondary Browse has no references");
Assert.That(
desiredReferences.Count,
Is.GreaterThan(0),
"Secondary Browse has zero references");
TestContext.Out.WriteLine(
"Secondary Browse for SubscriptionId {0} has {1} references",
subscriptionId,
desiredReferences.Count);
foreach (ReferenceDescription referenceDescription in desiredReferences)
{
NodeId recreated = default;
if (referenceDescription.NodeId.IsNull)
{
TestContext.Out.WriteLine(
"Subscription Reference {0} ExpandedNodeId is Null",
referenceDescription.BrowseName.Name);
TestContext.Out.WriteLine(
"Full ReferenceDescription {0}",
referenceDescription.ToString());
}
else
{
recreated = referenceDescription.NodeId.InnerNodeId.WithNamespaceIndex(
referenceDescription.NodeId.NamespaceIndex);
if (recreated.IsNull)
{
TestContext.Out.WriteLine(
"Subscription Reference {0} Recreated Node is Null",
referenceDescription.BrowseName.Name);
TestContext.Out.WriteLine(
"Full ReferenceDescription {0}",
referenceDescription.ToString());
}
else
{
TestContext.Out.WriteLine(
"Subscription Reference {0} ExpandedNodeId {1} Recreated {2}",
referenceDescription.BrowseName.Name,
referenceDescription.NodeId.ToString(),
recreated.ToString());
}
}
if (referenceDescription.BrowseName.Name!.Equals(
"MonitoredItemCount",
StringComparison.OrdinalIgnoreCase))
{
monitoredItemCountNodeId = recreated;
}
else if (referenceDescription.BrowseName.Name.Equals(
"MaxLifetimeCount",
StringComparison.OrdinalIgnoreCase))
{
maxLifetimeCountNodeId = recreated;
}
else if (referenceDescription.BrowseName.Name.Equals(
"MaxKeepAliveCount",
StringComparison.OrdinalIgnoreCase))
{
maxKeepAliveCountNodeId = recreated;
}
else if (referenceDescription.BrowseName.Name.Equals(
"CurrentLifetimeCount",
StringComparison.OrdinalIgnoreCase))
{
currentLifetimeCountNodeId = recreated;
}
else if (referenceDescription.BrowseName.Name.Equals(
"PublishingInterval",
StringComparison.OrdinalIgnoreCase))
{
publishingIntervalNodeId = recreated;
}
}
break;
}
}
Assert.That(monitoredItemCountNodeId.IsNull, Is.False, "Unable to find MonitoredItemCount");
Assert.That(maxLifetimeCountNodeId.IsNull, Is.False, "Unable to find MaxLifetimeCount");
Assert.That(maxKeepAliveCountNodeId.IsNull, Is.False, "Unable to find MaxKeepAliveCount");
Assert.That(currentLifetimeCountNodeId.IsNull, Is.False, "Unable to find CurrentLifetimeCount");
Assert.That(publishingIntervalNodeId.IsNull, Is.False, "Unable to find PublishingInterval");
desiredNodeIds.Add("MonitoredItemCount", monitoredItemCountNodeId);
desiredNodeIds.Add("MaxLifetimeCount", maxLifetimeCountNodeId);
desiredNodeIds.Add("MaxKeepAliveCount", maxKeepAliveCountNodeId);
desiredNodeIds.Add("CurrentLifetimeCount", currentLifetimeCountNodeId);
desiredNodeIds.Add("PublishingInterval", publishingIntervalNodeId);
return desiredNodeIds;
}
private async Task<Dictionary<string, object>> GetValuesAsync(Dictionary<string, NodeId> ids)
{
var values = new Dictionary<string, object>();
foreach (KeyValuePair<string, NodeId> id in ids)
{
values.Add(id.Key,
await Session.ReadValueAsync(id.Value).ConfigureAwait(false));
TestContext.Out.WriteLine($"{id.Key}: {values[id.Key]}");
}
return values;
}
private MonitoredItem CreateEventMonitoredItem(uint queueSize)
{
var whereClause = new ContentFilter();
whereClause.Push(
FilterOperator.Equals,
[
Variant.FromStructure(new SimpleAttributeOperand
{
AttributeId = Attributes.Value,
TypeDefinitionId = ObjectTypeIds.BaseEventType,
BrowsePath = [.. new QualifiedName[] { QualifiedName.From("EventType") }]
}),
Variant.FromStructure(new LiteralOperand
{
Value = Variant.From(ObjectTypeIds.BaseEventType)
})
]);
return new MonitoredItem(Session.MessageContext.Telemetry)
{
AttributeId = Attributes.EventNotifier,
StartNodeId = ObjectIds.Server,
MonitoringMode = MonitoringMode.Reporting,
Handle = 1,
SamplingInterval = -1,
Filter = new EventFilter
{
SelectClauses =
[
.. new SimpleAttributeOperand[]
{
new()
{
AttributeId = Attributes.Value,
TypeDefinitionId = ObjectTypeIds.BaseEventType,
BrowsePath = [.. new QualifiedName[] { QualifiedName.From(BrowseNames.Message) }]
}
}
],
WhereClause = whereClause
},
DiscardOldest = true,
QueueSize = queueSize
};
}
private static string DateTimeMs(DateTimeUtc dateTime)
{
var dt = dateTime.ToDateTime();
return dt.ToLongTimeString() +
"." +
dt.Millisecond.ToString("D3", CultureInfo.InvariantCulture);
}
}
}