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

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

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

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

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

## Related Issues

- Fixes #4439

## Checklist

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

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

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

---------

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

764 lines
28 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.Threading;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Core.Tests.Stack.State
{
/// <summary>
/// Tests for concurrency issues in BaseVariableState class
/// </summary>
[TestFixture]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Category("NodeStateConcurrency")]
[Parallelizable]
public class NodeStateHandlerConcurrencyTests
{
/// <summary>
/// This data tests all handlers in NodeState and BaseVariableState class
/// </summary>
public static IEnumerable<TestCaseData> VariableHandlerTestCases
{
get
{
// Test OnWriteValue
Action<BaseVariableState> action = state =>
{
state.OnWriteValue = ValueChangedHandler;
state.OnWriteValue = null;
};
yield return new TestCaseData(Attributes.Value, new Variant(15.5), action);
// Test OnSimpleWriteValue
action = state =>
{
state.OnSimpleWriteValue = NodeAttributeEventHandler;
state.OnSimpleWriteValue = null;
};
yield return new TestCaseData(Attributes.Value, new Variant(15.5), action);
// Test OnWriteNodeId
action = state =>
{
state.OnWriteNodeId = NodeAttributeEventHandler;
state.OnWriteNodeId = null;
};
yield return new TestCaseData(
Attributes.NodeId,
new Variant(new NodeId(22, 7)),
action);
// Test OnWriteNodeClass
action = state =>
{
state.OnWriteNodeClass = NodeAttributeEventHandler;
state.OnWriteNodeClass = null;
};
yield return new TestCaseData(
Attributes.NodeClass,
new Variant((int)NodeClass.Variable),
action);
// Test OnWriteBrowseName
action = state =>
{
state.OnWriteBrowseName = NodeAttributeEventHandler;
state.OnWriteBrowseName = null;
};
yield return new TestCaseData(
Attributes.BrowseName,
new Variant(QualifiedName.From("test")),
action);
// Test OnWriteDisplayName
action = state =>
{
state.OnWriteDisplayName = NodeAttributeEventHandler;
state.OnWriteDisplayName = null;
};
yield return new TestCaseData(
Attributes.DisplayName,
new Variant(new LocalizedText("test")),
action);
// Test OnWriteDescription
action = state =>
{
state.OnWriteDescription = NodeAttributeEventHandler;
state.OnWriteDescription = null;
};
yield return new TestCaseData(
Attributes.Description,
new Variant(new LocalizedText("test")),
action);
// Test OnWriteWriteMask
action = state =>
{
state.OnWriteWriteMask = NodeAttributeEventHandler;
state.OnWriteWriteMask = null;
};
yield return new TestCaseData(
Attributes.WriteMask,
new Variant(
(uint)AttributeWriteMask.WriteMask |
(uint)AttributeWriteMask.UserWriteMask),
action);
// Test OnWriteUserWriteMask
action = state =>
{
state.OnWriteUserWriteMask = NodeAttributeEventHandler;
state.OnWriteUserWriteMask = null;
};
yield return new TestCaseData(
Attributes.UserWriteMask,
new Variant(
(uint)AttributeWriteMask.WriteMask |
(uint)AttributeWriteMask.UserWriteMask),
action);
// Test OnWriteRolePermissions
action = state =>
{
state.OnWriteRolePermissions = NodeAttributeEventHandler;
state.OnWriteRolePermissions = null;
};
yield return new TestCaseData(
Attributes.RolePermissions,
new Variant(s_value),
action);
// Test OnWriteAccessRestrictions
action = state =>
{
state.OnWriteAccessRestrictions = NodeAttributeEventHandler;
state.OnWriteAccessRestrictions = null;
};
yield return new TestCaseData(
Attributes.AccessRestrictions,
new Variant((ushort)AccessRestrictionType.EncryptionRequired),
action);
// Test OnWriteDataType
action = state =>
{
state.OnWriteDataType = NodeAttributeEventHandler;
state.OnWriteDataType = null;
};
yield return new TestCaseData(
Attributes.DataType,
new Variant(DataTypeIds.Double),
action);
// Test OnWriteValueRank
action = state =>
{
state.OnWriteValueRank = NodeAttributeEventHandler;
state.OnWriteValueRank = null;
};
yield return new TestCaseData(
Attributes.ValueRank,
new Variant(ValueRanks.Scalar),
action);
// Test OnWriteArrayDimensions
action = state =>
{
state.OnWriteArrayDimensions = NodeAttributeEventHandler;
state.OnWriteArrayDimensions = null;
};
yield return new TestCaseData(
Attributes.ArrayDimensions,
new Variant(new List<uint> { 2, 2 }),
action);
// Test OnWriteAccessLevel
action = state =>
{
state.OnWriteAccessLevel = NodeAttributeEventHandler;
state.OnWriteAccessLevel = null;
};
yield return new TestCaseData(
Attributes.AccessLevel,
new Variant(AccessLevels.CurrentRead),
action);
// Test OnWriteUserAccessLevel
action = state =>
{
state.OnWriteUserAccessLevel = NodeAttributeEventHandler;
state.OnWriteUserAccessLevel = null;
};
yield return new TestCaseData(
Attributes.UserAccessLevel,
new Variant(AccessLevels.CurrentRead),
action);
// Test OnWriteMinimumSamplingInterval
action = state =>
{
state.OnWriteMinimumSamplingInterval = NodeAttributeEventHandler;
state.OnWriteMinimumSamplingInterval = null;
};
yield return new TestCaseData(
Attributes.MinimumSamplingInterval,
new Variant(1000.0),
action);
// Test OnWriteHistorizing
action = state =>
{
state.OnWriteHistorizing = NodeAttributeEventHandler;
state.OnWriteHistorizing = null;
};
yield return new TestCaseData(Attributes.Historizing, new Variant(true), action);
}
}
/// <summary>
/// This test data test all handlers in BaseVariableTypeState and BaseTypeState class
/// </summary>
public static IEnumerable<TestCaseData> VariableTypeHandlerTestCases
{
get
{
// Test OnWriteDataType
Action<BaseDataVariableTypeState> action = state =>
{
state.OnWriteDataType = NodeAttributeEventHandler;
state.OnWriteDataType = null;
};
yield return new TestCaseData(
Attributes.DataType,
new Variant(DataTypeIds.Double),
action);
// Test OnWriteValueRank
action = state =>
{
state.OnWriteValueRank = NodeAttributeEventHandler;
state.OnWriteValueRank = null;
};
yield return new TestCaseData(
Attributes.ValueRank,
new Variant(ValueRanks.Scalar),
action);
// Test OnWriteArrayDimensions
action = state =>
{
state.OnWriteArrayDimensions = NodeAttributeEventHandler;
state.OnWriteArrayDimensions = null;
};
yield return new TestCaseData(
Attributes.ArrayDimensions,
new Variant(new List<uint> { 2, 2 }),
action);
// Test OnWriteIsAbstract
action = state =>
{
state.OnWriteIsAbstract = NodeAttributeEventHandler;
state.OnWriteIsAbstract = null;
};
yield return new TestCaseData(Attributes.IsAbstract, new Variant(false), action);
}
}
/// <summary>
/// This test data test all handlers in BaseObjectState class
/// </summary>
public static IEnumerable<TestCaseData> ObjectHandlerTestCases
{
get
{
// Test EventNotifier
Action<BaseObjectState> action = state =>
{
state.OnWriteEventNotifier = NodeAttributeEventHandler;
state.OnWriteEventNotifier = null;
};
yield return new TestCaseData(
Attributes.EventNotifier,
new Variant(EventNotifiers.SubscribeToEvents),
action);
}
}
/// <summary>
/// This test data test all handlers in MethodState class
/// </summary>
public static IEnumerable<TestCaseData> MethodHandlerTestCases
{
get
{
// Test Executable
Action<MethodState> action = state =>
{
state.OnWriteExecutable = NodeAttributeEventHandler;
state.OnWriteExecutable = null;
};
yield return new TestCaseData(Attributes.Executable, new Variant(true), action);
// Test UserExecutable
action = state =>
{
state.OnWriteUserExecutable = NodeAttributeEventHandler;
state.OnWriteUserExecutable = null;
};
yield return new TestCaseData(Attributes.UserExecutable, new Variant(true), action);
}
}
/// <summary>
/// This test data test all handlers in ReferenceTypeState class
/// </summary>
public static IEnumerable<TestCaseData> ReferenceTypeHandlerTestCases
{
get
{
// Test InverseName
Action<ReferenceTypeState> action = state =>
{
state.OnWriteInverseName = NodeAttributeEventHandler;
state.OnWriteInverseName = null;
};
yield return new TestCaseData(
Attributes.InverseName,
new Variant(new LocalizedText("inverse test")),
action);
// Test Symmetric
action = state =>
{
state.OnWriteSymmetric = NodeAttributeEventHandler;
state.OnWriteSymmetric = null;
};
yield return new TestCaseData(Attributes.Symmetric, new Variant(true), action);
}
}
/// <summary>
/// This test data test all handlers in ViewState class
/// </summary>
public static IEnumerable<TestCaseData> ViewHandlerTestCases
{
get
{
// Test EventNotifier
Action<ViewState> action = state =>
{
state.OnWriteEventNotifier = NodeAttributeEventHandler;
state.OnWriteEventNotifier = null;
};
yield return new TestCaseData(
Attributes.EventNotifier,
new Variant(EventNotifiers.SubscribeToEvents),
action);
// Test ContainsNoLoops
action = state =>
{
state.OnWriteContainsNoLoops = NodeAttributeEventHandler;
state.OnWriteContainsNoLoops = null;
};
yield return new TestCaseData(
Attributes.ContainsNoLoops,
new Variant(true),
action);
}
}
/// <summary>
/// This test data test all handlers in DataTypeState class
/// </summary>
public static IEnumerable<TestCaseData> DataTypeHandlerTestCases
{
get
{
// Test DataTypeDefinition
Action<DataTypeState> action = state =>
{
state.OnWriteDataTypeDefinition = NodeAttributeEventHandler;
state.OnWriteDataTypeDefinition = null;
};
yield return new TestCaseData(
Attributes.DataTypeDefinition,
new Variant(new ExtensionObject()),
action);
}
}
private static readonly ExtensionObject[] s_value = [];
[TestCaseSource(nameof(VariableHandlerTestCases))]
[Parallelizable]
public void VariableNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action<BaseVariableState> concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new AnalogUnitRangeState(null);
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(VariableTypeHandlerTestCases))]
[Parallelizable]
public void VariableTypeNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action<BaseDataVariableTypeState> concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new BaseDataVariableTypeState();
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(ObjectHandlerTestCases))]
[Parallelizable]
public void ObjectNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action<BaseObjectState> concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new BaseObjectState(null);
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(MethodHandlerTestCases))]
[Parallelizable]
public void MethodNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action<MethodState> concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new MethodState(null);
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(ReferenceTypeHandlerTestCases))]
[Parallelizable]
public void ReferenceTypeNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action<ReferenceTypeState> concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new ReferenceTypeState();
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(ViewHandlerTestCases))]
[Parallelizable]
public void ViewNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action<ViewState> concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new ViewState();
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
private static void ExecuteNodeHandlerConcurrencyTest<T>(
ISystemContext systemContext,
uint attribute,
Variant variant,
T node,
Action<T> concurrentTaskAction)
where T : NodeState
{
node.WriteMask =
AttributeWriteMask.AccessLevel |
AttributeWriteMask.ArrayDimensions |
AttributeWriteMask.BrowseName |
AttributeWriteMask.ContainsNoLoops |
AttributeWriteMask.DataType |
AttributeWriteMask.Description |
AttributeWriteMask.DisplayName |
AttributeWriteMask.EventNotifier |
AttributeWriteMask.Executable |
AttributeWriteMask.Historizing |
AttributeWriteMask.InverseName |
AttributeWriteMask.IsAbstract |
AttributeWriteMask.MinimumSamplingInterval |
AttributeWriteMask.NodeClass |
AttributeWriteMask.NodeId |
AttributeWriteMask.Symmetric |
AttributeWriteMask.UserAccessLevel |
AttributeWriteMask.UserExecutable |
AttributeWriteMask.UserWriteMask |
AttributeWriteMask.ValueForVariableType |
AttributeWriteMask.ValueRank |
AttributeWriteMask.WriteMask |
AttributeWriteMask.RolePermissions |
AttributeWriteMask.AccessRestrictions;
if (node is BaseVariableState baseVariableState)
{
// Make Value attribute writable so that it is possible to test Value attribute writing
ServiceResult result = baseVariableState.WriteAttribute(
systemContext,
Attributes.AccessLevel,
default,
new DataValue(new Variant(AccessLevels.CurrentReadOrWrite)));
Assert.That(ServiceResult.IsGood(result), Is.True);
result = baseVariableState.WriteAttribute(
systemContext,
Attributes.UserAccessLevel,
default,
new DataValue(new Variant(AccessLevels.CurrentReadOrWrite)));
Assert.That(ServiceResult.IsGood(result), Is.True);
}
bool running = true;
Exception? workerError = null;
// The worker is a raw thread, so three things have to hold or it can
// take the whole test host down with it:
// * IsBackground, so a worker that somehow outlives this method
// cannot pin the process at exit. A foreground thread here is
// exactly the shape of an "all tests passed, then the host never
// exits" hang.
// * Volatile access to the stop flag, so the worker is guaranteed
// to observe the request rather than spinning on a cached read.
// * A catch, because an unhandled exception on a raw thread
// terminates the process and takes every remaining test with it.
// Capture it and surface it on the test thread instead.
var worker = new Thread(() =>
{
try
{
while (Volatile.Read(ref running))
{
concurrentTaskAction(node);
}
}
catch (Exception ex)
{
workerError = ex;
}
})
{
IsBackground = true,
Name = "NodeStateHandlerConcurrency.Worker"
};
worker.Start();
try
{
DateTime utcNow = DateTime.UtcNow;
while (DateTime.UtcNow - utcNow < TimeSpan.FromSeconds(1))
{
ServiceResult writeResult = node.WriteAttribute(
systemContext,
attribute,
default,
new DataValue(variant));
Assert.That(
ServiceResult.IsGood(writeResult),
Is.True,
$"Expected Good ServiceResult but was: {writeResult}");
}
}
finally
{
// Stop the worker even when the assertion above throws;
// otherwise a single failed assertion leaks a spinning thread
// for the rest of the run.
Volatile.Write(ref running, false);
if (!worker.Join(TimeSpan.FromSeconds(30)))
{
// Bounded so a stuck worker cannot hang the run. It is a
// background thread, so leaving it behind is survivable.
TestContext.Progress.WriteLine(
"NodeStateHandlerConcurrency worker did not stop within 30 seconds.");
}
}
if (workerError != null)
{
Assert.Fail($"The concurrent writer threw: {workerError}");
}
}
private static ServiceResult ValueChangedHandler(
ISystemContext context,
NodeState node,
NumericRange indexRange,
QualifiedName dataEncoding,
ref Variant value,
ref StatusCode statusCode,
ref DateTimeUtc timestamp)
{
return ServiceResult.Good;
}
private static ServiceResult NodeAttributeEventHandler<T>(
ISystemContext context,
NodeState node,
ref T value)
{
return ServiceResult.Good;
}
}
}