Files
UA-.NETStandard/tests/Opc.Ua.InformationModel.Tests/AttributeReadComplexTests.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

567 lines
24 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ========================================================================
* 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
using ISession = Opc.Ua.Client.ISession;
namespace Opc.Ua.InformationModel.Tests
{
/// <summary>
/// compliance tests for Attribute Service Set – Reading complex attributes,
/// structured types, data encodings, and optional/extended attributes.
/// </summary>
[TestFixture]
[Category("Conformance")]
[Category("AttributeReadComplex")]
public class AttributeReadComplexTests : TestFixture
{
[Description("Read Server_ServerStatus (structured type) and verify the value is returned as an ExtensionObject with Good status.")]
[Test]
public async Task ReadExtensionObjectValueAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"Read of Server_ServerStatus should return Good.");
Variant variant = response.Results[0].WrappedValue;
// Server_ServerStatus is per spec a ServerStatusDataType structure
// wire-encoded as ExtensionObject. Both shapes are accepted: either
// the wire ExtensionObject or a server that already decoded it.
bool isExtensionObject = variant.TryGetValue(out ExtensionObject _);
bool isDecoded = variant.TryGetStructure<ServerStatusDataType>(out ServerStatusDataType? _);
Assert.That(isExtensionObject || isDecoded, Is.True,
"Server_ServerStatus value should not be null.");
Assert.That(isExtensionObject || isDecoded, Is.True,
"Value should be an ExtensionObject or ServerStatusDataType.");
}
[Description("Read Server_ServerStatus and verify the decoded structure contains accessible nested fields such as StartTime and State.")]
[Test]
public async Task ReadNestedStructureValueAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True);
Variant variant = response.Results[0].WrappedValue;
ServerStatusDataType? serverStatus = null;
// Per spec ServerStatus is a ServerStatusDataType wire-encoded as
// ExtensionObject; accept both the wire form and an already-decoded
// structure.
if (variant.TryGetStructure<ServerStatusDataType>(out ServerStatusDataType? decoded))
{
serverStatus = decoded;
}
else if (variant.TryGetValue(out ExtensionObject extensionObject) &&
extensionObject.TryGetValue(out ServerStatusDataType? fromWire))
{
serverStatus = fromWire;
}
Assert.That(serverStatus, Is.Not.Null,
"Should be able to decode ServerStatusDataType.");
Assert.That(
serverStatus.StartTime, Is.Not.EqualTo(DateTime.MinValue),
"StartTime should be set.");
Assert.That(
#if NET5_0_OR_GREATER
Enum.IsDefined(serverStatus.State),
#else
Enum.IsDefined(typeof(ServerState), serverStatus.State),
#endif
Is.True,
"State should be a valid ServerState enum value.");
}
[Description("Read the SessionDiagnosticsArray variable which contains an array of ExtensionObjects. Verify the result is Good and the value is an array or empty collection.")]
[Test]
public async Task ReadArrayOfExtensionObjectsAsync()
{
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
try
{
ISession session = admin ?? Session;
ReadResponse response = await session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds
.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
StatusCode statusCode = response.Results[0].StatusCode;
if (!StatusCode.IsGood(statusCode))
{
Assert.Ignore(
$"SessionDiagnosticsArray not readable: {statusCode}");
}
// SessionDiagnosticsArray is genuinely polymorphic: server can
// return an array of ExtensionObject, an empty array, or a
// null Variant. A null/empty value is consistent with
// "no live sessions"; reject only the silent default-Variant
// shape.
Assert.That(
response.Results[0].WrappedValue.TypeInfo,
Is.Not.Null,
"Value should not be null when status is Good.");
}
finally
{
if (admin != null)
{
await admin.CloseAsync(5000, true).ConfigureAwait(false);
admin.Dispose();
}
}
}
[Description("Read Server_ServerStatus_State which is an enumeration value. Verify the result is a numeric value representing a valid ServerState.")]
[Test]
public async Task ReadEnumerationValueAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus_State,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"Read of Server_ServerStatus_State should return Good.");
int stateValue = response.Results[0].GetValue(0);
Assert.That(
Enum.IsDefined(typeof(ServerState), stateValue), Is.True,
$"State value {stateValue} should be a valid ServerState.");
}
[Description("Read a structured type node with DataEncoding set to \"Default Binary\". Expect Good status since binary encoding is the native OPC UA encoding.")]
[Test]
public async Task ReadWithDataEncodingDefaultBinaryAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value,
DataEncoding = new QualifiedName("Default Binary")
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingUnsupported,
Is.True,
"Default Binary encoding should return Good or unsupported.");
}
[Description("Read a structured type node with DataEncoding set to \"Default XML\". Expect Good or BadDataEncodingUnsupported since XML encoding is optional.")]
[Test]
public async Task ReadWithDataEncodingDefaultXmlAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value,
DataEncoding = new QualifiedName("Default XML")
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingUnsupported ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingInvalid,
Is.True,
"Default XML encoding should return Good or unsupported.");
}
[Description("Read with an invalid DataEncoding name. Expect BadDataEncodingInvalid or BadDataEncodingUnsupported.")]
[Test]
public async Task ReadWithInvalidDataEncodingAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value,
DataEncoding = new QualifiedName(
"NonExistentEncoding_Invalid")
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingInvalid ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingUnsupported,
Is.True,
"Invalid DataEncoding should return BadDataEncodingInvalid " +
"or BadDataEncodingUnsupported.");
}
[Description("Read all standard attributes of a Variable node in a single call. Verify each attribute returns Good status.")]
[Test]
public async Task ReadAllAttributesOfVariableNodeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
uint[] attributeIds =
[
Attributes.NodeId,
Attributes.NodeClass,
Attributes.BrowseName,
Attributes.DisplayName,
Attributes.Description,
Attributes.DataType,
Attributes.ValueRank,
Attributes.Value,
Attributes.AccessLevel,
Attributes.UserAccessLevel
];
var readValueIds = attributeIds
.Select(attrId => new ReadValueId
{
NodeId = nodeId,
AttributeId = attrId
}).ToArrayOf();
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
readValueIds,
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(attributeIds.Length));
for (int i = 0; i < response.Results.Count; i++)
{
Assert.That(
StatusCode.IsGood(response.Results[i].StatusCode), Is.True,
$"Variable attribute {attributeIds[i]} should return Good.");
}
}
[Description("Read all standard attributes of the Server object node. Verify NodeClass, BrowseName, DisplayName, Description, and EventNotifier return valid results.")]
[Test]
public async Task ReadAllAttributesOfObjectNodeAsync()
{
uint[] attributeIds =
[
Attributes.NodeClass,
Attributes.BrowseName,
Attributes.DisplayName,
Attributes.Description,
Attributes.EventNotifier
];
var readValueIds = attributeIds
.Select(attrId => new ReadValueId
{
NodeId = ObjectIds.Server,
AttributeId = attrId
}).ToArrayOf();
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
readValueIds,
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(attributeIds.Length));
for (int i = 0; i < response.Results.Count; i++)
{
Assert.That(
StatusCode.IsGood(response.Results[i].StatusCode), Is.True,
$"Object attribute {attributeIds[i]} should return Good.");
}
// Verify NodeClass is Object
int nodeClass = response.Results[0].GetValue(0);
Assert.That(nodeClass, Is.EqualTo((int)NodeClass.Object),
"Server node should have NodeClass = Object.");
}
[Description("Read the AccessLevelEx attribute (id=27) from a Variable node. This attribute may not be supported by all servers; if BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadAccessLevelExAttributeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.AccessLevelEx
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"AccessLevelEx attribute not supported by this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"AccessLevelEx should return Good when supported.");
}
[Description("Read the RolePermissions attribute (id=26) from a Variable node. This attribute may not be supported; if BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadRolePermissionsAttributeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.RolePermissions
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"RolePermissions attribute not supported by this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadNotReadable,
Is.True,
"RolePermissions should return Good or BadNotReadable.");
}
[Description("Read the UserRolePermissions attribute (id=25) from a Variable node. This attribute may not be supported; if BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadUserRolePermissionsAttributeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.UserRolePermissions
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"UserRolePermissions attribute not supported by " +
"this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadNotReadable,
Is.True,
"UserRolePermissions should return Good or BadNotReadable.");
}
[Description("Read the DataTypeDefinition attribute (id=23) on a DataType node. This provides the structure or enum definition of the type. If BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadDataTypeDefinitionAttributeAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = DataTypeIds.ServerStatusDataType,
AttributeId = Attributes.DataTypeDefinition
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"DataTypeDefinition attribute not supported by " +
"this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"DataTypeDefinition should return Good when supported.");
// DataTypeDefinition is an ExtensionObject wrapping a
// StructureDefinition or EnumDefinition; verify the wire form
// is non-null and that an inner IEncodeable can be unwrapped.
Assert.That(
response.Results[0].WrappedValue.TryGetValue(out ExtensionObject extObj),
Is.True,
"DataTypeDefinition value should be an ExtensionObject.");
Assert.That(
extObj.TryGetValue(out IEncodeable? encodeable),
Is.True,
"DataTypeDefinition body should decode to an IEncodeable.");
Assert.That(encodeable, Is.Not.Null,
"DataTypeDefinition value should not be null.");
}
[Description("Read the ArrayDimensions attribute on an array variable node. Verify the result is Good and the value is a valid array.")]
[Test]
public async Task ReadArrayDimensionsOnArrayNodeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.ArrayDimensions
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"ArrayDimensions on an array node should return Good.");
}
[Description("Read the DataType attribute of a Variable node and verify it returns a valid, non-null NodeId.")]
[Test]
public async Task ReadDataTypeOfVariableAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticDouble);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.DataType
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"DataType attribute should return Good.");
NodeId dataType = response.Results[0].GetValue<NodeId>(default);
Assert.That(dataType, Is.Not.Null,
"DataType should not be null.");
Assert.That(dataType, Is.Not.EqualTo(NodeId.Null),
"DataType should not be the Null NodeId.");
Assert.That(dataType, Is.EqualTo(DataTypeIds.Double),
"DataType for ScalarStaticDouble should be Double.");
}
}
}