mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +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>
764 lines
28 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|