/* ======================================================================== * 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 { /// /// 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. /// [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 = "SN" } ] }); ModelDependencyV1? decoded = ModelDependencyV1.FromBase64Payload(dependency.ToBase64Payload()); Assert.That(decoded, Is.Not.Null); IReadOnlyList 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 = """ http://test.org/UA/FlowVariableType/ i=11 i=45 i=40 i=35 i=47 i=37 FlowVariableType i=63 RatedFlowVariableType i=63 42.5 UnratedFlowVariableType ns=1;i=2001 UnsetRatedFlow ns=1;i=2001 i=85 RatedFlowMeter i=58 i=85 ns=1;i=5002 ns=1;i=5003 UnsetFlow ns=1;i=2001 SetFlow ns=1;i=2001 7.25 RatedFlowMeterType i=58 ns=1;i=6000 RatedFlow ns=1;i=2001 i=78 12.5 RatedFlowMeter1 ns=1;i=1001 i=85 ns=1;i=5005 RatedFlow ns=1;i=2001 """; } }