mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-22 09:37:12 +08:00
9b034f8291
# 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>
1914 lines
74 KiB
C#
1914 lines
74 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.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Client;
|
|
using Opc.Ua.Client.TestFramework;
|
|
|
|
namespace Opc.Ua.InformationModel.Tests
|
|
{
|
|
/// <summary>
|
|
/// compliance tests for Base Information behavioral CUs:
|
|
/// OptionSet, Diagnostics, GetMonitoredItems, ResendData,
|
|
/// RequestServerStateChange, DeviceFailure, EventQueueOverflow,
|
|
/// ProgressEvents, SecurityRoleCapabilities, SelectionList,
|
|
/// and OrderedList.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Conformance")]
|
|
[Category("BaseInfoBehavioral")]
|
|
public class BaseInfoBehavioralTests : TestFixture
|
|
{
|
|
[Test]
|
|
public async Task OptionSet001ReadAccessLevelExOnServerStatusStateAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
VariableIds.Server_ServerStatus_State,
|
|
Attributes.AccessLevelEx).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("AccessLevelEx not supported.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet002ReadWriteMaskOnServerAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.WriteMask).ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"WriteMask should be readable on Server object.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet003ReadUserWriteMaskOnServerAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.UserWriteMask).ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"UserWriteMask should be readable on Server object.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet004ReadEventNotifierOnServerAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.EventNotifier).ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"EventNotifier should be readable on Server object.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet005BrowseServerCapabilitiesForAccessRestrictionsAsync()
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ObjectIds.Server_ServerCapabilities,
|
|
ReferenceTypeIds.HasProperty).ConfigureAwait(false);
|
|
Assert.That(refs, Is.Not.Empty,
|
|
"ServerCapabilities should have properties.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet006ReadAccessRestrictionsAttributeAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.AccessRestrictions, admin).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore("AccessRestrictions not available.");
|
|
}
|
|
// The Server node may legitimately have no AccessRestrictions
|
|
// configured — accept null but require a Good status.
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet007ReadRolePermissionsOnServerAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.RolePermissions, admin).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore("RolePermissions not available.");
|
|
}
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet008ReadUserRolePermissionsAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.UserRolePermissions, admin).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore("UserRolePermissions not available.");
|
|
}
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet009BrowseDataTypeDefinitionEnumerationAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
new NodeId(DataTypes.ServerState),
|
|
Attributes.DataTypeDefinition).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("DataTypeDefinition not available on enumeration.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet010ReadDataTypeDefinitionStructureAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
new NodeId(DataTypes.Argument),
|
|
Attributes.DataTypeDefinition).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("DataTypeDefinition not available on structure type.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet011ReadAccessLevelExOnWritableVariableAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
VariableIds.Server_ServerStatus_State,
|
|
Attributes.AccessLevelEx).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("AccessLevelEx not supported.");
|
|
}
|
|
uint val = dv.GetValue<uint>(0);
|
|
Assert.That(val, Is.GreaterThanOrEqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet012VerifyWriteMaskBitsAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.WriteMask).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
uint mask = dv.GetValue<uint>(0);
|
|
Assert.That(mask, Is.GreaterThanOrEqualTo((uint)0),
|
|
"WriteMask should be a valid bitmask.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet013VerifyUserWriteMaskBitsAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
ObjectIds.Server,
|
|
Attributes.UserWriteMask).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
uint mask = dv.GetValue<uint>(0);
|
|
Assert.That(mask, Is.GreaterThanOrEqualTo((uint)0),
|
|
"UserWriteMask should be a valid bitmask.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet014VerifyAccessLevelOnVariableAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
VariableIds.Server_ServerStatus_CurrentTime,
|
|
Attributes.AccessLevel).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
byte level = dv.GetValue<byte>(0);
|
|
Assert.That(level & AccessLevels.CurrentRead,
|
|
Is.Not.Zero,
|
|
"CurrentTime should be readable.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OptionSet015VerifyUserAccessLevelOnVariableAsync()
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
VariableIds.Server_ServerStatus_CurrentTime,
|
|
Attributes.UserAccessLevel).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
byte level = dv.GetValue<byte>(0);
|
|
Assert.That(level & AccessLevels.CurrentRead,
|
|
Is.Not.Zero,
|
|
"UserAccessLevel should include CurrentRead.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics000ReadEnabledFlagAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
VariableIds.Server_ServerDiagnostics_EnabledFlag)
|
|
.ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("EnabledFlag not readable.");
|
|
}
|
|
Assert.That(dv.WrappedValue.TryGetValue(out bool _), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics001ReadServerDiagnosticsSummaryAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_ServerDiagnosticsSummary)
|
|
.ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode) ||
|
|
dv.StatusCode.Code == StatusCodes.BadNotReadable ||
|
|
dv.StatusCode.Code == StatusCodes.BadUserAccessDenied,
|
|
Is.True,
|
|
"ServerDiagnosticsSummary should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics002ReadServerViewCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2276)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("ServerViewCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics003ReadCurrentSessionCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2277)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("CurrentSessionCount not accessible.");
|
|
}
|
|
uint count = dv.GetValue<uint>(0);
|
|
Assert.That(count, Is.GreaterThan((uint)0),
|
|
"At least one session should be active.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics004ReadCumulatedSessionCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2278)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("CumulatedSessionCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics005ReadSecurityRejectedSessionCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2279)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("SecurityRejectedSessionCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics006ReadSessionAbortCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2282)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("SessionAbortCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics007ReadPublishingIntervalCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2284)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("PublishingIntervalCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics008ReadCurrentSubscriptionCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2285)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("CurrentSubscriptionCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics009ReadCumulatedSubscriptionCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2286)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("CumulatedSubscriptionCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics010ReadSecurityRejectedRequestsCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2287)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("SecurityRejectedRequestsCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics011ReadSamplingIntervalDiagnosticsArrayAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
DataValue dv = await ReadAttributeAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_SamplingIntervalDiagnosticsArray,
|
|
Attributes.Value, admin)
|
|
.ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore(
|
|
"SamplingIntervalDiagnosticsArray not accessible: " +
|
|
$"{dv.StatusCode}");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics012ReadSubscriptionDiagnosticsArrayAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_SubscriptionDiagnosticsArray)
|
|
.ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode) ||
|
|
dv.StatusCode.Code == StatusCodes.BadNotReadable ||
|
|
dv.StatusCode.Code == StatusCodes.BadUserAccessDenied,
|
|
Is.True,
|
|
"SubscriptionDiagnosticsArray should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics013ReadSessionDiagnosticsArrayAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray)
|
|
.ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode) ||
|
|
dv.StatusCode.Code == StatusCodes.BadNotReadable ||
|
|
dv.StatusCode.Code == StatusCodes.BadUserAccessDenied,
|
|
Is.True,
|
|
"SessionDiagnosticsArray should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics014ReadSessionSecurityDiagnosticsArrayAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionSecurityDiagnosticsArray)
|
|
.ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(dv.StatusCode) ||
|
|
dv.StatusCode.Code == StatusCodes.BadNotReadable ||
|
|
dv.StatusCode.Code == StatusCodes.BadUserAccessDenied,
|
|
Is.True,
|
|
"SessionSecurityDiagnosticsArray should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics016ReadRejectedSessionCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(3705)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("RejectedSessionCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics017ReadRejectedRequestsCountAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
new NodeId(2288)).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("RejectedRequestsCount not accessible.");
|
|
}
|
|
Assert.That(dv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics0181BrowseSessionDiagnosticsAsync()
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ObjectIds
|
|
.Server_ServerDiagnostics_SessionsDiagnosticsSummary)
|
|
.ConfigureAwait(false);
|
|
Assert.That(refs, Is.Not.Empty,
|
|
"SessionsDiagnosticsSummary should have children.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics0182BrowseSessionSecurityDiagnosticsAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ObjectIds
|
|
.Server_ServerDiagnostics_SessionsDiagnosticsSummary,
|
|
session: admin)
|
|
.ConfigureAwait(false);
|
|
bool hasSecArray = refs.Any(
|
|
r => r.BrowseName.Name == "SessionSecurityDiagnosticsArray");
|
|
if (!hasSecArray)
|
|
{
|
|
Assert.Ignore("Diagnostics arrays not available in default ReferenceServer configuration.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics0183BrowseSubscriptionDiagnosticsAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ObjectIds.Server_ServerDiagnostics,
|
|
session: admin)
|
|
.ConfigureAwait(false);
|
|
bool hasSubArray = refs.Any(
|
|
r => r.BrowseName.Name == "SubscriptionDiagnosticsArray");
|
|
if (!hasSubArray)
|
|
{
|
|
Assert.Ignore("Diagnostics arrays not available in default ReferenceServer configuration.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics019ReadServerStatusAfterDiagnosticsAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
VariableIds.Server_ServerStatus).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"ServerStatus should be readable.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics023EnabledFlagIsBoolAsync()
|
|
{
|
|
DataValue dv = await ReadValueAsync(
|
|
VariableIds.Server_ServerDiagnostics_EnabledFlag)
|
|
.ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("EnabledFlag not readable.");
|
|
}
|
|
Assert.That(dv.WrappedValue.TryGetValue(out bool _), Is.True,
|
|
"EnabledFlag should be a Boolean.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Diagnostics024SummaryAggregatesSessionDiagnosticsAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
ISession session = admin ?? Session;
|
|
DataValue summaryDv = await ReadAttributeAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_ServerDiagnosticsSummary,
|
|
Attributes.Value, session)
|
|
.ConfigureAwait(false);
|
|
DataValue sessionArrayDv = await ReadAttributeAsync(
|
|
VariableIds
|
|
.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray,
|
|
Attributes.Value, session)
|
|
.ConfigureAwait(false);
|
|
|
|
if (StatusCode.IsBad(summaryDv.StatusCode) ||
|
|
StatusCode.IsBad(sessionArrayDv.StatusCode))
|
|
{
|
|
Assert.Ignore(
|
|
"Diagnostics summary or session array not readable.");
|
|
}
|
|
|
|
Assert.That(summaryDv.WrappedValue.IsNull, Is.False);
|
|
Assert.That(sessionArrayDv.WrappedValue.IsNull, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetMonitoredItems001BrowseMethodAsync()
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
MethodIds.Server_GetMonitoredItems,
|
|
ReferenceTypeIds.HasProperty).ConfigureAwait(false);
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "InputArguments"),
|
|
Is.True, "InputArguments should exist.");
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "OutputArguments"),
|
|
Is.True, "OutputArguments should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetMonitoredItems002CallWithValidSubscriptionAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync()
|
|
.ConfigureAwait(false);
|
|
try
|
|
{
|
|
await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_GetMonitoredItems,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"GetMonitoredItems should succeed.");
|
|
Assert.That(result.OutputArguments.Count,
|
|
Is.EqualTo(2),
|
|
"Should return ServerHandles and ClientHandles.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetMonitoredItems003EmptySubscriptionAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync()
|
|
.ConfigureAwait(false);
|
|
try
|
|
{
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_GetMonitoredItems,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"GetMonitoredItems should succeed on empty sub.");
|
|
Assert.That(result.OutputArguments.Count,
|
|
Is.EqualTo(2));
|
|
|
|
uint[] serverHandles = ExtractUIntArray(
|
|
result.OutputArguments[0]);
|
|
uint[] clientHandles = ExtractUIntArray(
|
|
result.OutputArguments[1]);
|
|
Assert.That(serverHandles, Is.Not.Null);
|
|
Assert.That(clientHandles, Is.Not.Null);
|
|
Assert.That(serverHandles, Has.Length.Zero);
|
|
Assert.That(clientHandles, Has.Length.Zero);
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetMonitoredItems004MultipleSubscriptionsAsync()
|
|
{
|
|
uint subId1 = await CreateTestSubscriptionAsync()
|
|
.ConfigureAwait(false);
|
|
uint subId2 = await CreateTestSubscriptionAsync()
|
|
.ConfigureAwait(false);
|
|
try
|
|
{
|
|
await CreateMonitoredItemAsync(
|
|
subId1, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
|
|
CallMethodResult result1 = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_GetMonitoredItems,
|
|
new Variant(subId1)).ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(result1.StatusCode), Is.True);
|
|
|
|
CallMethodResult result2 = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_GetMonitoredItems,
|
|
new Variant(subId2)).ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(result2.StatusCode), Is.True);
|
|
|
|
uint[] handles1 = ExtractUIntArray(
|
|
result1.OutputArguments[0]);
|
|
uint[] handles2 = ExtractUIntArray(
|
|
result2.OutputArguments[0]);
|
|
Assert.That(handles1, Is.Not.Null);
|
|
Assert.That(handles2, Is.Not.Null);
|
|
Assert.That(handles1, Is.Not.Empty);
|
|
Assert.That(handles2, Has.Length.Zero);
|
|
}
|
|
finally
|
|
{
|
|
await Session.DeleteSubscriptionsAsync(
|
|
null,
|
|
new uint[] { subId1, subId2 }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetMonitoredItemsErr001InvalidSubscriptionIdAsync()
|
|
{
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_GetMonitoredItems,
|
|
new Variant((uint)999999)).ConfigureAwait(false);
|
|
|
|
Assert.That(result.StatusCode.Code,
|
|
Is.EqualTo(StatusCodes.BadSubscriptionIdInvalid),
|
|
"Invalid subscription should return BadSubscriptionIdInvalid.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetMonitoredItemsErr003CrossSessionReturnsBadStatusAsync()
|
|
{
|
|
uint subscriptionId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
ISession otherSession = await ClientFixture
|
|
.ConnectAsync(ServerUrl, SecurityPolicies.None)
|
|
.ConfigureAwait(false);
|
|
try
|
|
{
|
|
var request = new CallMethodRequest
|
|
{
|
|
ObjectId = ObjectIds.Server,
|
|
MethodId = MethodIds.Server_GetMonitoredItems,
|
|
InputArguments = new Variant[] { new(subscriptionId) }.ToArrayOf()
|
|
};
|
|
|
|
CallResponse response = await otherSession.CallAsync(
|
|
null,
|
|
new CallMethodRequest[] { request }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(response.Results.Count, Is.EqualTo(1));
|
|
Assert.That(StatusCode.IsBad(response.Results[0].StatusCode), Is.True,
|
|
"Calling GetMonitoredItems from a different session must fail; " +
|
|
$"expected a Bad status, got {response.Results[0].StatusCode}.");
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
await otherSession.CloseAsync(5000, true).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
otherSession.Dispose();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subscriptionId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData000BrowseMethodAsync()
|
|
{
|
|
await AssertNodeExistsAsync(
|
|
MethodIds.Server_ResendData, "ResendData")
|
|
.ConfigureAwait(false);
|
|
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
MethodIds.Server_ResendData,
|
|
ReferenceTypeIds.HasProperty).ConfigureAwait(false);
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "InputArguments"),
|
|
Is.True, "ResendData should have InputArguments.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData001CallWithReportingItemsAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync()
|
|
.ConfigureAwait(false);
|
|
try
|
|
{
|
|
await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed on valid subscription.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData002CallWithSamplingItemsAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
CreateMonitoredItemsResponse cmi = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(cmi.Results[0].StatusCode), Is.True);
|
|
|
|
// Switch to Sampling mode (collected but not reported)
|
|
SetMonitoringModeResponse smm = await Session.SetMonitoringModeAsync(
|
|
null, subId, MonitoringMode.Sampling,
|
|
new uint[] { cmi.Results[0].MonitoredItemId }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(smm.Results[0]), Is.True);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed even on sampling-mode items.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData003CallWithDisabledItemsAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
CreateMonitoredItemsResponse cmi = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(cmi.Results[0].StatusCode), Is.True);
|
|
|
|
SetMonitoringModeResponse smm = await Session.SetMonitoringModeAsync(
|
|
null, subId, MonitoringMode.Disabled,
|
|
new uint[] { cmi.Results[0].MonitoredItemId }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(smm.Results[0]), Is.True);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed even when all items are disabled.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData004CallWithMultipleModesAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
CreateMonitoredItemsResponse cmi1 = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
CreateMonitoredItemsResponse cmi2 = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_State)
|
|
.ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(cmi1.Results[0].StatusCode), Is.True);
|
|
Assert.That(StatusCode.IsGood(cmi2.Results[0].StatusCode), Is.True);
|
|
|
|
await Session.SetMonitoringModeAsync(
|
|
null, subId, MonitoringMode.Sampling,
|
|
new uint[] { cmi2.Results[0].MonitoredItemId }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed with mixed monitoring modes.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData005DoesNotErrorOnEmptyAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
// No monitored items added — call ResendData on empty sub.
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData on a subscription with no items should succeed.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData006CallWithLargeQueueAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
CreateMonitoredItemsResponse cmi = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(cmi.Results[0].StatusCode), Is.True);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed regardless of queue size.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData007CallWithDataChangeFilterAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
CreateMonitoredItemsResponse cmi = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(cmi.Results[0].StatusCode), Is.True);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed even if items have data change filters.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData008CallAfterModifyMonitoredItemsAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
CreateMonitoredItemsResponse cmi = await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(cmi.Results[0].StatusCode), Is.True);
|
|
|
|
// Modify the monitored item — should not lose the resend behavior.
|
|
var modifyRequest = new MonitoredItemModifyRequest
|
|
{
|
|
MonitoredItemId = cmi.Results[0].MonitoredItemId,
|
|
RequestedParameters = new MonitoringParameters
|
|
{
|
|
ClientHandle = 1,
|
|
SamplingInterval = 500,
|
|
QueueSize = 20,
|
|
DiscardOldest = true
|
|
}
|
|
};
|
|
await Session.ModifyMonitoredItemsAsync(
|
|
null, subId, TimestampsToReturn.Both,
|
|
new MonitoredItemModifyRequest[] { modifyRequest }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
"ResendData should succeed after modifying monitored items.");
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData009CallOnMultipleSubscriptionsAsync()
|
|
{
|
|
uint subId1 = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
uint subId2 = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
await CreateMonitoredItemAsync(
|
|
subId1, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
await CreateMonitoredItemAsync(
|
|
subId2, VariableIds.Server_ServerStatus_State)
|
|
.ConfigureAwait(false);
|
|
|
|
CallMethodResult r1 = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId1)).ConfigureAwait(false);
|
|
CallMethodResult r2 = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId2)).ConfigureAwait(false);
|
|
|
|
Assert.That(StatusCode.IsGood(r1.StatusCode), Is.True);
|
|
Assert.That(StatusCode.IsGood(r2.StatusCode), Is.True);
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId1).ConfigureAwait(false);
|
|
await DeleteSubscriptionAsync(subId2).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendData010CalledRepeatedlyIsIdempotentAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
|
|
// Fire multiple ResendData calls back-to-back; all should succeed.
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant(subId)).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True,
|
|
$"ResendData iteration {i} should succeed.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendDataErr001NonexistentSubscriptionAsync()
|
|
{
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant((uint)999999)).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
StatusCode.IsBad(result.StatusCode), Is.True,
|
|
"ResendData with invalid subscription should fail.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendDataErr002CrossSessionAsync()
|
|
{
|
|
uint subId = await CreateTestSubscriptionAsync().ConfigureAwait(false);
|
|
ISession? otherSession = null;
|
|
try
|
|
{
|
|
await CreateMonitoredItemAsync(
|
|
subId, VariableIds.Server_ServerStatus_CurrentTime)
|
|
.ConfigureAwait(false);
|
|
|
|
// Create a second session and try to ResendData with the first
|
|
// session's subscription id from the second session — must fail.
|
|
otherSession = await ClientFixture.ConnectAsync(
|
|
ServerUrl, SecurityPolicies.None).ConfigureAwait(false);
|
|
|
|
CallResponse resp = await otherSession.CallAsync(
|
|
null,
|
|
new CallMethodRequest[]
|
|
{
|
|
new() {
|
|
ObjectId = ObjectIds.Server,
|
|
MethodId = MethodIds.Server_ResendData,
|
|
InputArguments = new Variant[]
|
|
{
|
|
new(subId)
|
|
}.ToArrayOf()
|
|
}
|
|
}.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(resp.Results.Count, Is.EqualTo(1));
|
|
Assert.That(StatusCode.IsBad(resp.Results[0].StatusCode), Is.True,
|
|
"ResendData on a sub owned by another session should fail.");
|
|
}
|
|
finally
|
|
{
|
|
if (otherSession != null)
|
|
{
|
|
await otherSession.CloseAsync(5000, true).ConfigureAwait(false);
|
|
otherSession.Dispose();
|
|
}
|
|
await DeleteSubscriptionAsync(subId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendDataErr003NoSubscriptionsAsync()
|
|
{
|
|
CallMethodResult result = await CallMethodAsync(
|
|
ObjectIds.Server,
|
|
MethodIds.Server_ResendData,
|
|
new Variant((uint)1)).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
StatusCode.IsBad(result.StatusCode), Is.True,
|
|
"ResendData when no matching subscription should fail.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task RequestServerStateChange000MethodExistsAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
ISession session = admin ?? Session;
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
RequestServerStateChangeId, session).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore("RequestServerStateChange not found.");
|
|
}
|
|
Assert.That(
|
|
dv.GetValue<QualifiedName>(default).Name,
|
|
Is.EqualTo("RequestServerStateChange"));
|
|
}
|
|
finally
|
|
{
|
|
if (admin != null)
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task DeviceFailure000BrowseSubtypesAsync()
|
|
{
|
|
_ = await BrowseForwardAsync(
|
|
DeviceFailureEventTypeId,
|
|
ReferenceTypeIds.HasSubtype).ConfigureAwait(false);
|
|
// The type should exist; subtypes are optional
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
DeviceFailureEventTypeId).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"DeviceFailureEventType should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task EventQueueOverflow001TypeExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
EventQueueOverflowEventTypeId).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"EventQueueOverflowEventType should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task EventQueueOverflow002IsSubtypeOfBaseEventAsync()
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseInverseAsync(
|
|
EventQueueOverflowEventTypeId,
|
|
ReferenceTypeIds.HasSubtype).ConfigureAwait(false);
|
|
Assert.That(refs, Is.Not.Empty,
|
|
"Should have a supertype.");
|
|
var parent = ExpandedNodeId.ToNodeId(
|
|
refs[0].NodeId, Session.NamespaceUris);
|
|
Assert.That(parent, Is.EqualTo(ObjectTypeIds.BaseEventType),
|
|
"Supertype should be BaseEventType.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task EventQueueOverflow003StandardEventFieldsAsync()
|
|
{
|
|
_ = await BrowseForwardAsync(
|
|
EventQueueOverflowEventTypeId).ConfigureAwait(false);
|
|
|
|
// Standard fields are inherited from BaseEventType;
|
|
// verify the type has browseable references
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
EventQueueOverflowEventTypeId).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task ProgressEvents001TypeExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
ProgressEventTypeId).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dv.StatusCode), Is.True,
|
|
"ProgressEventType should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ProgressEvents002VerifyPropertiesAsync()
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ProgressEventTypeId).ConfigureAwait(false);
|
|
if (refs.Count == 0)
|
|
{
|
|
Assert.Fail("ProgressEventType children not browseable.");
|
|
}
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "Context"),
|
|
Is.True, "Context property should exist.");
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "Progress"),
|
|
Is.True, "Progress property should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ProgressEvents003IsSubtypeOfBaseEventAsync()
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseInverseAsync(
|
|
ProgressEventTypeId,
|
|
ReferenceTypeIds.HasSubtype).ConfigureAwait(false);
|
|
Assert.That(refs, Is.Not.Empty,
|
|
"Should have a supertype.");
|
|
var parent = ExpandedNodeId.ToNodeId(
|
|
refs[0].NodeId, Session.NamespaceUris);
|
|
Assert.That(parent, Is.EqualTo(ObjectTypeIds.BaseEventType),
|
|
"Supertype should be BaseEventType.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task SecurityRoles000RoleSetExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
ObjectIds.Server_ServerCapabilities_RoleSet)
|
|
.ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("RoleSet not accessible.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task SecurityRoles001BrowseRoleSetChildrenAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
ObjectIds.Server_ServerCapabilities_RoleSet)
|
|
.ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("RoleSet not accessible.");
|
|
}
|
|
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ObjectIds.Server_ServerCapabilities_RoleSet)
|
|
.ConfigureAwait(false);
|
|
Assert.That(refs, Is.Not.Empty,
|
|
"RoleSet should contain roles.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task SecurityRoles002BrowseRoleTypeInstanceAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
Assert.That(admin, Is.Not.Null);
|
|
|
|
try
|
|
{
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
ObjectIds.WellKnownRole_Anonymous,
|
|
session: admin).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "Identities"),
|
|
Is.True,
|
|
"RoleType.Identities is Mandatory and must exist on the Anonymous role.");
|
|
}
|
|
finally
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task SecurityRoles003AllRolesHaveRequiredPropertiesAsync()
|
|
{
|
|
NodeId[] roleIds =
|
|
[
|
|
ObjectIds.WellKnownRole_Anonymous,
|
|
ObjectIds.WellKnownRole_AuthenticatedUser,
|
|
ObjectIds.WellKnownRole_Observer,
|
|
ObjectIds.WellKnownRole_Operator,
|
|
ObjectIds.WellKnownRole_Engineer,
|
|
ObjectIds.WellKnownRole_Supervisor,
|
|
ObjectIds.WellKnownRole_SecurityAdmin,
|
|
ObjectIds.WellKnownRole_ConfigureAdmin
|
|
];
|
|
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
Assert.That(admin, Is.Not.Null);
|
|
|
|
try
|
|
{
|
|
foreach (NodeId roleId in roleIds)
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(roleId, admin)
|
|
.ConfigureAwait(false);
|
|
Assert.That(dv.StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
roleId,
|
|
session: admin).ConfigureAwait(false);
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "Identities"),
|
|
Is.True,
|
|
$"Role {roleId} must expose its Mandatory Identities child to SecurityAdmin.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task RestrictedMandatoryChildrenFollowStandardRolePermissionsAsync()
|
|
{
|
|
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
|
|
Assert.That(admin, Is.Not.Null, "The reference server must expose username authentication.");
|
|
|
|
try
|
|
{
|
|
(NodeId ParentId, string[] ChildNames)[] groups =
|
|
[
|
|
(ObjectIds.WellKnownRole_Anonymous, ["Identities"]),
|
|
(ObjectIds.Server_ServerCapabilities_RoleSet, ["AddRole", "RemoveRole"]),
|
|
(ObjectIds.UserManagement, ["Users", "AddUser", "ModifyUser", "RemoveUser"]),
|
|
(ObjectIds.ServerConfiguration,
|
|
["UpdateCertificate", "ApplyChanges", "CreateSigningRequest", "GetRejectedList"]),
|
|
(ObjectIds.Server_ServerDiagnostics, ["ServerDiagnosticsSummary"])
|
|
];
|
|
|
|
foreach ((NodeId parentId, string[] childNames) in groups)
|
|
{
|
|
BrowseResult lowPrivilege = await BrowseForwardResultAsync(parentId)
|
|
.ConfigureAwait(false);
|
|
BrowseResult securityAdmin = await BrowseForwardResultAsync(
|
|
parentId,
|
|
session: admin).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
lowPrivilege.StatusCode.Code,
|
|
Is.AnyOf(StatusCodes.Good, StatusCodes.BadUserAccessDenied));
|
|
Assert.That(securityAdmin.StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
|
|
foreach (string childName in childNames)
|
|
{
|
|
if (StatusCode.IsGood(lowPrivilege.StatusCode))
|
|
{
|
|
Assert.That(
|
|
HasReference(lowPrivilege, childName),
|
|
Is.False,
|
|
$"{childName} must be hidden from a low-privilege Browse.");
|
|
}
|
|
Assert.That(
|
|
HasReference(securityAdmin, childName),
|
|
Is.True,
|
|
$"{childName} is Mandatory and must be visible to SecurityAdmin.");
|
|
}
|
|
}
|
|
|
|
// every session may browse SubscriptionDiagnosticsArray to find its own
|
|
// subscriptions (Part 5 6.3.5); its value stays restricted.
|
|
BrowseResult diagnostics = await BrowseForwardResultAsync(ObjectIds.Server_ServerDiagnostics)
|
|
.ConfigureAwait(false);
|
|
Assert.That(diagnostics.StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
Assert.That(HasReference(diagnostics, "SubscriptionDiagnosticsArray"), Is.True,
|
|
"SubscriptionDiagnosticsArray must be browsable by a low-privilege session.");
|
|
|
|
DataValue currentSessionCount = await ReadAttributeAsync(
|
|
VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary_CurrentSessionCount,
|
|
Attributes.Value,
|
|
admin).ConfigureAwait(false);
|
|
|
|
Assert.That(currentSessionCount.StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
Assert.That(currentSessionCount.GetValue<uint>(0), Is.GreaterThanOrEqualTo((uint)2));
|
|
}
|
|
finally
|
|
{
|
|
await admin.CloseAsync(5000, true).ConfigureAwait(false);
|
|
admin.Dispose();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task SelectionList001SelectionsPropertyExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("SelectionListType not found.");
|
|
}
|
|
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "Selections"),
|
|
Is.True, "Selections property should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task SelectionList002RestrictToListExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("SelectionListType not found.");
|
|
}
|
|
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
Assert.That(
|
|
refs.Any(r => r.BrowseName.Name == "RestrictToList"),
|
|
Is.True, "RestrictToList property should exist.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task SelectionList003IsSubtypeOfBaseDataVariableTypeAsync()
|
|
{
|
|
// Verify SelectionListType (i=16309) is declared as a subtype of
|
|
// BaseDataVariableType (i=63) per Part 5 §7.18.
|
|
BrowseResponse browseResp = await Session.BrowseAsync(
|
|
null, null, 0,
|
|
new BrowseDescription[]
|
|
{
|
|
new() {
|
|
NodeId = SelectionListTypeId,
|
|
BrowseDirection = BrowseDirection.Inverse,
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IncludeSubtypes = false,
|
|
NodeClassMask = 0,
|
|
ResultMask = (uint)BrowseResultMask.All
|
|
}
|
|
}.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(browseResp.Results.Count, Is.EqualTo(1));
|
|
if (browseResp.Results[0].References == default! ||
|
|
browseResp.Results[0].References.Count == 0)
|
|
{
|
|
Assert.Ignore("SelectionListType not exposed by server.");
|
|
}
|
|
|
|
ReferenceDescription parent = browseResp.Results[0].References[0];
|
|
var parentId = ExpandedNodeId.ToNodeId(
|
|
parent.NodeId, Session.NamespaceUris);
|
|
Assert.That(parentId, Is.EqualTo(VariableTypeIds.BaseDataVariableType),
|
|
"SelectionListType should be a subtype of BaseDataVariableType.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task SelectionList004SelectionDescriptionsPropertyExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore("SelectionListType not exposed by server.");
|
|
}
|
|
|
|
// SelectionDescriptions is an optional LocalizedText[] property
|
|
// (i=17633) on SelectionListType per Part 5 §7.18.
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
ReferenceDescription selDesc = refs.FirstOrDefault(
|
|
r => r.BrowseName.Name == "SelectionDescriptions")!;
|
|
if (selDesc == null)
|
|
{
|
|
Assert.Ignore("SelectionDescriptions optional property not exposed.");
|
|
}
|
|
|
|
var selDescId = ExpandedNodeId.ToNodeId(
|
|
selDesc.NodeId, Session.NamespaceUris);
|
|
DataValue dt = await ReadAttributeAsync(
|
|
selDescId, Attributes.DataType).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dt.StatusCode), Is.True);
|
|
Assert.That(dt.WrappedValue.TryGetValue(out NodeId dataType), Is.True);
|
|
Assert.That(dataType, Is.EqualTo(DataTypeIds.LocalizedText),
|
|
"SelectionDescriptions must be LocalizedText[].");
|
|
}
|
|
|
|
[Test]
|
|
public async Task SelectionList005RestrictToListIsBooleanAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Ignore("SelectionListType not exposed by server.");
|
|
}
|
|
|
|
// RestrictToList (i=16312) is a Boolean property of
|
|
// SelectionListType per Part 5 §7.18.
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
SelectionListTypeId).ConfigureAwait(false);
|
|
ReferenceDescription restrict = refs.FirstOrDefault(
|
|
r => r.BrowseName.Name == "RestrictToList")!;
|
|
if (restrict == null)
|
|
{
|
|
Assert.Ignore("RestrictToList optional property not exposed.");
|
|
}
|
|
|
|
var restrictId = ExpandedNodeId.ToNodeId(
|
|
restrict.NodeId, Session.NamespaceUris);
|
|
DataValue dt = await ReadAttributeAsync(
|
|
restrictId, Attributes.DataType).ConfigureAwait(false);
|
|
Assert.That(StatusCode.IsGood(dt.StatusCode), Is.True);
|
|
Assert.That(dt.WrappedValue.TryGetValue(out NodeId dataType), Is.True);
|
|
Assert.That(dataType, Is.EqualTo(DataTypeIds.Boolean),
|
|
"RestrictToList must be Boolean.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OrderedList001TypeExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
OrderedListTypeId).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("OrderedListType not found.");
|
|
}
|
|
|
|
List<ReferenceDescription> refs = await BrowseForwardAsync(
|
|
OrderedListTypeId).ConfigureAwait(false);
|
|
bool hasOrderedRef = refs.Any(
|
|
r => r.ReferenceTypeId == ReferenceTypeIds.HasOrderedComponent ||
|
|
r.BrowseName.Name!.Contains("Ordered", StringComparison.Ordinal));
|
|
Assert.That(dv.GetValue<QualifiedName>(default).Name,
|
|
Is.EqualTo("OrderedListType"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task OrderedList002IOrderedObjectTypeExistsAsync()
|
|
{
|
|
DataValue dv = await ReadBrowseNameAsync(
|
|
IOrderedObjectTypeId).ConfigureAwait(false);
|
|
if (StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
Assert.Fail("IOrderedObjectType not found.");
|
|
}
|
|
Assert.That(
|
|
dv.GetValue<QualifiedName>(default).Name,
|
|
Is.EqualTo("IOrderedObjectType"));
|
|
}
|
|
|
|
private static readonly NodeId DeviceFailureEventTypeId = new(2131);
|
|
private static readonly NodeId EventQueueOverflowEventTypeId = new(3035);
|
|
private static readonly NodeId ProgressEventTypeId = new(11436);
|
|
private static readonly NodeId SelectionListTypeId = new(16309);
|
|
private static readonly NodeId OrderedListTypeId = new(23518);
|
|
private static readonly NodeId IOrderedObjectTypeId = new(23513);
|
|
private static readonly NodeId RequestServerStateChangeId = new(12886);
|
|
|
|
private async Task<DataValue> ReadAttributeAsync(
|
|
NodeId nodeId, uint attributeId, ISession? session = null)
|
|
{
|
|
session ??= Session;
|
|
ReadResponse response = await session.ReadAsync(
|
|
null, 0, TimestampsToReturn.Both,
|
|
new ReadValueId[]
|
|
{
|
|
new() {
|
|
NodeId = nodeId,
|
|
AttributeId = attributeId
|
|
}
|
|
}.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(response.Results.Count, Is.EqualTo(1));
|
|
return response.Results[0];
|
|
}
|
|
|
|
private Task<DataValue> ReadValueAsync(NodeId nodeId)
|
|
{
|
|
return ReadAttributeAsync(
|
|
nodeId, Attributes.Value);
|
|
}
|
|
|
|
private Task<DataValue> ReadBrowseNameAsync(NodeId nodeId, ISession? session = null)
|
|
{
|
|
return ReadAttributeAsync(
|
|
nodeId, Attributes.BrowseName, session);
|
|
}
|
|
|
|
private async Task<List<ReferenceDescription>> BrowseForwardAsync(
|
|
NodeId nodeId,
|
|
NodeId referenceTypeId = default,
|
|
bool includeSubtypes = true,
|
|
ISession? session = null)
|
|
{
|
|
BrowseResult result = await BrowseForwardResultAsync(
|
|
nodeId,
|
|
referenceTypeId,
|
|
includeSubtypes,
|
|
session).ConfigureAwait(false);
|
|
|
|
var refs = new List<ReferenceDescription>();
|
|
if (result.References != default!)
|
|
{
|
|
foreach (ReferenceDescription r in result.References)
|
|
{
|
|
refs.Add(r);
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
private async Task<BrowseResult> BrowseForwardResultAsync(
|
|
NodeId nodeId,
|
|
NodeId referenceTypeId = default,
|
|
bool includeSubtypes = true,
|
|
ISession? session = null)
|
|
{
|
|
session ??= Session;
|
|
NodeId refType = referenceTypeId.IsNull
|
|
? ReferenceTypeIds.HierarchicalReferences
|
|
: referenceTypeId;
|
|
|
|
BrowseResponse response = await session.BrowseAsync(
|
|
null, null, 0,
|
|
new BrowseDescription[]
|
|
{
|
|
new() {
|
|
NodeId = nodeId,
|
|
BrowseDirection = BrowseDirection.Forward,
|
|
ReferenceTypeId = refType,
|
|
IncludeSubtypes = includeSubtypes,
|
|
NodeClassMask = 0,
|
|
ResultMask = (uint)BrowseResultMask.All
|
|
}
|
|
}.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(response.Results.Count, Is.EqualTo(1));
|
|
return response.Results[0];
|
|
}
|
|
|
|
private static bool HasReference(BrowseResult result, string browseName)
|
|
{
|
|
if (result.References == default!)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (ReferenceDescription reference in result.References)
|
|
{
|
|
if (reference.BrowseName.Name == browseName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task<List<ReferenceDescription>> BrowseInverseAsync(
|
|
NodeId nodeId,
|
|
NodeId referenceTypeId = default,
|
|
bool includeSubtypes = false)
|
|
{
|
|
NodeId refType = referenceTypeId.IsNull
|
|
? ReferenceTypeIds.HierarchicalReferences
|
|
: referenceTypeId;
|
|
|
|
BrowseResponse response = await Session.BrowseAsync(
|
|
null, null, 0,
|
|
new BrowseDescription[]
|
|
{
|
|
new() {
|
|
NodeId = nodeId,
|
|
BrowseDirection = BrowseDirection.Inverse,
|
|
ReferenceTypeId = refType,
|
|
IncludeSubtypes = includeSubtypes,
|
|
NodeClassMask = 0,
|
|
ResultMask = (uint)BrowseResultMask.All
|
|
}
|
|
}.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(response.Results.Count, Is.EqualTo(1));
|
|
var refs = new List<ReferenceDescription>();
|
|
if (response.Results[0].References != default!)
|
|
{
|
|
foreach (ReferenceDescription r in response.Results[0].References)
|
|
{
|
|
refs.Add(r);
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
private async Task AssertNodeExistsAsync(NodeId nodeId, string name)
|
|
{
|
|
DataValue result = await ReadBrowseNameAsync(nodeId)
|
|
.ConfigureAwait(false);
|
|
if (StatusCode.IsBad(result.StatusCode))
|
|
{
|
|
Assert.Ignore($"{name} not found.");
|
|
}
|
|
}
|
|
|
|
private async Task<CallMethodResult> CallMethodAsync(
|
|
NodeId objectId,
|
|
NodeId methodId,
|
|
params Variant[] inputArgs)
|
|
{
|
|
var request = new CallMethodRequest
|
|
{
|
|
ObjectId = objectId,
|
|
MethodId = methodId,
|
|
InputArguments = inputArgs.ToArrayOf()
|
|
};
|
|
|
|
CallResponse response = await Session.CallAsync(
|
|
null,
|
|
new CallMethodRequest[] { request }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(response.Results.Count, Is.EqualTo(1));
|
|
return response.Results[0];
|
|
}
|
|
|
|
private async Task<uint> CreateTestSubscriptionAsync()
|
|
{
|
|
CreateSubscriptionResponse resp =
|
|
await Session.CreateSubscriptionAsync(
|
|
null, 1000, 10, 2, 0, true, 0,
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
Assert.That(
|
|
StatusCode.IsGood(resp.ResponseHeader.ServiceResult),
|
|
Is.True, "CreateSubscription failed.");
|
|
return resp.SubscriptionId;
|
|
}
|
|
|
|
private async Task DeleteSubscriptionAsync(uint subscriptionId)
|
|
{
|
|
await Session.DeleteSubscriptionsAsync(
|
|
null,
|
|
new uint[] { subscriptionId }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task<CreateMonitoredItemsResponse>
|
|
CreateMonitoredItemAsync(
|
|
uint subscriptionId, NodeId nodeId)
|
|
{
|
|
var item = new MonitoredItemCreateRequest
|
|
{
|
|
ItemToMonitor = new ReadValueId
|
|
{
|
|
NodeId = nodeId,
|
|
AttributeId = Attributes.Value
|
|
},
|
|
MonitoringMode = MonitoringMode.Reporting,
|
|
RequestedParameters = new MonitoringParameters
|
|
{
|
|
ClientHandle = 1,
|
|
SamplingInterval = 1000,
|
|
QueueSize = 10,
|
|
DiscardOldest = true
|
|
}
|
|
};
|
|
|
|
return await Session.CreateMonitoredItemsAsync(
|
|
null, subscriptionId,
|
|
TimestampsToReturn.Both,
|
|
new MonitoredItemCreateRequest[] { item }.ToArrayOf(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
|
|
private static uint[] ExtractUIntArray(Variant variant)
|
|
{
|
|
if (variant.TryGetValue(out ArrayOf<uint> arr))
|
|
{
|
|
return arr.ToArray()!;
|
|
}
|
|
// Some upstream code paths still surface uint[] / IConvertableToArray
|
|
// directly; iterate via TypeInfo to avoid the boxed-object detour.
|
|
switch (variant.TypeInfo.BuiltInType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
if (variant.TryGetValue(out uint single))
|
|
{
|
|
return [single];
|
|
}
|
|
break;
|
|
}
|
|
// FUTURE-AsBoxedObject-cleanup: legacy compatibility for callers
|
|
// that still produce uint[] / IConvertableToArray outside the
|
|
// typed Variant accessors. Once those paths migrate this can drop.
|
|
object val = variant.AsBoxedObject()!;
|
|
if (val is uint[] legacyArr)
|
|
{
|
|
return legacyArr;
|
|
}
|
|
if (val is IConvertableToArray convertable)
|
|
{
|
|
var converted = convertable.ToArray();
|
|
if (converted is uint[] uintArr)
|
|
{
|
|
return uintArr;
|
|
}
|
|
|
|
return [.. converted!.Cast<object>().Select(Convert.ToUInt32)];
|
|
}
|
|
if (val is Array a)
|
|
{
|
|
return [.. a.Cast<object>().Select(Convert.ToUInt32)];
|
|
}
|
|
return null!;
|
|
}
|
|
}
|
|
}
|