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

926 lines
42 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.TestFramework;
namespace Opc.Ua.Subscriptions.Tests
{
/// <summary>
/// compliance tests for Monitor Basic conformance unit.
/// Tests 002-039 covering CreateMonitoredItems, ModifyMonitoredItems,
/// SetMonitoringMode, DataEncoding, and multi-dimensional arrays.
/// </summary>
[NonParallelizable]
[TestFixture]
[Category("Conformance")]
[Category("MonitorBasic")]
public class MonitorBasicTests : TestFixture
{
[SetUp]
public async Task SetUp()
{
m_subscriptionId = await CreateSetupSubscriptionAsync(
publishingInterval: 1000, requestedLifetimeCount: 100,
requestedMaxKeepAliveCount: 10).ConfigureAwait(false);
}
[TearDown]
public async Task TearDown()
{
if (m_subscriptionId > 0)
{
try
{
await Session.DeleteSubscriptionsAsync(
null,
new uint[] { m_subscriptionId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
catch (ServiceResultException)
{
// Subscription may already be deleted
}
m_subscriptionId = 0;
}
}
[Test]
public async Task CreateMonitoredItemsDisabledModeServerTimestampAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse response = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Server,
new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1, queueSize: 1,
mode: MonitoringMode.Disabled)
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(response.ResponseHeader.ServiceResult), Is.True);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(response.Results[0].StatusCode), Is.True);
}
[Test]
public async Task ModifyMonitoredItemChangeClientHandleAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
ModifyMonitoredItemsResponse modifyResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
new MonitoredItemModifyRequest[]
{
new() {
MonitoredItemId = monitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 0x1234,
SamplingInterval = 1000,
QueueSize = 10,
DiscardOldest = true
}
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modifyResp.ResponseHeader.ServiceResult), Is.True);
Assert.That(modifyResp.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(modifyResp.Results[0].StatusCode), Is.True);
}
[Test]
public async Task ModifyMonitoredItemTimestampsToSourceAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Server,
new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1, samplingInterval: 100)
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
ModifyMonitoredItemsResponse modifyResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Source,
new MonitoredItemModifyRequest[]
{
new() {
MonitoredItemId = monitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 100,
QueueSize = 10,
DiscardOldest = true
}
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modifyResp.Results[0].StatusCode), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pubResp = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pubResp.ResponseHeader.ServiceResult), Is.True);
Assert.That(pubResp.NotificationMessage.NotificationData.Count, Is.GreaterThan(0));
}
[Test]
public async Task ModifyMonitoredItemTimestampsToServerAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Source,
new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1, samplingInterval: 100)
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
ModifyMonitoredItemsResponse modifyResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Server,
new MonitoredItemModifyRequest[]
{
new() {
MonitoredItemId = monitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 100,
QueueSize = 10,
DiscardOldest = true
}
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modifyResp.Results[0].StatusCode), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pubResp = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pubResp.ResponseHeader.ServiceResult), Is.True);
Assert.That(pubResp.NotificationMessage.NotificationData.Count, Is.GreaterThan(0));
}
[Test]
public async Task ModifyMonitoredItemTimestampsToNeitherAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Server,
new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1, samplingInterval: 100)
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
ModifyMonitoredItemsResponse modifyResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Neither,
new MonitoredItemModifyRequest[]
{
new() {
MonitoredItemId = monitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 100,
QueueSize = 10,
DiscardOldest = true
}
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modifyResp.Results[0].StatusCode), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pubResp = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pubResp.ResponseHeader.ServiceResult), Is.True);
Assert.That(pubResp.NotificationMessage.NotificationData.Count, Is.GreaterThan(0));
}
[Test]
public async Task ModifyMultipleItemsSamplingIntervalsAsync()
{
var items = new List<MonitoredItemCreateRequest>();
int count = Math.Min(5, Constants.ScalarStaticNodes.Length);
for (int i = 0; i < count; i++)
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticNodes[i]);
items.Add(CreateItemRequest(nodeId, (uint)(100 + i)));
}
CreateMonitoredItemsResponse createResp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
items.ToArray().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(createResp.Results.Count, Is.EqualTo(count));
var modItems = new List<MonitoredItemModifyRequest>();
for (int i = 0; i < count; i++)
{
Assert.That(StatusCode.IsGood(createResp.Results[i].StatusCode), Is.True);
modItems.Add(new MonitoredItemModifyRequest
{
MonitoredItemId = createResp.Results[i].MonitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = (uint)(100 + i),
SamplingInterval = i % 2 == 0 ? 1000 : 3000,
QueueSize = 10,
DiscardOldest = true
}
});
}
ModifyMonitoredItemsResponse modResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
modItems.ToArray().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(modResp.Results.Count, Is.EqualTo(count));
foreach (MonitoredItemModifyResult r in modResp.Results)
{
Assert.That(StatusCode.IsGood(r.StatusCode), Is.True);
Assert.That(r.RevisedSamplingInterval, Is.GreaterThanOrEqualTo(0.0));
}
}
[Test]
public async Task ModifyMonitoredItemQueueSizeZeroAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
ModifyMonitoredItemsResponse modResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
new MonitoredItemModifyRequest[]
{
new() {
MonitoredItemId = monitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 1000,
QueueSize = 0,
DiscardOldest = true
}
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modResp.Results[0].StatusCode), Is.True);
Assert.That(modResp.Results[0].RevisedQueueSize, Is.GreaterThan(0u),
"Server should revise QueueSize=0 to at least 1");
}
[Test]
public async Task ModifyMonitoredItemQueueSizeMaxUInt32Async()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
ModifyMonitoredItemsResponse modResp = await Session.ModifyMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
new MonitoredItemModifyRequest[]
{
new() {
MonitoredItemId = monitoredItemId,
RequestedParameters = new MonitoringParameters
{
ClientHandle = 1,
SamplingInterval = 1000,
QueueSize = uint.MaxValue,
DiscardOldest = true
}
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(modResp.Results[0].StatusCode), Is.True);
Assert.That(modResp.Results[0].RevisedQueueSize, Is.GreaterThan(0u));
}
[Test]
public async Task SetMonitoringModeOnDeletedItemReturnsBadIdAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var items = new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1),
CreateItemRequest(nodeId, 2)
};
CreateMonitoredItemsResponse createResp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
items.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
uint[] itemIds = [.. createResp.Results.ToArray()!.Select(r => r.MonitoredItemId)];
await Session.DeleteMonitoredItemsAsync(
null, m_subscriptionId, itemIds.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Reporting,
itemIds.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.ResponseHeader.ServiceResult), Is.True);
foreach (StatusCode sc in smResp.Results)
{
Assert.That(sc.Code, Is.EqualTo(StatusCodes.BadMonitoredItemIdInvalid));
}
}
[Test]
public async Task SetMonitoringModeOnMixDeletedAndValidItemsAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var items = new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1),
CreateItemRequest(nodeId, 2),
CreateItemRequest(nodeId, 3),
CreateItemRequest(nodeId, 4)
};
CreateMonitoredItemsResponse createResp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
items.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
uint[] allIds = [.. createResp.Results.ToArray()!.Select(r => r.MonitoredItemId)];
uint[] toDelete = [allIds[0], allIds[1]];
await Session.DeleteMonitoredItemsAsync(
null, m_subscriptionId, toDelete.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Reporting,
allIds.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(smResp.Results.Count, Is.EqualTo(4));
Assert.That(smResp.Results[0].Code, Is.EqualTo(StatusCodes.BadMonitoredItemIdInvalid));
Assert.That(smResp.Results[1].Code, Is.EqualTo(StatusCodes.BadMonitoredItemIdInvalid));
Assert.That(StatusCode.IsGood(smResp.Results[2]), Is.True);
Assert.That(StatusCode.IsGood(smResp.Results[3]), Is.True);
}
[Test]
public async Task CreateItemSamplingIntervalZeroReportingAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse resp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 0,
mode: MonitoringMode.Reporting)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(resp.Results[0].StatusCode), Is.True);
Assert.That(resp.Results[0].RevisedSamplingInterval, Is.GreaterThanOrEqualTo(0.0));
}
[Test]
public async Task SetMonitoringModeDisabledToDisabledAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Disabled)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub1 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub1.ResponseHeader.ServiceResult), Is.True);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Disabled,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
}
[Test]
public async Task SetMonitoringModeDisabledToSamplingAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Disabled)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
await Task.Delay(500).ConfigureAwait(false);
await PublishAndAckAsync().ConfigureAwait(false);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Sampling,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
}
[Test]
public async Task SetMonitoringModeDisabledToReportingAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Disabled)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Reporting,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pubResp = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pubResp.ResponseHeader.ServiceResult), Is.True);
Assert.That(pubResp.NotificationMessage.NotificationData.Count,
Is.GreaterThan(0), "Expected data after switching to Reporting");
}
[Test]
[Category("LongRunning")]
public async Task SetMonitoringModeSamplingToDisabledAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Sampling)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
try
{
await Task.Delay(500).ConfigureAwait(false);
await PublishAndAckAsync().ConfigureAwait(false);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Disabled,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
}
catch (ServiceResultException sre) when (IsTransientCiTimeoutStatus(sre.StatusCode))
{
Assert.Ignore(
$"Timing-sensitive: SetMonitoringMode/Publish interrupted by CI runner load ({sre.StatusCode}).");
}
}
[Test]
[Category("LongRunning")]
public async Task SetMonitoringModeSamplingToSamplingAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Sampling)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
try
{
await Task.Delay(500).ConfigureAwait(false);
await PublishAndAckAsync().ConfigureAwait(false);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Sampling,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
}
catch (ServiceResultException sre) when (IsTransientCiTimeoutStatus(sre.StatusCode))
{
Assert.Ignore(
$"Timing-sensitive: SetMonitoringMode/Publish interrupted by CI runner load ({sre.StatusCode}).");
}
}
[Test]
public async Task SetMonitoringModeSamplingToReportingAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Sampling)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
await Task.Delay(500).ConfigureAwait(false);
await PublishAndAckAsync().ConfigureAwait(false);
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Reporting,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
Assert.That(pub2.NotificationMessage.NotificationData.Count,
Is.GreaterThan(0), "Expected data after switching to Reporting");
}
[Test]
public async Task SetMonitoringModeReportingToDisabledAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Reporting)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub1 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub1.ResponseHeader.ServiceResult), Is.True);
Assert.That(pub1.NotificationMessage.NotificationData.Count, Is.GreaterThan(0));
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Disabled,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
}
[Test]
public async Task SetMonitoringModeReportingToSamplingAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Reporting)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub1 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub1.ResponseHeader.ServiceResult), Is.True);
Assert.That(pub1.NotificationMessage.NotificationData.Count, Is.GreaterThan(0));
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Sampling,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
}
[Test]
public async Task SetMonitoringModeReportingToReportingAsync()
{
NodeId nodeId = VariableIds.Server_ServerStatus_CurrentTime;
CreateMonitoredItemsResponse createResp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 100,
mode: MonitoringMode.Reporting)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(createResp.Results[0].StatusCode), Is.True);
uint monitoredItemId = createResp.Results[0].MonitoredItemId;
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub1 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub1.ResponseHeader.ServiceResult), Is.True);
Assert.That(pub1.NotificationMessage.NotificationData.Count, Is.GreaterThan(0));
SetMonitoringModeResponse smResp = await Session.SetMonitoringModeAsync(
null, m_subscriptionId, MonitoringMode.Reporting,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(smResp.Results[0]), Is.True);
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pub2 = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pub2.ResponseHeader.ServiceResult), Is.True);
Assert.That(pub2.NotificationMessage.NotificationData.Count,
Is.GreaterThan(0), "Expected data after re-setting Reporting mode");
}
[Test]
public async Task CreateMonitoredItemsForAllAttributesAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
uint[] attributeIds =
[
Attributes.NodeId,
Attributes.NodeClass,
Attributes.BrowseName,
Attributes.DisplayName,
Attributes.Description,
Attributes.WriteMask,
Attributes.UserWriteMask,
Attributes.Value,
Attributes.DataType,
Attributes.ValueRank,
Attributes.AccessLevel,
Attributes.UserAccessLevel,
Attributes.Historizing
];
var items = new List<MonitoredItemCreateRequest>();
for (int i = 0; i < attributeIds.Length; i++)
{
items.Add(CreateItemRequest(nodeId, (uint)(200 + i),
attributeId: attributeIds[i]));
}
CreateMonitoredItemsResponse resp = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
items.ToArray().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(resp.Results.Count, Is.EqualTo(attributeIds.Length));
int goodCount = resp.Results.ToArray()!
.Count(r => StatusCode.IsGood(r.StatusCode));
Assert.That(goodCount, Is.GreaterThan(0),
"At least some attribute monitors should succeed");
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pubResp = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pubResp.ResponseHeader.ServiceResult), Is.True);
}
[Test]
public async Task CreateMonitoredItemDataEncodingVariationsAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var testCases = new (QualifiedName Encoding, string Name, uint Attribute)[]
{
(default, "null", Attributes.Value),
(new QualifiedName(string.Empty), "empty", Attributes.Value),
(new QualifiedName("Default Binary", 0), "Default Binary", Attributes.Value),
(new QualifiedName("Default XML", 0), "Default XML", Attributes.Value),
(new QualifiedName("Default JSON", 0), "Default JSON", Attributes.Value),
(new QualifiedName("Modbus", 0), "unknown Modbus", Attributes.Value),
(new QualifiedName("Default Binary", 999), "invalid namespace", Attributes.Value),
(new QualifiedName("Default Binary", 0), "BrowseName with encoding",
Attributes.BrowseName)
};
for (int i = 0; i < testCases.Length; i++)
{
var item = new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
NodeId = nodeId,
AttributeId = testCases[i].Attribute,
DataEncoding = testCases[i].Encoding
},
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
ClientHandle = (uint)(50 + i),
SamplingInterval = 1000,
Filter = default,
DiscardOldest = true,
QueueSize = 10
}
};
CreateMonitoredItemsResponse resp =
await CreateSingleItemAsync(item).ConfigureAwait(false);
StatusCode sc = resp.Results[0].StatusCode;
Assert.That(
StatusCode.IsGood(sc) ||
sc == StatusCodes.BadDataEncodingUnsupported ||
sc == StatusCodes.BadDataEncodingInvalid,
Is.True, $"Encoding '{testCases[i].Name}': unexpected status {sc}");
if (StatusCode.IsGood(sc))
{
await DeleteMonitoredItemAsync(
resp.Results[0].MonitoredItemId).ConfigureAwait(false);
}
}
}
[Test]
public async Task CreateMonitoredItemsDisabledModeServerTimestampDuplicateAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse response = await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Server,
new MonitoredItemCreateRequest[]
{
CreateItemRequest(nodeId, 1, queueSize: 1,
mode: MonitoringMode.Disabled)
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(response.ResponseHeader.ServiceResult), Is.True);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(response.Results[0].StatusCode), Is.True);
}
[Test]
public async Task CreateItemSamplingIntervalZeroVerifyRevisedAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
CreateMonitoredItemsResponse resp = await CreateSingleItemAsync(
CreateItemRequest(nodeId, 1, samplingInterval: 0,
mode: MonitoringMode.Reporting)).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(resp.Results[0].StatusCode), Is.True);
Assert.That(resp.Results[0].RevisedSamplingInterval,
Is.GreaterThanOrEqualTo(0.0),
"Server should revise sampling interval of 0");
}
[Test]
public async Task CreateMonitoredItemsMultiDimensionalArrayAsync()
{
var items = new List<MonitoredItemCreateRequest>();
int count = Constants.ScalarStaticArrayNodes.Length;
for (int i = 0; i < count; i++)
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayNodes[i]);
items.Add(CreateItemRequest(nodeId, (uint)(300 + i),
samplingInterval: 100));
}
CreateMonitoredItemsResponse createResp =
await Session.CreateMonitoredItemsAsync(
null, m_subscriptionId, TimestampsToReturn.Both,
items.ToArray().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(createResp.Results.Count, Is.EqualTo(count));
int goodCount = createResp.Results.ToArray()!
.Count(r => StatusCode.IsGood(r.StatusCode));
Assert.That(goodCount, Is.GreaterThan(0),
"At least some array node monitors should succeed");
await Task.Delay(500).ConfigureAwait(false);
PublishResponse pubResp = await PublishAndAckAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(pubResp.ResponseHeader.ServiceResult), Is.True);
if (pubResp.NotificationMessage.NotificationData.Count > 0)
{
var dcn = ExtensionObject.ToEncodeable(
pubResp.NotificationMessage.NotificationData[0]) as
DataChangeNotification;
Assert.That(dcn, Is.Not.Null);
Assert.That(dcn.MonitoredItems.Count, Is.GreaterThan(0),
"Should receive initial values for array monitored items");
}
}
private static MonitoredItemCreateRequest CreateItemRequest(
NodeId nodeId,
uint clientHandle,
double samplingInterval = 1000,
uint queueSize = 10,
MonitoringMode mode = MonitoringMode.Reporting,
uint attributeId = Attributes.Value,
bool discardOldest = true,
ExtensionObject filter = default)
{
return new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
NodeId = nodeId,
AttributeId = attributeId
},
MonitoringMode = mode,
RequestedParameters = new MonitoringParameters
{
ClientHandle = clientHandle,
SamplingInterval = samplingInterval,
Filter = filter,
DiscardOldest = discardOldest,
QueueSize = queueSize
}
};
}
private async Task<CreateMonitoredItemsResponse> CreateSingleItemAsync(
MonitoredItemCreateRequest item,
TimestampsToReturn timestamps = TimestampsToReturn.Both)
{
return await Session.CreateMonitoredItemsAsync(
null,
m_subscriptionId,
timestamps,
new MonitoredItemCreateRequest[] { item }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
private async Task<PublishResponse> PublishAndAckAsync()
{
return await Session.PublishWithTimeoutAsync().ConfigureAwait(false);
}
private async Task DeleteMonitoredItemAsync(uint monitoredItemId)
{
await Session.DeleteMonitoredItemsAsync(
null, m_subscriptionId,
new uint[] { monitoredItemId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
private uint m_subscriptionId;
}
}