mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 00:56:28 +08:00
fc0687a2dc
# Description A Variable or VariableType in a NodeSet2 that has no Value element has a null Value (OPC 10000-6 F.5); the runtime NodeSet2 importer loads it that way. The source generator initialized such a node with the default value of its DataType instead ((int)0, (double)0, ...), so a server built from the generated code reported values the model never declared. The NodeSet2 importer now records that the node declares no Value (NodeSetValueIsNull), the flag follows the node into the merged instance hierarchy, and the node state generator emits Variant.Null for it when no value was merged in from elsewhere. ModelDesign inputs, where a missing DefaultValue implies the DataType default, are unchanged. The tests generate a DI-style NodeSet2 together with the DI NodeSet2 and check the generated values of a type's InstanceDeclaration, of an instance's children with and without a Value, and of a VariableType. ## Related Issues None. ## Checklist - [x] 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. - [x] 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. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
335 lines
15 KiB
C#
335 lines
15 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.Extensions.Logging;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.SourceGeneration;
|
|
using Opc.Ua.SourceGeneration.Dependency;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Schema.Model.Tests
|
|
{
|
|
/// <summary>
|
|
/// A NodeSet2 Variable or VariableType without a Value element has a
|
|
/// null Value (OPC 10000-6 F.5). The generator used to initialize such
|
|
/// a node with the default value of its DataType ((int)0, (double)0),
|
|
/// so a server built from the NodeSet reported a value the model never
|
|
/// declared.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("ModelDesign")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
public class NodeSetValueAbsentTests
|
|
{
|
|
private const string NullValue = "baseState.WrappedValue = global::Opc.Ua.Variant.Null;";
|
|
|
|
[TestCase("CreatePump1_RevisionCounter", "(int)0")]
|
|
[TestCase("CreatePump1_RatedFlow", "(double)0")]
|
|
[TestCase("CreatePumpType_RatedFlow", "(double)0")]
|
|
public void VariableWithoutValueIsNull(string method, string dataTypeDefault)
|
|
{
|
|
string body = ExtractMethodBody(GenerateDeviceSetPump(), method);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(body, Does.Contain(NullValue));
|
|
Assert.That(body, Does.Not.Contain(dataTypeDefault));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void VariableWithValueKeepsIt()
|
|
{
|
|
string body = ExtractMethodBody(GenerateDeviceSetPump(), "CreatePump1_SerialNumber");
|
|
|
|
Assert.That(
|
|
body,
|
|
Does.Contain("baseState.WrappedValue = global::Opc.Ua.Variant.From(\"SN-0001\");"));
|
|
}
|
|
|
|
[Test]
|
|
public void VariableTypeWithoutValueIsNull()
|
|
{
|
|
string body = ExtractMethodBody(GenerateDeviceSetPump(), "CreateFlowVariableType");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(body, Does.Contain(NullValue));
|
|
Assert.That(body, Does.Not.Contain("(double)0"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void InstanceOfVariableTypeWithoutValueIsNull()
|
|
{
|
|
string body = ExtractMethodBody(GenerateDeviceSetPump(), "CreateInstanceOfFlowVariableType");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(body, Does.Contain(NullValue));
|
|
Assert.That(body, Does.Not.Contain("(double)0"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void SubtypeWithoutValueDoesNotInheritTheValueOfItsSupertype()
|
|
{
|
|
string generated = GenerateDeviceSetPump();
|
|
string supertype = ExtractMethodBody(generated, "CreateInstanceOfRatedFlowVariableType");
|
|
string subtype = ExtractMethodBody(generated, "CreateInstanceOfUnratedFlowVariableType");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(supertype, Does.Contain("42.5"), "the supertype keeps its declared Value");
|
|
Assert.That(subtype, Does.Contain(NullValue));
|
|
Assert.That(subtype, Does.Not.Contain("42.5"));
|
|
Assert.That(subtype, Does.Not.Contain("(double)0"));
|
|
});
|
|
}
|
|
|
|
[TestCase("CreateUnsetRatedFlow")]
|
|
[TestCase("CreateRatedFlowMeter_UnsetFlow")]
|
|
[TestCase("CreateRatedFlowMeter1_RatedFlow")]
|
|
public void VariableWithoutValueDoesNotInheritTheValueOfItsVariableType(string method)
|
|
{
|
|
string body = ExtractMethodBody(GenerateDeviceSetPump(), method);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(body, Does.Contain(NullValue));
|
|
Assert.That(body, Does.Not.Contain("42.5"));
|
|
Assert.That(body, Does.Not.Contain("12.5"));
|
|
Assert.That(body, Does.Not.Contain("(double)0"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void VariableWithValueOverridesTheValueOfItsVariableType()
|
|
{
|
|
string body = ExtractMethodBody(GenerateDeviceSetPump(), "CreateRatedFlowMeter_SetFlow");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(body, Does.Contain("7.25"));
|
|
Assert.That(body, Does.Not.Contain("42.5"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void DependencyPayloadCarriesTheAbsentValue()
|
|
{
|
|
var dependency = new ModelDependencyV1 { ModelUri = "http://test.org/UA/ValueAbsent/" };
|
|
dependency.Nodes.Add(new DependencyNode
|
|
{
|
|
SymbolicName = "PumpType",
|
|
SymbolicNamespace = "http://test.org/UA/ValueAbsent/",
|
|
ClassName = "PumpType",
|
|
Kind = DependencyNodeKind.ObjectType,
|
|
Children =
|
|
[
|
|
new DependencyChild
|
|
{
|
|
BrowseName = "RatedFlow",
|
|
SymbolicName = "RatedFlow",
|
|
InstanceKind = 3,
|
|
NodeSetValueIsNull = true
|
|
},
|
|
new DependencyChild
|
|
{
|
|
BrowseName = "SerialNumber",
|
|
SymbolicName = "SerialNumber",
|
|
InstanceKind = 3,
|
|
DefaultValueXml = "<String xmlns=\"http://opcfoundation.org/UA/2008/02/Types.xsd\">SN</String>"
|
|
}
|
|
]
|
|
});
|
|
|
|
ModelDependencyV1? decoded = ModelDependencyV1.FromBase64Payload(dependency.ToBase64Payload());
|
|
|
|
Assert.That(decoded, Is.Not.Null);
|
|
IReadOnlyList<DependencyChild> children = decoded!.Nodes.Single().Children;
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(children[0].NodeSetValueIsNull, Is.True);
|
|
Assert.That(children[1].NodeSetValueIsNull, Is.False);
|
|
Assert.That(children[1].DefaultValueXml, Does.Contain("SN"));
|
|
});
|
|
}
|
|
|
|
private static string GenerateDeviceSetPump()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error);
|
|
using var fileSystem = new VirtualFileSystem();
|
|
string resources = Path.Combine(Directory.GetCurrentDirectory(), "Resources");
|
|
const string flowTypePath = "memory://FlowVariableType.NodeSet2.xml";
|
|
fileSystem.Add(flowTypePath, Encoding.UTF8.GetBytes(FlowVariableTypeNodeSet));
|
|
var nodesets = new NodesetFileCollection(
|
|
[
|
|
(Path.Combine(resources, "Opc.Ua.Di.NodeSet2.xml"), new NodesetFileOptions()),
|
|
(Path.Combine(resources, "DeviceSetPump.NodeSet2.xml"), new NodesetFileOptions()),
|
|
(flowTypePath, new NodesetFileOptions())
|
|
],
|
|
[],
|
|
fileSystem,
|
|
telemetry);
|
|
nodesets.GenerateCode(
|
|
fileSystem,
|
|
string.Empty,
|
|
telemetry,
|
|
new GeneratorOptions { OmitFluentApi = true });
|
|
return string.Join(
|
|
Environment.NewLine,
|
|
fileSystem.CreatedFiles
|
|
.Where(c => Path.GetExtension(c) == ".cs")
|
|
.Select(c => Encoding.UTF8.GetString(fileSystem.Get(c))));
|
|
}
|
|
|
|
private static string ExtractMethodBody(string code, string methodName)
|
|
{
|
|
Match match = Regex.Match(code, @"\s" + Regex.Escape(methodName) + @"\(\s*this\s");
|
|
Assert.That(match.Success, Is.True, $"Method {methodName} was not generated.");
|
|
int end = code.IndexOf("return state;", match.Index, StringComparison.Ordinal);
|
|
Assert.That(end, Is.GreaterThan(match.Index));
|
|
return code[match.Index..end];
|
|
}
|
|
|
|
private const string FlowVariableTypeNodeSet = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
|
|
<NamespaceUris>
|
|
<Uri>http://test.org/UA/FlowVariableType/</Uri>
|
|
</NamespaceUris>
|
|
<Models>
|
|
<Model ModelUri="http://test.org/UA/FlowVariableType/" PublicationDate="2026-10-01T00:00:00Z" Version="1.0.0" />
|
|
</Models>
|
|
<Aliases>
|
|
<Alias Alias="Double">i=11</Alias>
|
|
<Alias Alias="HasSubtype">i=45</Alias>
|
|
<Alias Alias="HasTypeDefinition">i=40</Alias>
|
|
<Alias Alias="Organizes">i=35</Alias>
|
|
<Alias Alias="HasComponent">i=47</Alias>
|
|
<Alias Alias="HasModellingRule">i=37</Alias>
|
|
</Aliases>
|
|
<UAVariableType NodeId="ns=1;i=2000" BrowseName="1:FlowVariableType" DataType="Double">
|
|
<DisplayName>FlowVariableType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasSubtype" IsForward="false">i=63</Reference>
|
|
</References>
|
|
</UAVariableType>
|
|
<UAVariableType NodeId="ns=1;i=2001" BrowseName="1:RatedFlowVariableType" DataType="Double">
|
|
<DisplayName>RatedFlowVariableType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasSubtype" IsForward="false">i=63</Reference>
|
|
</References>
|
|
<Value>
|
|
<Double xmlns="http://opcfoundation.org/UA/2008/02/Types.xsd">42.5</Double>
|
|
</Value>
|
|
</UAVariableType>
|
|
<UAVariableType NodeId="ns=1;i=2002" BrowseName="1:UnratedFlowVariableType" DataType="Double">
|
|
<DisplayName>UnratedFlowVariableType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasSubtype" IsForward="false">ns=1;i=2001</Reference>
|
|
</References>
|
|
</UAVariableType>
|
|
<UAVariable NodeId="ns=1;i=5000" BrowseName="1:UnsetRatedFlow" DataType="Double">
|
|
<DisplayName>UnsetRatedFlow</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">ns=1;i=2001</Reference>
|
|
<Reference ReferenceType="Organizes" IsForward="false">i=85</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
<UAObject NodeId="ns=1;i=5001" BrowseName="1:RatedFlowMeter">
|
|
<DisplayName>RatedFlowMeter</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">i=58</Reference>
|
|
<Reference ReferenceType="Organizes" IsForward="false">i=85</Reference>
|
|
<Reference ReferenceType="HasComponent">ns=1;i=5002</Reference>
|
|
<Reference ReferenceType="HasComponent">ns=1;i=5003</Reference>
|
|
</References>
|
|
</UAObject>
|
|
<UAVariable NodeId="ns=1;i=5002" BrowseName="1:UnsetFlow" ParentNodeId="ns=1;i=5001" DataType="Double">
|
|
<DisplayName>UnsetFlow</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">ns=1;i=2001</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
<UAVariable NodeId="ns=1;i=5003" BrowseName="1:SetFlow" ParentNodeId="ns=1;i=5001" DataType="Double">
|
|
<DisplayName>SetFlow</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">ns=1;i=2001</Reference>
|
|
</References>
|
|
<Value>
|
|
<Double xmlns="http://opcfoundation.org/UA/2008/02/Types.xsd">7.25</Double>
|
|
</Value>
|
|
</UAVariable>
|
|
<UAObjectType NodeId="ns=1;i=1001" BrowseName="1:RatedFlowMeterType">
|
|
<DisplayName>RatedFlowMeterType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
|
|
<Reference ReferenceType="HasComponent">ns=1;i=6000</Reference>
|
|
</References>
|
|
</UAObjectType>
|
|
<UAVariable NodeId="ns=1;i=6000" BrowseName="1:RatedFlow" ParentNodeId="ns=1;i=1001" DataType="Double">
|
|
<DisplayName>RatedFlow</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">ns=1;i=2001</Reference>
|
|
<Reference ReferenceType="HasModellingRule">i=78</Reference>
|
|
</References>
|
|
<Value>
|
|
<Double xmlns="http://opcfoundation.org/UA/2008/02/Types.xsd">12.5</Double>
|
|
</Value>
|
|
</UAVariable>
|
|
<UAObject NodeId="ns=1;i=5004" BrowseName="1:RatedFlowMeter1">
|
|
<DisplayName>RatedFlowMeter1</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">ns=1;i=1001</Reference>
|
|
<Reference ReferenceType="Organizes" IsForward="false">i=85</Reference>
|
|
<Reference ReferenceType="HasComponent">ns=1;i=5005</Reference>
|
|
</References>
|
|
</UAObject>
|
|
<UAVariable NodeId="ns=1;i=5005" BrowseName="1:RatedFlow" ParentNodeId="ns=1;i=5004" DataType="Double">
|
|
<DisplayName>RatedFlow</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="HasTypeDefinition">ns=1;i=2001</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
</UANodeSet>
|
|
""";
|
|
}
|
|
}
|