Files
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

1040 lines
48 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
namespace Opc.Ua.Server.TestFramework
{
/// <summary>
/// Test workers using test services.
/// </summary>
public static class CommonTestWorkers
{
public const int DefaultMonitoredItemsQueueSize = 0;
public const int DefaultMonitoredItemsSamplingInterval = -1;
public static readonly ExpandedNodeId[] NodeIdTestSetStatic =
[
new ExpandedNodeId(
"Scalar_Static_SByte",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_Int16",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_Int32",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_Byte",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_UInt16",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_UInt32",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_NodeId",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_LocalizedText",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_QualifiedName",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Static_Variant",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer)
];
/// <summary>
/// static variables from namespace TestData
/// </summary>
public static readonly ExpandedNodeId[] NodeIdTestDataSetStatic =
[
new ExpandedNodeId(
TestData.Variables.Data_Static_Scalar_Int16Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Static_Scalar_Int32Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Static_Scalar_UInt16Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Static_Scalar_UInt32Value,
TestData.Namespaces.TestData)
];
/// <summary>
/// CTT simulation data
/// </summary>
public static readonly ExpandedNodeId[] NodeIdTestSetSimulation =
[
new ExpandedNodeId(
"Scalar_Simulation_SByte",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_Int16",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_Int32",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_Byte",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_UInt16",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_UInt32",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_NodeId",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer),
new ExpandedNodeId(
"Scalar_Simulation_LocalizedText",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer
),
new ExpandedNodeId(
"Scalar_Simulation_QualifiedName",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer
),
new ExpandedNodeId(
"Scalar_Simulation_Variant",
Quickstarts.ReferenceServer.Namespaces.ReferenceServer)
];
/// <summary>
/// Ref server test data node manager.
/// </summary>
public static readonly ExpandedNodeId[] NodeIdTestSetDataSimulation =
[
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_Int16Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_Int32Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_UInt16Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_UInt32Value,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.AnalogScalarValueObjectType_UInt32Value,
TestData.Namespaces.TestData
),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_AnalogArray_ByteValue,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_VectorValue,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_VectorValue_X,
TestData.Namespaces.TestData),
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Structure_ScalarStructure,
TestData.Namespaces.TestData)
];
public static readonly ExpandedNodeId[] NodeIdTestDataHistory =
[
new ExpandedNodeId(
TestData.Variables.Data_Dynamic_Scalar_Int32Value,
TestData.Namespaces.TestData)
];
public static readonly ExpandedNodeId[] NodeIdMemoryBufferSimulation =
[
// dynamic variables from namespace MemoryBuffer
new ExpandedNodeId("UInt32[64]", MemoryBuffer.Namespaces.MemoryBuffer + "/Instance"),
new ExpandedNodeId("Double[40]", MemoryBuffer.Namespaces.MemoryBuffer + "/Instance")
];
/// <summary>
/// Mass numeric static nodes for performance tests.
/// </summary>
public static readonly (Type Type, ExpandedNodeId[] NodeIds)[] NodeIdTestSetStaticMassNumeric =
[
(typeof(short), Enumerable.Range(0, 100).Select(i =>
new ExpandedNodeId($"Scalar_Static_Mass_Int16_Int16_{i:D2}", Quickstarts.ReferenceServer.Namespaces.ReferenceServer)).ToArray()),
(typeof(int), Enumerable.Range(0, 100).Select(i =>
new ExpandedNodeId($"Scalar_Static_Mass_Int32_Int32_{i:D2}", Quickstarts.ReferenceServer.Namespaces.ReferenceServer)).ToArray()),
(typeof(long), Enumerable.Range(0, 100).Select(i =>
new ExpandedNodeId($"Scalar_Static_Mass_Int64_Int64_{i:D2}", Quickstarts.ReferenceServer.Namespaces.ReferenceServer)).ToArray()),
(typeof(ushort), Enumerable.Range(0, 100).Select(i =>
new ExpandedNodeId($"Scalar_Static_Mass_UInt16_UInt16_{i:D2}", Quickstarts.ReferenceServer.Namespaces.ReferenceServer)).ToArray()),
(typeof(uint), Enumerable.Range(0, 100).Select(i =>
new ExpandedNodeId($"Scalar_Static_Mass_UInt32_UInt32_{i:D2}", Quickstarts.ReferenceServer.Namespaces.ReferenceServer)).ToArray()),
(typeof(ulong), Enumerable.Range(0, 100).Select(i =>
new ExpandedNodeId($"Scalar_Static_Mass_UInt64_UInt64_{i:D2}", Quickstarts.ReferenceServer.Namespaces.ReferenceServer)).ToArray())
];
/// <summary>
/// Worker function to browse the full address space of a server.
/// </summary>
/// <param name="services">The service interface.</param>
/// <param name="requestHeader">The request header.</param>
/// <param name="operationLimits">The operation limits.</param>
/// <param name="browseDescription">The browse template.</param>
/// <param name="outputResult">Attach the complete listing rather than retaining it in test output.</param>
public static async Task<ArrayOf<ReferenceDescription>> BrowseFullAddressSpaceWorkerAsync(
IServerTestServices services,
RequestHeader requestHeader,
OperationLimits? operationLimits = null,
BrowseDescription? browseDescription = null,
bool outputResult = false)
{
operationLimits ??= new OperationLimits();
requestHeader.Timestamp = DateTime.UtcNow;
// Browse template
NodeId startingNode = ObjectIds.RootFolder;
BrowseDescription browseTemplate =
browseDescription
?? new BrowseDescription
{
NodeId = startingNode,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};
ArrayOf<BrowseDescription> browseDescriptionCollection =
ServerFixtureUtils.CreateBrowseDescriptionCollectionFromNodeId(
[.. new NodeId[] { ObjectIds.RootFolder }],
browseTemplate);
// Browse
uint requestedMaxReferencesPerNode = operationLimits.MaxNodesPerBrowse;
bool verifyMaxNodesPerBrowse = operationLimits.MaxNodesPerBrowse > 0;
var referenceDescriptions = new List<ReferenceDescription>();
// Test if server responds with BadNothingToDo
{
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
_ = await services.BrowseAsync(
requestHeader,
null!,
0,
[]).ConfigureAwait(false))!;
Assert.That(sre!.StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo));
}
while (browseDescriptionCollection.Count > 0)
{
var allResults = new List<BrowseResult>();
if (verifyMaxNodesPerBrowse &&
browseDescriptionCollection.Count > operationLimits.MaxNodesPerBrowse)
{
verifyMaxNodesPerBrowse = false;
// Test if server responds with BadTooManyOperations
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
_ = await services.BrowseAsync(
requestHeader,
null!,
0,
browseDescriptionCollection).ConfigureAwait(false))!;
Assert.That(
sre!.StatusCode,
Is.EqualTo(StatusCodes.BadTooManyOperations));
// Test if server responds with BadTooManyOperations
ArrayOf<BrowseDescription> tempBrowsePath =
browseDescriptionCollection[..((int)operationLimits.MaxNodesPerBrowse + 1)];
sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
_ = await services.BrowseAsync(
requestHeader,
null!,
0,
tempBrowsePath).ConfigureAwait(false))!;
Assert.That(
sre!.StatusCode,
Is.EqualTo(StatusCodes.BadTooManyOperations));
}
bool repeatBrowse;
uint maxNodesPerBrowse = operationLimits.MaxNodesPerBrowse;
BrowseResponse? browseResponse = null;
do
{
if (maxNodesPerBrowse >= browseDescriptionCollection.Count)
{
maxNodesPerBrowse = 0; // Do not slice, take all
}
ArrayOf<BrowseDescription> browseCollection = maxNodesPerBrowse == 0
? browseDescriptionCollection
: browseDescriptionCollection[..(int)maxNodesPerBrowse];
repeatBrowse = false;
try
{
requestHeader.Timestamp = DateTime.UtcNow;
browseResponse = await services.BrowseAsync(
requestHeader,
null!,
requestedMaxReferencesPerNode,
browseCollection).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(
browseResponse.ResponseHeader,
browseResponse.Results,
browseCollection);
ServerFixtureUtils.ValidateDiagnosticInfos(
browseResponse.DiagnosticInfos,
browseCollection,
browseResponse.ResponseHeader.StringTable,
services.Logger);
allResults.AddRange(browseResponse.Results);
}
catch (ServiceResultException sre) when (
sre.StatusCode == StatusCodes.BadEncodingLimitsExceeded ||
sre.StatusCode == StatusCodes.BadResponseTooLarge)
{
// try to address by overriding operation limit
maxNodesPerBrowse =
maxNodesPerBrowse == 0
? (uint)browseCollection.Count / 2
: maxNodesPerBrowse / 2;
repeatBrowse = true;
}
} while (repeatBrowse);
browseDescriptionCollection = maxNodesPerBrowse == 0 ?
default :
browseDescriptionCollection[(int)maxNodesPerBrowse..];
// Browse next
ArrayOf<ByteString> continuationPoints = ServerFixtureUtils.PrepareBrowseNext(
browseResponse!.Results);
while (continuationPoints.Count > 0)
{
requestHeader.Timestamp = DateTime.UtcNow;
BrowseNextResponse browseNextResponse = await services.BrowseNextAsync(
requestHeader,
false,
continuationPoints).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(
browseNextResponse.ResponseHeader,
browseNextResponse.Results,
continuationPoints);
ServerFixtureUtils.ValidateDiagnosticInfos(
browseNextResponse.DiagnosticInfos,
continuationPoints,
browseNextResponse.ResponseHeader.StringTable,
services.Logger);
allResults.AddRange(browseNextResponse.Results);
continuationPoints = ServerFixtureUtils.PrepareBrowseNext(
browseNextResponse.Results);
}
// Build browse request for next level
var browseTable = new List<NodeId>();
foreach (BrowseResult result in allResults)
{
referenceDescriptions.AddRange(result.References);
foreach (ReferenceDescription reference in result.References)
{
browseTable.Add(ExpandedNodeId.ToNodeId(reference.NodeId, null!));
}
}
browseDescriptionCollection = ServerFixtureUtils
.CreateBrowseDescriptionCollectionFromNodeId(
browseTable,
browseTemplate);
}
referenceDescriptions.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
int diagnosticNs = services.MessageContext.NamespaceUris
.GetIndex(Ua.Namespaces.OpcUa + "Diagnostics");
// Remove diagnostic nodes since they change per session
referenceDescriptions.RemoveAll(r => r.NodeId.NamespaceIndex == diagnosticNs);
TestContext.Out
.WriteLine("Found {0} references on server.", referenceDescriptions.Count);
if (outputResult)
{
string directory = Path.Combine(TestContext.CurrentContext.WorkDirectory, "TestResults", "BrowseResults");
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, $"browse-{Guid.NewGuid():N}.txt");
using (var stream = new FileStream(
path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, useAsync: true))
using (var writer = new StreamWriter(stream))
{
foreach (ReferenceDescription reference in referenceDescriptions)
{
await writer.WriteLineAsync(
$"NodeId {reference.NodeId} {reference.NodeClass} {reference.BrowseName}")
.ConfigureAwait(false);
}
await writer.FlushAsync().ConfigureAwait(false);
}
TestContext.AddTestAttachment(path, "Full address-space browse results");
}
return referenceDescriptions;
}
/// <summary>
/// Worker method to translate the browse path.
/// </summary>
public static async Task<ArrayOf<BrowsePathResult>> TranslateBrowsePathWorkerAsync(
IServerTestServices services,
ArrayOf<ReferenceDescription> referenceDescriptions,
RequestHeader requestHeader,
OperationLimits operationLimits)
{
// Browse template
NodeId startingNode = ObjectIds.RootFolder;
requestHeader.Timestamp = DateTime.UtcNow;
// TranslateBrowsePath
bool verifyMaxNodesPerBrowse = operationLimits
.MaxNodesPerTranslateBrowsePathsToNodeIds > 0;
ArrayOf<BrowsePath> browsePaths = referenceDescriptions.ConvertAll(r => new BrowsePath
{
RelativePath = new RelativePath(r.BrowseName),
StartingNode = startingNode
});
var allBrowsePaths = new List<BrowsePathResult>();
while (browsePaths.Count > 0)
{
if (verifyMaxNodesPerBrowse &&
browsePaths.Count > operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds)
{
verifyMaxNodesPerBrowse = false;
// Test if server responds with BadTooManyOperations
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
_ = await services.TranslateBrowsePathsToNodeIdsAsync(
requestHeader,
browsePaths).ConfigureAwait(false))!;
Assert.That(
sre!.StatusCode,
Is.EqualTo(StatusCodes.BadTooManyOperations));
}
ArrayOf<BrowsePath> browsePathSnippet =
operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds == 0 ||
browsePaths.Count <= operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds
? browsePaths // take all
: browsePaths[..(int)operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds];
TranslateBrowsePathsToNodeIdsResponse translateResponse = await services.TranslateBrowsePathsToNodeIdsAsync(
requestHeader,
browsePathSnippet).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(translateResponse.ResponseHeader, translateResponse.Results, browsePathSnippet);
ServerFixtureUtils.ValidateDiagnosticInfos(
translateResponse.DiagnosticInfos,
browsePathSnippet,
translateResponse.ResponseHeader.StringTable,
services.Logger);
allBrowsePaths.AddRange(translateResponse.Results);
foreach (BrowsePathResult result in translateResponse.Results)
{
if (!result.Targets.IsEmpty)
{
TestContext.Out.WriteLine("BrowsePath {0}",
result.Targets[0].TargetId.ToString());
}
}
browsePaths =
operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds == 0 ||
browsePaths.Count <= operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds ?
default : // done
browsePaths[(int)operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds..];
}
return allBrowsePaths;
}
/// <summary>
/// Worker method to test subscriptions of a server.
/// </summary>
public static async Task SubscriptionTestAsync(
IServerTestServices services,
RequestHeader requestHeader)
{
// start time
requestHeader.Timestamp = DateTime.UtcNow;
// create subscription
const double publishingInterval = 1000.0;
const uint lifetimeCount = 60;
const uint maxKeepAliveCount = 2;
const uint maxNotificationPerPublish = 0;
const byte priority = 128;
bool enabled = false;
const uint queueSize = 5;
CreateSubscriptionResponse createSubscriptionResponse = await services.CreateSubscriptionAsync(
requestHeader,
publishingInterval,
lifetimeCount,
maxKeepAliveCount,
maxNotificationPerPublish,
enabled,
priority).ConfigureAwait(false);
Assert.That(createSubscriptionResponse.RevisedPublishingInterval, Is.EqualTo(publishingInterval));
Assert.That(createSubscriptionResponse.RevisedLifetimeCount, Is.EqualTo(lifetimeCount));
Assert.That(createSubscriptionResponse.RevisedMaxKeepAliveCount, Is.EqualTo(maxKeepAliveCount));
ServerFixtureUtils.ValidateResponse(createSubscriptionResponse.ResponseHeader);
uint id = createSubscriptionResponse.SubscriptionId;
ArrayOf<MonitoredItemCreateRequest> itemsToCreate = default;
// check badnothingtodo
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
await services.CreateMonitoredItemsAsync(
requestHeader,
id,
TimestampsToReturn.Neither,
itemsToCreate).ConfigureAwait(false))!;
Assert.That(sre!.StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo));
// add item
uint handleCounter = 1;
itemsToCreate =
[
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
AttributeId = Attributes.Value,
NodeId = VariableIds.Server_ServerStatus_CurrentTime
},
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = ++handleCounter,
SamplingInterval = -1,
Filter = default,
DiscardOldest = true,
QueueSize = queueSize
}
},
//add event item
CreateEventMonitoredItem(queueSize, ref handleCounter)
];
CreateMonitoredItemsResponse createMonitoredItemsResponse = await services.CreateMonitoredItemsAsync(
requestHeader,
id,
TimestampsToReturn.Neither,
itemsToCreate).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(createMonitoredItemsResponse.ResponseHeader, createMonitoredItemsResponse.Results, itemsToCreate);
ServerFixtureUtils.ValidateDiagnosticInfos(
createMonitoredItemsResponse.DiagnosticInfos,
itemsToCreate,
createMonitoredItemsResponse.ResponseHeader.StringTable,
services.Logger);
// modify subscription
ModifySubscriptionResponse modifySubscriptionResponse = await services.ModifySubscriptionAsync(
requestHeader,
id,
publishingInterval,
lifetimeCount,
maxKeepAliveCount,
maxNotificationPerPublish,
priority).ConfigureAwait(false);
Assert.That(modifySubscriptionResponse.RevisedPublishingInterval, Is.EqualTo(publishingInterval));
Assert.That(modifySubscriptionResponse.RevisedLifetimeCount, Is.EqualTo(lifetimeCount));
Assert.That(modifySubscriptionResponse.RevisedMaxKeepAliveCount, Is.EqualTo(maxKeepAliveCount));
ServerFixtureUtils.ValidateResponse(modifySubscriptionResponse.ResponseHeader);
// modify monitored item, just timestamps to return
ArrayOf<MonitoredItemModifyRequest> itemsToModify = createMonitoredItemsResponse.Results
.ConvertAll(itemCreated => new MonitoredItemModifyRequest
{
MonitoredItemId = itemCreated.MonitoredItemId
});
ModifyMonitoredItemsResponse modifyMonitoredItemsResponse = await services.ModifyMonitoredItemsAsync(
requestHeader,
id,
TimestampsToReturn.Both,
itemsToModify).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(modifyMonitoredItemsResponse.ResponseHeader, modifyMonitoredItemsResponse.Results, itemsToModify);
ServerFixtureUtils.ValidateDiagnosticInfos(
modifyMonitoredItemsResponse.DiagnosticInfos,
itemsToModify,
modifyMonitoredItemsResponse.ResponseHeader.StringTable,
services.Logger);
// publish request
ArrayOf<SubscriptionAcknowledgement> acknowledgements = default;
PublishResponse publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader, publishResponse.Results, acknowledgements);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
services.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(id));
Assert.That(publishResponse.AvailableSequenceNumbers.Count, Is.Zero);
// enable publishing
enabled = true;
ArrayOf<uint> subscriptions = [id];
SetPublishingModeResponse setPublishingModeResponse = await services.SetPublishingModeAsync(
requestHeader,
enabled,
subscriptions).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(setPublishingModeResponse.ResponseHeader, setPublishingModeResponse.Results, subscriptions);
ServerFixtureUtils.ValidateDiagnosticInfos(
setPublishingModeResponse.DiagnosticInfos,
subscriptions,
setPublishingModeResponse.ResponseHeader.StringTable,
services.Logger);
// wait some time to fill queue
int loopCounter = (int)queueSize;
Thread.Sleep(loopCounter * 1000);
acknowledgements = [];
do
{
// get publish responses
publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader, publishResponse.Results, acknowledgements);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
services.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(id));
if (publishResponse.NotificationMessage.NotificationData.Count == 0)
{
TestContext.Out.WriteLine("No notifications received in publish");
}
else
{
DataChangeNotification dataChangeNotification = (publishResponse.NotificationMessage.NotificationData[0]
.TryGetValue(out DataChangeNotification? d) ? d : default)!;
EventNotificationList eventNotification = (publishResponse.NotificationMessage.NotificationData[0]
.TryGetValue(out EventNotificationList? e) ? e : default)!;
TestContext.Out.WriteLine(
"Notification: {0} {1}",
publishResponse.NotificationMessage.SequenceNumber,
publishResponse.NotificationMessage.PublishTime);
}
acknowledgements =
[
new SubscriptionAcknowledgement
{
SubscriptionId = id,
SequenceNumber = publishResponse.NotificationMessage.SequenceNumber
}
];
} while (acknowledgements.Count > 0 && --loopCounter > 0);
// republish
RepublishResponse republishResponse = await services.RepublishAsync(
requestHeader,
publishResponse.SubscriptionId,
publishResponse.NotificationMessage.SequenceNumber).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(republishResponse.ResponseHeader);
// disable publishing
enabled = false;
setPublishingModeResponse = await services.SetPublishingModeAsync(
requestHeader,
enabled,
subscriptions).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(setPublishingModeResponse.ResponseHeader, setPublishingModeResponse.Results, subscriptions);
ServerFixtureUtils.ValidateDiagnosticInfos(
setPublishingModeResponse.DiagnosticInfos,
subscriptions,
setPublishingModeResponse.ResponseHeader.StringTable,
services.Logger);
// disable monitoring
ArrayOf<uint> monitoredItemIds =
createMonitoredItemsResponse.Results.ConvertAll(r => r.MonitoredItemId);
SetMonitoringModeResponse setMonitoringModeResponse = await services.SetMonitoringModeAsync(
requestHeader,
id,
MonitoringMode.Disabled,
monitoredItemIds).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(setMonitoringModeResponse.ResponseHeader, setMonitoringModeResponse.Results, monitoredItemIds);
ServerFixtureUtils.ValidateDiagnosticInfos(
setMonitoringModeResponse.DiagnosticInfos,
monitoredItemIds,
setMonitoringModeResponse.ResponseHeader.StringTable,
services.Logger);
// delete subscription
DeleteSubscriptionsResponse deleteSubscriptionsResponse = await services.DeleteSubscriptionsAsync(
requestHeader,
subscriptions).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(deleteSubscriptionsResponse.ResponseHeader, deleteSubscriptionsResponse.Results, subscriptions);
ServerFixtureUtils.ValidateDiagnosticInfos(
deleteSubscriptionsResponse.DiagnosticInfos,
subscriptions,
deleteSubscriptionsResponse.ResponseHeader.StringTable,
services.Logger);
}
/// <summary>
/// Worker method to test TransferSubscriptions of a server.
/// </summary>
public static async Task<ArrayOf<uint>> CreateSubscriptionForTransferAsync(
IServerTestServices services,
RequestHeader requestHeader,
NodeId[] testNodes,
uint queueSize = DefaultMonitoredItemsQueueSize,
int samplingInterval = DefaultMonitoredItemsSamplingInterval)
{
// start time
requestHeader.Timestamp = DateTime.UtcNow;
uint subscriptionId = await CreateSubscriptionAsync(services, requestHeader).ConfigureAwait(false);
uint clientHandle = 1;
foreach (NodeId testNode in testNodes)
{
await CreateMonitoredItemAsync(
services,
requestHeader,
subscriptionId,
testNode,
clientHandle++,
queueSize,
samplingInterval).ConfigureAwait(false);
}
ArrayOf<uint> subscriptionIds = [subscriptionId];
// enable publishing
SetPublishingModeResponse setPublishingModeResponse = await services.SetPublishingModeAsync(
requestHeader,
true,
subscriptionIds).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(setPublishingModeResponse.ResponseHeader, setPublishingModeResponse.Results, subscriptionIds);
ServerFixtureUtils.ValidateDiagnosticInfos(
setPublishingModeResponse.DiagnosticInfos,
subscriptionIds,
setPublishingModeResponse.ResponseHeader.StringTable,
services.Logger);
// wait some time to settle
Thread.Sleep(1000);
// publish request (use invalid sequence number for status)
ArrayOf<SubscriptionAcknowledgement> acknowledgements =
[
new SubscriptionAcknowledgement
{
SubscriptionId = subscriptionId,
SequenceNumber = 123
}
];
PublishResponse publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader, publishResponse.Results, acknowledgements);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
services.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionId));
// static node, do not acknowledge
Assert.That(publishResponse.AvailableSequenceNumbers.Count, Is.EqualTo(1));
return subscriptionIds;
}
/// <summary>
/// Worker method to test Transfer of subscriptions to new session.
/// </summary>
public static async Task TransferSubscriptionTestAsync(
IServerTestServices services,
RequestHeader requestHeader,
ArrayOf<uint> subscriptionIds,
bool sendInitialData,
bool expectAccessDenied)
{
Assert.That(subscriptionIds.Count, Is.EqualTo(1));
requestHeader.Timestamp = DateTime.UtcNow;
TransferSubscriptionsResponse transferResponse = await services.TransferSubscriptionsAsync(
requestHeader,
subscriptionIds,
sendInitialData).ConfigureAwait(false);
Assert.That(transferResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
Assert.That(transferResponse.Results.Count, Is.EqualTo(subscriptionIds.Count));
ServerFixtureUtils.ValidateResponse(transferResponse.ResponseHeader, transferResponse.Results, subscriptionIds);
ServerFixtureUtils.ValidateDiagnosticInfos(
transferResponse.DiagnosticInfos,
subscriptionIds,
transferResponse.ResponseHeader.StringTable,
services.Logger);
foreach (TransferResult transferResult in transferResponse.Results)
{
TestContext.Out.WriteLine("TransferResult: {0}", transferResult.StatusCode);
if (expectAccessDenied)
{
Assert.That(
transferResult.StatusCode,
Is.EqualTo(StatusCodes.BadUserAccessDenied));
}
else
{
Assert.That(StatusCode.IsGood(transferResult.StatusCode), Is.True);
Assert.That(transferResult.AvailableSequenceNumbers.Count, Is.EqualTo(1));
}
}
if (expectAccessDenied)
{
return;
}
requestHeader.Timestamp = DateTime.UtcNow;
ArrayOf<SubscriptionAcknowledgement> acknowledgements = default;
PublishResponse publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
Assert.That(publishResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
services.Logger);
Assert.That(publishResponse.SubscriptionId, Is.EqualTo(subscriptionIds[0]));
Assert.That(publishResponse.NotificationMessage.NotificationData.Count, Is.EqualTo(sendInitialData ? 1 : 0));
if (sendInitialData)
{
ExtensionObject items = publishResponse.NotificationMessage.NotificationData[0];
Assert.That(items.TryGetValue(out DataChangeNotification? dataChangeNotification), Is.True);
ArrayOf<MonitoredItemNotification> monitoredItemsCollection = dataChangeNotification!.MonitoredItems;
Assert.That(monitoredItemsCollection.IsEmpty, Is.False);
}
//Assert.AreEqual(0, availableSequenceNumbers.Count);
requestHeader.Timestamp = DateTime.UtcNow;
DeleteSubscriptionsResponse deleteResponse = await services.DeleteSubscriptionsAsync(requestHeader, subscriptionIds).ConfigureAwait(false);
Assert.That(deleteResponse.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good));
}
/// <summary>
/// Worker method to verify the SubscriptionTransferred message of a server.
/// </summary>
public static async Task VerifySubscriptionTransferredAsync(
IServerTestServices services,
RequestHeader requestHeader,
ArrayOf<uint> subscriptionIds,
bool deleteSubscriptions)
{
// start time
requestHeader.Timestamp = DateTime.UtcNow;
// wait some time to settle
Thread.Sleep(100);
// publish request
ArrayOf<SubscriptionAcknowledgement> acknowledgements = default;
PublishResponse publishResponse = await services.PublishAsync(
requestHeader,
acknowledgements).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(publishResponse.ResponseHeader);
ServerFixtureUtils.ValidateDiagnosticInfos(
publishResponse.DiagnosticInfos,
acknowledgements,
publishResponse.ResponseHeader.StringTable,
services.Logger);
Assert.That(publishResponse.MoreNotifications, Is.False);
Assert.That(subscriptionIds.ToArray(), Does.Contain(publishResponse.SubscriptionId));
Assert.That(publishResponse.NotificationMessage.NotificationData.Count, Is.EqualTo(1));
string statusMessage = publishResponse.NotificationMessage.NotificationData[0].ToString();
// Should contain GoodSubscriptionTransferred status code
Assert.That(statusMessage, Does.StartWith("StatusChangeNotification"));
Assert.That(statusMessage, Does.Contain("Status=GoodSubscriptionTransferred"));
// static node, do not acknowledge
Assert.That(publishResponse.AvailableSequenceNumbers.Count, Is.Zero);
if (deleteSubscriptions)
{
DeleteSubscriptionsResponse deleteResponse = await services.DeleteSubscriptionsAsync(
requestHeader,
subscriptionIds).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(deleteResponse.ResponseHeader, deleteResponse.Results, subscriptionIds);
ServerFixtureUtils.ValidateDiagnosticInfos(
deleteResponse.DiagnosticInfos,
subscriptionIds,
deleteResponse.ResponseHeader.StringTable,
services.Logger);
}
}
private static async Task<uint> CreateSubscriptionAsync(
IServerTestServices services,
RequestHeader requestHeader)
{
// start time
requestHeader.Timestamp = DateTime.UtcNow;
// create subscription
const double publishingInterval = 1000.0;
const uint lifetimeCount = 60;
const uint maxKeepAliveCount = 2;
const uint maxNotificationPerPublish = 0;
const byte priority = 128;
const bool enabled = false;
CreateSubscriptionResponse response = await services.CreateSubscriptionAsync(
requestHeader,
publishingInterval,
lifetimeCount,
maxKeepAliveCount,
maxNotificationPerPublish,
enabled,
priority).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(response.ResponseHeader);
return response.SubscriptionId;
}
private static async Task CreateMonitoredItemAsync(
IServerTestServices services,
RequestHeader requestHeader,
uint subscriptionId,
NodeId nodeId,
uint clientHandle,
uint queueSize,
int samplingInterval)
{
ArrayOf<MonitoredItemCreateRequest> itemsToCreate =
[
// add item
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
AttributeId = Attributes.Value,
NodeId = nodeId
},
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = clientHandle,
SamplingInterval = samplingInterval,
Filter = default,
DiscardOldest = true,
QueueSize = queueSize
}
}
];
CreateMonitoredItemsResponse response = await services.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
TimestampsToReturn.Neither,
itemsToCreate).ConfigureAwait(false);
ServerFixtureUtils.ValidateResponse(response.ResponseHeader, response.Results, itemsToCreate);
ServerFixtureUtils.ValidateDiagnosticInfos(
response.DiagnosticInfos,
itemsToCreate,
response.ResponseHeader.StringTable,
services.Logger);
}
private static MonitoredItemCreateRequest CreateEventMonitoredItem(
uint queueSize,
ref uint handleCounter)
{
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 MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
AttributeId = Attributes.EventNotifier,
NodeId = ObjectIds.Server
},
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = ++handleCounter,
SamplingInterval = -1,
Filter = new ExtensionObject(
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
}
};
}
}
}