mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 16:52:07 +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>
650 lines
26 KiB
C#
650 lines
26 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.Text;
|
|
using System.Xml;
|
|
using Microsoft.Extensions.Logging;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
using SchemaTypes = Opc.Ua.Schema.Types;
|
|
|
|
namespace Opc.Ua.Schema.Model.Tests
|
|
{
|
|
/// <summary>
|
|
/// Regression tests for defects in <see cref="ModelDesignValidator"/>
|
|
/// (identifier allocation, dependency load order, method argument
|
|
/// properties, enum identifiers and dependency value ranks).
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("ModelDesign")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
public class ModelDesignValidatorRegressionTests
|
|
{
|
|
private VirtualFileSystem m_fileSystem;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
m_fileSystem = new VirtualFileSystem();
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
m_fileSystem?.Dispose();
|
|
m_fileSystem = null!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// An explicit NumericId in the design must be reserved before
|
|
/// identifiers are allocated, otherwise another node of the same
|
|
/// namespace is handed the same number and validation fails with
|
|
/// "Duplicate identifiers".
|
|
/// </summary>
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public void ExplicitNumericIdIsNotHandedOutToAnotherNode(bool pinnedFirst)
|
|
{
|
|
const string pinned =
|
|
"""<opc:ObjectType SymbolicName="PinnedType" BaseType="ua:BaseObjectType" NumericId="1" />""";
|
|
const string auto =
|
|
"""<opc:ObjectType SymbolicName="AutoType" BaseType="ua:BaseObjectType" />""";
|
|
string path = AddDesign("Pinned", pinnedFirst ? pinned + auto : auto + pinned);
|
|
ModelDesignValidator validator = CreateValidator(startId: 1);
|
|
|
|
Assert.DoesNotThrow(() => validator.Validate([path], [], null!));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(FindNode(validator, "PinnedType", "Pinned").NumericId, Is.EqualTo(1u));
|
|
Assert.That(FindNode(validator, "AutoType", "Pinned").NumericId, Is.EqualTo(2u));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// The dependency load order must be a topological order of the
|
|
/// imports independent of the order the dependency files are passed
|
|
/// in: Z imports X which imports Y (which the target imports too).
|
|
/// </summary>
|
|
[TestCase("Z", "X", "Y")]
|
|
[TestCase("X", "Z", "Y")]
|
|
[TestCase("Y", "Z", "X")]
|
|
[TestCase("Z", "Y", "X")]
|
|
public void DependenciesLoadInImportOrderRegardlessOfFileOrder(
|
|
string first,
|
|
string second,
|
|
string third)
|
|
{
|
|
var files = new Dictionary<string, string>
|
|
{
|
|
["Y"] = AddDesign(
|
|
"Y",
|
|
"""<opc:ObjectType SymbolicName="YType" BaseType="ua:BaseObjectType" />"""),
|
|
["X"] = AddDesign(
|
|
"X",
|
|
"""<opc:ObjectType SymbolicName="XType" BaseType="y:YType" />""",
|
|
"Y"),
|
|
["Z"] = AddDesign(
|
|
"Z",
|
|
"""<opc:ObjectType SymbolicName="ZType" BaseType="x:XType" />""",
|
|
"X")
|
|
};
|
|
string target = AddDesign(
|
|
"T",
|
|
"""<opc:ObjectType SymbolicName="TType" BaseType="y:YType" />""",
|
|
"Y");
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
Assert.DoesNotThrow(() => validator.Validate(
|
|
[target],
|
|
[files[first], files[second], files[third]],
|
|
null!));
|
|
|
|
var zType = (TypeDesign)FindNode(validator, "ZType", "Z");
|
|
Assert.That(zType.BaseTypeNode?.SymbolicName?.Name, Is.EqualTo("XType"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A dependency that imports the primary target must still be loaded
|
|
/// after it.
|
|
/// </summary>
|
|
[Test]
|
|
public void DependencyImportingTheTargetLoadsAfterTheTarget()
|
|
{
|
|
string downstream = AddDesign(
|
|
"D",
|
|
"""<opc:ObjectType SymbolicName="DType" BaseType="t:TType" />""",
|
|
"T");
|
|
string target = AddDesign(
|
|
"T",
|
|
"""<opc:ObjectType SymbolicName="TType" BaseType="ua:BaseObjectType" />""");
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
Assert.DoesNotThrow(() => validator.Validate([target], [downstream], null!));
|
|
|
|
var dType = (TypeDesign)FindNode(validator, "DType", "D");
|
|
Assert.That(dType.BaseTypeNode?.SymbolicName?.Name, Is.EqualTo("TType"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// OPC 10000-3 only defines the InputArguments / OutputArguments
|
|
/// properties of a method when it has arguments of that direction.
|
|
/// A child method without arguments must not grow empty argument
|
|
/// properties (which consume NodeIds and are emitted as nodes).
|
|
/// </summary>
|
|
[Test]
|
|
public void ChildMethodWithoutArgumentsHasNoArgumentProperties()
|
|
{
|
|
string path = AddDesign("Methods", MachineTypeWithMethods("MachineType", "ua:BaseObjectType"));
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
validator.Validate([path], [], null!);
|
|
|
|
var type = (TypeDesign)FindNode(validator, "MachineType", "Methods");
|
|
List<string?> paths = type.Hierarchy.NodeList.ConvertAll(n => n.RelativePath);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(paths, Does.Contain("Reset"));
|
|
Assert.That(paths, Does.Not.Contain("Reset_InputArguments"));
|
|
Assert.That(paths, Does.Not.Contain("Reset_OutputArguments"));
|
|
Assert.That(paths, Does.Contain("Start_InputArguments"));
|
|
Assert.That(paths, Does.Not.Contain("Start_OutputArguments"));
|
|
Assert.That(paths, Does.Contain("Stop_OutputArguments"));
|
|
Assert.That(paths, Does.Not.Contain("Stop_InputArguments"));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Same as <see cref="ChildMethodWithoutArgumentsHasNoArgumentProperties"/>
|
|
/// for methods inherited from a type declared in a dependency design
|
|
/// (linked by LinkDependencyInstances instead of ValidateInstance).
|
|
/// </summary>
|
|
[Test]
|
|
public void InheritedDependencyMethodWithoutArgumentsHasNoArgumentProperties()
|
|
{
|
|
string dependency = AddDesign("Dep", MachineTypeWithMethods("DepMachineType", "ua:BaseObjectType"));
|
|
string target = AddDesign(
|
|
"Tgt",
|
|
"""<opc:ObjectType SymbolicName="SubMachineType" BaseType="dep:DepMachineType" />""",
|
|
"Dep");
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
validator.Validate([target], [dependency], null!);
|
|
|
|
var type = (TypeDesign)FindNode(validator, "SubMachineType", "Tgt");
|
|
List<string?> paths = type.Hierarchy.NodeList.ConvertAll(n => n.RelativePath);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(paths, Does.Contain("Reset"));
|
|
Assert.That(paths, Does.Not.Contain("Reset_InputArguments"));
|
|
Assert.That(paths, Does.Not.Contain("Reset_OutputArguments"));
|
|
Assert.That(paths, Does.Contain("Start_InputArguments"));
|
|
Assert.That(paths, Does.Not.Contain("Start_OutputArguments"));
|
|
Assert.That(paths, Does.Contain("Stop_OutputArguments"));
|
|
Assert.That(paths, Does.Not.Contain("Stop_InputArguments"));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// ALT-6: the argument properties are ensured per direction, in the
|
|
/// target and the dependency path alike. A method that declares its
|
|
/// InputArguments property explicitly keeps that one (the target path
|
|
/// used to add a second) and still gets its OutputArguments property
|
|
/// (the dependency path used to skip both when either existed).
|
|
/// </summary>
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public void ExplicitArgumentPropertyIsKeptAndTheOtherDirectionAdded(bool fromDependency)
|
|
{
|
|
const string machine =
|
|
"""
|
|
<opc:ObjectType SymbolicName="MoverType" BaseType="ua:BaseObjectType">
|
|
<opc:Children>
|
|
<opc:Method SymbolicName="Move" ModellingRule="Mandatory">
|
|
<opc:Children>
|
|
<opc:Property SymbolicName="ua:InputArguments" DataType="ua:Argument" ValueRank="Array" ModellingRule="Mandatory" />
|
|
</opc:Children>
|
|
<opc:InputArguments>
|
|
<opc:Argument Name="Distance" DataType="ua:Double" />
|
|
</opc:InputArguments>
|
|
<opc:OutputArguments>
|
|
<opc:Argument Name="Reached" DataType="ua:Boolean" />
|
|
</opc:OutputArguments>
|
|
</opc:Method>
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
""";
|
|
ModelDesignValidator validator = CreateValidator();
|
|
string ns;
|
|
if (fromDependency)
|
|
{
|
|
string dependency = AddDesign("Dep", machine);
|
|
string target = AddDesign(
|
|
"Tgt",
|
|
"""<opc:ObjectType SymbolicName="SubMoverType" BaseType="dep:MoverType" />""",
|
|
"Dep");
|
|
validator.Validate([target], [dependency], null!);
|
|
ns = "Dep";
|
|
}
|
|
else
|
|
{
|
|
validator.Validate([AddDesign("Movers", machine)], [], null!);
|
|
ns = "Movers";
|
|
}
|
|
|
|
var type = (TypeDesign)FindNode(validator, "MoverType", ns);
|
|
var method = (MethodDesign)type.Children.Items.Single(c => c.SymbolicName.Name == "Move");
|
|
List<string> names = [.. method.Children.Items.Select(c => c.SymbolicName.Name)];
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(names.Count(n => n == "InputArguments"), Is.EqualTo(1), string.Join(",", names));
|
|
Assert.That(names.Count(n => n == "OutputArguments"), Is.EqualTo(1), string.Join(",", names));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// The trailing "_<digits>" naming convention only supplies an
|
|
/// enum value when none is authored: an explicit Identifier wins.
|
|
/// </summary>
|
|
[Test]
|
|
public void ExplicitEnumIdentifierWinsOverTheNameSuffix()
|
|
{
|
|
string path = AddDesign(
|
|
"Enums",
|
|
"""
|
|
<opc:DataType SymbolicName="TextEncoding" BaseType="ua:Enumeration">
|
|
<opc:Fields>
|
|
<opc:Field Name="UTF_8" Identifier="0" />
|
|
<opc:Field Name="UTF_16" Identifier="1" />
|
|
<opc:Field Name="Other" />
|
|
<opc:Field Name="Legacy_7" />
|
|
<opc:Field Name="Next" />
|
|
</opc:Fields>
|
|
</opc:DataType>
|
|
""");
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
validator.Validate([path], [], null!);
|
|
|
|
var dataType = (DataTypeDesign)FindNode(validator, "TextEncoding", "Enums");
|
|
Assert.That(
|
|
dataType.Fields.Select(f => f.Identifier),
|
|
Is.EqualTo(new decimal[] { 0, 1, 2, 7, 8 }));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A child variable of a dependency type keeps its authored ValueRank
|
|
/// instead of taking the rank of its default value.
|
|
/// </summary>
|
|
[Test]
|
|
public void DependencyChildVariableKeepsItsAuthoredValueRank()
|
|
{
|
|
string dependency = AddDesign(
|
|
"Dep",
|
|
"""
|
|
<opc:ObjectType SymbolicName="DeviceType" BaseType="ua:BaseObjectType">
|
|
<opc:Children>
|
|
<opc:Variable SymbolicName="Limits" DataType="ua:Double" ValueRank="ScalarOrArray"
|
|
TypeDefinition="ua:BaseDataVariableType">
|
|
<opc:DefaultValue>
|
|
<uax:Double>0</uax:Double>
|
|
</opc:DefaultValue>
|
|
</opc:Variable>
|
|
<opc:Variable SymbolicName="Level" DataType="ua:Double"
|
|
TypeDefinition="ua:BaseDataVariableType">
|
|
<opc:DefaultValue>
|
|
<uax:Double>0</uax:Double>
|
|
</opc:DefaultValue>
|
|
</opc:Variable>
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
""");
|
|
string target = AddDesign(
|
|
"Tgt",
|
|
"""<opc:ObjectType SymbolicName="SubDeviceType" BaseType="dep:DeviceType" />""",
|
|
"Dep");
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
validator.Validate([target], [dependency], null!);
|
|
|
|
var type = (TypeDesign)FindNode(validator, "SubDeviceType", "Tgt");
|
|
var limits = (VariableDesign)type.Hierarchy.NodeList
|
|
.Single(n => n.RelativePath == "Limits").Instance!;
|
|
var level = (VariableDesign)type.Hierarchy.NodeList
|
|
.Single(n => n.RelativePath == "Level").Instance!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(limits!.ValueRank, Is.EqualTo(ValueRank.ScalarOrArray));
|
|
Assert.That(level!.ValueRank, Is.EqualTo(ValueRank.Scalar));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// A method that references a method declaration through its
|
|
/// TypeDefinition keeps its own authored description.
|
|
/// </summary>
|
|
[Test]
|
|
public void MethodWithTypeDefinitionKeepsItsOwnDescription()
|
|
{
|
|
string path = AddDesign(
|
|
"Pumps",
|
|
"""
|
|
<opc:Method SymbolicName="StartMethodType">
|
|
<opc:Description>Starts the device.</opc:Description>
|
|
</opc:Method>
|
|
<opc:ObjectType SymbolicName="PumpType" BaseType="ua:BaseObjectType">
|
|
<opc:Children>
|
|
<opc:Method SymbolicName="Start" TypeDefinition="StartMethodType">
|
|
<opc:Description>Starts the pump.</opc:Description>
|
|
</opc:Method>
|
|
<opc:Method SymbolicName="Restart" TypeDefinition="StartMethodType" />
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
""");
|
|
ModelDesignValidator validator = CreateValidator();
|
|
|
|
validator.Validate([path], [], null!);
|
|
|
|
var type = (TypeDesign)FindNode(validator, "PumpType", "Pumps");
|
|
InstanceDesign start = type.Children.Items.Single(c => c.SymbolicName.Name == "Start");
|
|
InstanceDesign restart = type.Children.Items.Single(c => c.SymbolicName.Name == "Restart");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(start.Description?.Value, Is.EqualTo("Starts the pump."));
|
|
Assert.That(restart.Description?.Value, Is.EqualTo("Starts the device."));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// An inherited child whose name merely starts with the name of an
|
|
/// explicitly declared child (StatusCode vs Status) is not explicitly
|
|
/// defined and takes its identifier from the implicit range.
|
|
/// </summary>
|
|
[Test]
|
|
public void InheritedChildSharingANamePrefixIsNotExplicitlyDefined()
|
|
{
|
|
string path = AddDesign(
|
|
"Prefix",
|
|
"""
|
|
<opc:ObjectType SymbolicName="BaseMachineType" BaseType="ua:BaseObjectType">
|
|
<opc:Children>
|
|
<opc:Property SymbolicName="StatusCode" DataType="ua:Int32" ModellingRule="Optional" />
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
<opc:ObjectType SymbolicName="MachineType" BaseType="BaseMachineType">
|
|
<opc:Children>
|
|
<opc:Property SymbolicName="Status" DataType="ua:Int32" />
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
""");
|
|
ModelDesignValidator validator = CreateValidator(startId: 1);
|
|
|
|
validator.Validate([path], [], null!);
|
|
|
|
var type = (TypeDesign)FindNode(validator, "MachineType", "Prefix");
|
|
HierarchyNode status = type.Hierarchy.NodeList.Single(n => n.RelativePath == "Status");
|
|
HierarchyNode statusCode = type.Hierarchy.NodeList.Single(n => n.RelativePath == "StatusCode");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That((uint)status.Identifier!, Is.LessThan(1000000u));
|
|
Assert.That((uint)statusCode.Identifier!, Is.GreaterThanOrEqualTo(1000000u));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// The reference type of a child is validated: an unknown reference
|
|
/// type is reported instead of silently flowing into generated code.
|
|
/// </summary>
|
|
[TestCase("ua:HasComponnet", true)]
|
|
[TestCase("ua:HasOrderedComponent", false)]
|
|
public void UnknownChildReferenceTypeIsReported(string referenceType, bool reported)
|
|
{
|
|
string path = AddDesign(
|
|
"Refs",
|
|
$"""
|
|
<opc:ObjectType SymbolicName="HolderType" BaseType="ua:BaseObjectType">
|
|
<opc:Children>
|
|
<opc:Object SymbolicName="Part" TypeDefinition="ua:BaseObjectType">
|
|
<opc:ReferenceType>{referenceType}</opc:ReferenceType>
|
|
</opc:Object>
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
""");
|
|
var entries = new List<string>();
|
|
ModelDesignValidator validator = CreateValidator(
|
|
telemetry: new CapturingTelemetryContext(entries));
|
|
|
|
try
|
|
{
|
|
validator.Validate([path], [], null!);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// a later stage may reject the unknown reference type too.
|
|
}
|
|
|
|
Assert.That(
|
|
entries.Exists(e => e.Contains(referenceType[3..], StringComparison.Ordinal)),
|
|
Is.EqualTo(reported));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A matrix field of the built-in type dictionary keeps one
|
|
/// dimension per rank.
|
|
/// </summary>
|
|
[TestCase(-1, ValueRank.Scalar, null)]
|
|
[TestCase(1, ValueRank.Array, null)]
|
|
[TestCase(2, ValueRank.OneOrMoreDimensions, "0,0")]
|
|
[TestCase(3, ValueRank.OneOrMoreDimensions, "0,0,0")]
|
|
public void ImportFieldKeepsTheRankOfMatrixFields(
|
|
int rank,
|
|
ValueRank expectedRank,
|
|
string? expectedDimensions)
|
|
{
|
|
var field = new SchemaTypes.FieldType
|
|
{
|
|
Name = "Field",
|
|
DataType = new XmlQualifiedName("Double"),
|
|
ValueRank = rank
|
|
};
|
|
|
|
Parameter parameter = ModelDesignValidator.ImportField(field)!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(parameter!.ValueRank, Is.EqualTo(expectedRank));
|
|
Assert.That(parameter.ArrayDimensions, Is.EqualTo(expectedDimensions));
|
|
});
|
|
}
|
|
|
|
private static string MachineTypeWithMethods(string name, string baseType)
|
|
{
|
|
return $"""
|
|
<opc:ObjectType SymbolicName="{name}" BaseType="{baseType}">
|
|
<opc:Children>
|
|
<opc:Method SymbolicName="Reset" ModellingRule="Mandatory" />
|
|
<opc:Method SymbolicName="Start" ModellingRule="Mandatory">
|
|
<opc:InputArguments>
|
|
<opc:Argument Name="Speed" DataType="ua:Double" />
|
|
</opc:InputArguments>
|
|
</opc:Method>
|
|
<opc:Method SymbolicName="Stop" ModellingRule="Mandatory">
|
|
<opc:OutputArguments>
|
|
<opc:Argument Name="Duration" DataType="ua:Double" />
|
|
</opc:OutputArguments>
|
|
</opc:Method>
|
|
</opc:Children>
|
|
</opc:ObjectType>
|
|
""";
|
|
}
|
|
|
|
private static string NamespaceUri(string name)
|
|
{
|
|
return $"http://test.org/UA/{name}/";
|
|
}
|
|
|
|
private string AddDesign(string name, string body, params string[] imports)
|
|
{
|
|
var design = new StringBuilder();
|
|
design.Append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n")
|
|
.Append("<opc:ModelDesign\n")
|
|
.Append(" xmlns:opc=\"http://opcfoundation.org/UA/ModelDesign.xsd\"\n")
|
|
.Append(" xmlns:ua=\"http://opcfoundation.org/UA/\"\n")
|
|
.Append(" xmlns:uax=\"http://opcfoundation.org/UA/2008/02/Types.xsd\"\n");
|
|
foreach (string import in imports)
|
|
{
|
|
design.Append(" xmlns:").Append(import.ToLowerInvariant())
|
|
.Append("=\"").Append(NamespaceUri(import)).Append("\"\n");
|
|
}
|
|
design.Append(" xmlns=\"").Append(NamespaceUri(name)).Append("\"\n")
|
|
.Append(" TargetNamespace=\"").Append(NamespaceUri(name)).Append("\">\n")
|
|
.Append(" <opc:Namespaces>\n")
|
|
.Append(" <opc:Namespace Name=\"OpcUa\" Prefix=\"Opc.Ua\" ")
|
|
.Append("XmlNamespace=\"http://opcfoundation.org/UA/2008/02/Types.xsd\">")
|
|
.Append("http://opcfoundation.org/UA/</opc:Namespace>\n");
|
|
foreach (string ns in imports.Prepend(name))
|
|
{
|
|
design.Append(" <opc:Namespace Name=\"").Append(ns)
|
|
.Append("\" Prefix=\"Test.").Append(ns).Append("\">")
|
|
.Append(NamespaceUri(ns)).Append("</opc:Namespace>\n");
|
|
}
|
|
design.Append(" </opc:Namespaces>\n")
|
|
.Append(body).Append('\n')
|
|
.Append("</opc:ModelDesign>\n");
|
|
|
|
string path = $"memory://{name}.design.xml";
|
|
m_fileSystem.Add(path, Encoding.UTF8.GetBytes(design.ToString()));
|
|
return path;
|
|
}
|
|
|
|
private static NodeDesign FindNode(ModelDesignValidator validator, string name, string ns)
|
|
{
|
|
Assert.That(
|
|
validator.TryFindNode(
|
|
new XmlQualifiedName(name, NamespaceUri(ns)),
|
|
"Test",
|
|
"Node",
|
|
out NodeDesign? node),
|
|
Is.True,
|
|
name);
|
|
return node!;
|
|
}
|
|
|
|
private ModelDesignValidator CreateValidator(
|
|
uint startId = 1000,
|
|
ITelemetryContext? telemetry = null)
|
|
{
|
|
telemetry ??= NUnitTelemetryContext.Create(logLevel: LogLevel.Error);
|
|
IFileSystem fileSystem = typeof(ModelDesignValidator).Assembly
|
|
.AsFileSystem("Opc.Ua.SourceGeneration.Design")
|
|
.WithFallback(m_fileSystem);
|
|
return new ModelDesignValidator(
|
|
fileSystem,
|
|
startId,
|
|
null!,
|
|
telemetry,
|
|
SpecificationVersion.V105);
|
|
}
|
|
|
|
private sealed class CapturingTelemetryContext : TelemetryContextBase
|
|
{
|
|
public CapturingTelemetryContext(List<string> entries)
|
|
: base(new CapturingLoggerFactory(entries))
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class CapturingLoggerFactory : ILoggerFactory
|
|
{
|
|
public CapturingLoggerFactory(List<string> entries)
|
|
{
|
|
m_logger = new CapturingLogger(entries);
|
|
}
|
|
|
|
public void AddProvider(ILoggerProvider provider)
|
|
{
|
|
}
|
|
|
|
public ILogger CreateLogger(string categoryName)
|
|
{
|
|
return m_logger;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
|
|
private readonly CapturingLogger m_logger;
|
|
}
|
|
|
|
private sealed class CapturingLogger : ILogger
|
|
{
|
|
public CapturingLogger(List<string> entries)
|
|
{
|
|
m_entries = entries;
|
|
}
|
|
|
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull
|
|
{
|
|
return null!;
|
|
}
|
|
|
|
public bool IsEnabled(LogLevel logLevel)
|
|
{
|
|
return logLevel >= LogLevel.Warning;
|
|
}
|
|
|
|
public void Log<TState>(
|
|
LogLevel logLevel,
|
|
EventId eventId,
|
|
TState state,
|
|
Exception? exception,
|
|
Func<TState, Exception, string> formatter)
|
|
{
|
|
if (IsEnabled(logLevel))
|
|
{
|
|
m_entries.Add(formatter(state, exception!));
|
|
}
|
|
}
|
|
|
|
private readonly List<string> m_entries;
|
|
}
|
|
}
|
|
}
|